Memory Optimization: Backend Threading/GC + Frontend Monaco Lazy-Loading - #28
Conversation
…migrate WebClient to JDK HttpClient
- Cap JVM heap (-Xms64m, -Xmx256m) and Metaspace (-XX:MaxMetaspaceSize=192m) across jpackage and Gradle run tasks
- Restrict GC threads (-XX:ParallelGCThreads=2, -XX:ConcGCThreads=1) to prevent CPU-core-based thread inflation
- Replace embedded Tomcat with Jetty 12 and restrict worker thread pool (max 10, min 2)
- Tune HikariCP connection pool for SQLite (max pool size 2, min idle 1) and disable Spring JMX
- Migrate GitHub API integration from WebFlux/Netty WebClient to JDK 21 HttpClient via Spring 6.1 RestClient
- Refactor release download stream in DownloadUpdateImpl to use 8KB buffered InputStream with SseEmitter
…t-level cleanup and resource throttling
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (6)
📝 WalkthroughWalkthroughO PR migra a atualização para RestClient, InputStream, callbacks e SSE assíncrono; configura Jetty e o runtime JVM; melhora o ciclo de vida desktop; e atualiza visualização de código, cache, auto-lock, foco, Monaco e estilos do frontend. ChangesPipeline de atualização
Interface frontend
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ReleaseController
participant BackgroundTaskRunner
participant DownloadUpdateImpl
participant GitHubClient
Client->>ReleaseController: POST /download-and-install
ReleaseController->>BackgroundTaskRunner: run download task
BackgroundTaskRunner->>DownloadUpdateImpl: execute(onProgress)
DownloadUpdateImpl->>GitHubClient: downloadAsset(downloadUrl)
GitHubClient-->>DownloadUpdateImpl: InputStream
DownloadUpdateImpl-->>ReleaseController: progress callback
ReleaseController-->>Client: SSE progress event
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
- Add explicit listener detachment for WebView's loadWorker to prevent lingering state listeners. - Gracefully release resources on app shutdown, including WebView and splash scene cleanup. - Encapsulate splash logo loading with try-catch to handle potential I/O failures without blocking startup.
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/src/main/java/com/devaulty/backend/adapter/in/web/release/ReleaseApi.java (1)
60-64: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winA resposta
400documentada não é mais alcançável neste endpoint.
UpdateNotAvailableExceptionagora é lançada dentro derunDownloadna thread de background (ReleaseControllerlinhas 59-66), depois que o SSE já foi estabelecido; o cliente recebe um erro terminal no stream, não um400JSON. Vale alinhar esta descrição com o comportamento real (como já foi feito para o500).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/main/java/com/devaulty/backend/adapter/in/web/release/ReleaseApi.java` around lines 60 - 64, Atualize a anotação de resposta `400` em `ReleaseApi` para refletir que esse endpoint não retorna mais um `400` JSON quando não há atualização ou instalador compatível. Remova essa resposta documentada, mantendo a documentação alinhada ao erro terminal enviado pelo SSE após `runDownload` em `ReleaseController` e às respostas já existentes, incluindo `500`.
🧹 Nitpick comments (2)
backend/src/main/java/com/devaulty/backend/adapter/out/external/github/common/GitHubConfig.java (1)
26-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueJavadoc deslocado.
O comentário “Default RestClient for GitHub API requests.” descreve
githubRestClient, mas está acima desharedHttpClient.♻️ Ajuste sugerido
- /** - * Default RestClient for GitHub API requests. - */ `@Bean` public HttpClient sharedHttpClient() { return HttpClient.newBuilder() .connectTimeout(CONNECT_TIMEOUT) .followRedirects(HttpClient.Redirect.NORMAL) .build(); } + /** + * Default RestClient for GitHub API requests. + */ `@Bean`(name = "githubRestClient")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/main/java/com/devaulty/backend/adapter/out/external/github/common/GitHubConfig.java` around lines 26 - 35, Corrija o Javadoc acima de sharedHttpClient para descrever o cliente HTTP compartilhado, ou mova “Default RestClient for GitHub API requests.” para o método githubRestClient correspondente, mantendo cada comentário associado ao bean correto.backend/src/main/java/com/devaulty/backend/adapter/in/web/common/BackgroundTaskRunner.java (1)
18-22: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueConsidere thread nomeada/daemon e proteção contra tarefas concorrentes enfileiradas.
newSingleThreadExecutor()cria thread não-daemon anônima com fila ilimitada: múltiplas requisições de download ficam enfileiradas (osSseEmitteranteriores expiram) e o encerramento depende do@PreDestroy. Nomear a thread facilita o diagnóstico; rejeitar/ignorar um novo download enquanto outro está em andamento evita downloads redundantes.♻️ Sugestão
- private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable, "devaulty-background-task"); + thread.setDaemon(true); + return thread; + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/main/java/com/devaulty/backend/adapter/in/web/common/BackgroundTaskRunner.java` around lines 18 - 22, Atualize BackgroundTaskRunner para criar uma thread nomeada e daemon no executor, facilitando diagnóstico e encerramento. Adicione proteção no método run para aceitar apenas uma tarefa enquanto outra estiver em execução, rejeitando ou ignorando novas tarefas enfileiradas; libere o estado ao concluir a tarefa, inclusive em caso de erro.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@backend/src/main/java/com/devaulty/backend/adapter/in/web/release/ReleaseController.java`:
- Around line 68-74: Atualize o fluxo de download em runDownloadProcess e
sendProgress para tratar desconexões do cliente como cancelamento previsível.
Registre callbacks onError, onTimeout e onCompletion no SseEmitter, sinalizando
o cancelamento para interromper o processamento e remover o arquivo parcial. Em
sendProgress, trate também IllegalStateException após o encerramento do emitter,
evitando que a exceção escape do callback.
In
`@backend/src/main/java/com/devaulty/backend/adapter/out/external/github/GitHubClient.java`:
- Around line 47-51: Atualize o método downloadAsset para validar o status HTTP
da resposta antes de retornar response.getBody(). Rejeite respostas 4xx/5xx com
a exceção de erro apropriada do cliente REST, mantendo o stream aberto para o
consumidor apenas quando a resposta for bem-sucedida.
In
`@backend/src/main/java/com/devaulty/backend/application/impl/release/DownloadUpdateImpl.java`:
- Around line 113-134: Antes de chamar installUpdateUseCase.execute(targetPath)
no fluxo de DownloadUpdateImpl, encerre o try-with-resources do OutputStream
para garantir que os dados sejam descarregados e valide que downloadedBytes
corresponde a totalBytes quando o tamanho total for conhecido. Só prossiga para
INSTALLING e instalação após essa validação; trate uma divergência como falha do
download, sem executar o instalador.
In `@backend/src/main/java/com/devaulty/backend/desktop/DevaultyDesktop.java`:
- Around line 117-120: Separe o tratamento de Worker.State.CANCELLED de
Worker.State.FAILED no listener de carregamento que chama showErrorAndExit. Para
cancelamentos, forneça uma causa própria ou torne showErrorAndExit nulo-seguro
antes de acessar cause, preservando o encerramento esperado sem
NullPointerException; mantenha o tratamento da exceção real para FAILED.
In `@frontend/src/components/CodeViewer.tsx`:
- Line 19: Update CodeViewer’s line-gutter rendering so it does not create a DOM
node for every line in arbitrarily large content. Add virtualization or an
explicit limit/pagination around the lines derived from the code value, while
preserving correct line numbering and code display for the rendered portion.
In `@frontend/src/features/snippets/components/SnippetForm.tsx`:
- Around line 64-93: Atualize o fluxo de layout do editor em SnippetForm para
armazenar o timer de setTimeout em uma ref e cancelá-lo no cleanup do unmount,
antes de descartar o Monaco editor. Garanta que o callback não execute após
editorRef.current ter sido descartado.
In `@frontend/src/features/snippets/components/SnippetsWorkspace.tsx`:
- Around line 297-307: Atualize o bloco que renderiza o SnippetForm para que
React.Suspense e o próprio SnippetForm só sejam montados quando
isSnippetFormOpen for true. Preserve as props existentes e o comportamento de
fechamento, evitando disparar o carregamento lazy do formulário e do Monaco
enquanto o modal estiver fechado.
In `@frontend/src/hooks/useInactivityAutoLock.ts`:
- Around line 6-11: Atualize useInactivityAutoLock para registrar separadamente
o horário da última atividade real, sem tratá-la como um reset de timer quando
ocorrer dentro de THROTTLE_MS. Ao programar o auto-lock, calcule o atraso
restante a partir dessa atividade mais recente, garantindo que o vault nunca
seja bloqueado antes do período completo de inatividade; preserve o throttling
das redefinições efetivas.
---
Outside diff comments:
In
`@backend/src/main/java/com/devaulty/backend/adapter/in/web/release/ReleaseApi.java`:
- Around line 60-64: Atualize a anotação de resposta `400` em `ReleaseApi` para
refletir que esse endpoint não retorna mais um `400` JSON quando não há
atualização ou instalador compatível. Remova essa resposta documentada, mantendo
a documentação alinhada ao erro terminal enviado pelo SSE após `runDownload` em
`ReleaseController` e às respostas já existentes, incluindo `500`.
---
Nitpick comments:
In
`@backend/src/main/java/com/devaulty/backend/adapter/in/web/common/BackgroundTaskRunner.java`:
- Around line 18-22: Atualize BackgroundTaskRunner para criar uma thread nomeada
e daemon no executor, facilitando diagnóstico e encerramento. Adicione proteção
no método run para aceitar apenas uma tarefa enquanto outra estiver em execução,
rejeitando ou ignorando novas tarefas enfileiradas; libere o estado ao concluir
a tarefa, inclusive em caso de erro.
In
`@backend/src/main/java/com/devaulty/backend/adapter/out/external/github/common/GitHubConfig.java`:
- Around line 26-35: Corrija o Javadoc acima de sharedHttpClient para descrever
o cliente HTTP compartilhado, ou mova “Default RestClient for GitHub API
requests.” para o método githubRestClient correspondente, mantendo cada
comentário associado ao bean correto.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: dd69efc2-86ea-49c4-9a4f-398fd253c80a
📒 Files selected for processing (36)
backend/build.gradle.ktsbackend/src/main/java/com/devaulty/backend/adapter/in/web/common/BackgroundTaskRunner.javabackend/src/main/java/com/devaulty/backend/adapter/in/web/common/UriLocationBuilderHelper.javabackend/src/main/java/com/devaulty/backend/adapter/in/web/credential/CredentialController.javabackend/src/main/java/com/devaulty/backend/adapter/in/web/link/LinkController.javabackend/src/main/java/com/devaulty/backend/adapter/in/web/note/NoteController.javabackend/src/main/java/com/devaulty/backend/adapter/in/web/problem/ProblemController.javabackend/src/main/java/com/devaulty/backend/adapter/in/web/project/ProjectController.javabackend/src/main/java/com/devaulty/backend/adapter/in/web/release/ReleaseApi.javabackend/src/main/java/com/devaulty/backend/adapter/in/web/release/ReleaseController.javabackend/src/main/java/com/devaulty/backend/adapter/in/web/snippet/SnippetController.javabackend/src/main/java/com/devaulty/backend/adapter/in/web/tag/TagController.javabackend/src/main/java/com/devaulty/backend/adapter/out/external/github/GitHubClient.javabackend/src/main/java/com/devaulty/backend/adapter/out/external/github/GitHubReleaseAdapter.javabackend/src/main/java/com/devaulty/backend/adapter/out/external/github/common/GitHubConfig.javabackend/src/main/java/com/devaulty/backend/application/impl/release/DownloadUpdateImpl.javabackend/src/main/java/com/devaulty/backend/application/port/in/release/DownloadUpdateUseCase.javabackend/src/main/java/com/devaulty/backend/application/port/out/external/release/ReleasePort.javabackend/src/main/java/com/devaulty/backend/desktop/DevaultyDesktop.javabackend/src/main/resources/application.yamlbackend/src/test/java/com/devaulty/backend/adapter/in/web/release/ReleaseControllerIT.javabackend/src/test/java/com/devaulty/backend/adapter/out/external/github/GitHubClientTest.javabackend/src/test/java/com/devaulty/backend/application/impl/release/DownloadUpdateImplTest.javafrontend/src/components/CodeViewer.tsxfrontend/src/components/HackerLogo.module.cssfrontend/src/features/credentials/components/CredentialForm.tsxfrontend/src/features/links/components/LinkForm.tsxfrontend/src/features/notes/components/NoteForm.tsxfrontend/src/features/problems/components/ProblemsWorkspace.tsxfrontend/src/features/problems/hooks/useProblems.tsfrontend/src/features/snippets/components/SnippetForm.tsxfrontend/src/features/snippets/components/SnippetsWorkspace.tsxfrontend/src/features/snippets/hooks/useSnippets.tsfrontend/src/hooks/useInactivityAutoLock.tsfrontend/src/main.tsxfrontend/src/routes/__root.module.css
💤 Files with no reviewable changes (1)
- frontend/src/routes/__root.module.css
… progress handling in the backend, and add error checking to the GitHub download client.
Memory Optimization: Backend Threading/GC + Frontend Monaco Lazy-Loading
Context
Baseline investigation (
jcmd, NMT,/proc/<pid>/smaps_rollup, thread dumps) showed the packaged desktop app sitting at ~1.04–1.09GB RSS at rest, with only ~92MB of that being live JVM heap. Root cause breakdown:spring-boot-starter-webclient) spun up 24 event-loop threads sized to CPU core count (12c/24t host), plus ~18 G1 GC threads scaled the same way, despite the app being a single-user desktop tool@monaco-editor/reactat the top of the default project tab (SnippetsWorkspace.tsx), so the Monaco engine (parser, Web Workers, canvas) was initialized on app launch even for read-only content (stack traces, solution scripts)Backend Changes
-Xms64m,-Xmx256m) and Metaspace (-XX:MaxMetaspaceSize=192m) across jpackage and Gradle run tasks-XX:ParallelGCThreads=2,-XX:ConcGCThreads=1) to prevent CPU-core-based thread inflationWebClientto JDK 21HttpClientvia Spring 6.1RestClientDownloadUpdateImplto use 8KB bufferedInputStreamwithSseEmitterWhy RestClient over WebClient
The rest of the backend runs on blocking Spring MVC (Tomcat → Jetty), not WebFlux. Keeping a reactive
WebClientaround solely for outbound GitHub calls meant a second, independent concurrency model (Reactor Netty's event loop) coexisting with the blocking servlet stack — the direct source of those 24 idle threads. Switching to JDKHttpClientremoves the Netty/Reactor dependency entirely and drops the release-download/update-check call sites to a singleHttpClient-SelectorManagerthread.SSE download pipeline
DownloadUpdateUseCase.execute()changed from returning aFlux<UpdateProgressInfo>to accepting aConsumer<UpdateProgressInfo> onProgress. Progress is streamed to the client viaSseEmitter, with the blocking download itself dispatched onto a dedicated single-threadBackgroundTaskRunner(Spring-managed bean,@PreDestroy-aware shutdown) so the SSE-initiating servlet thread is never held for the duration of a (potentially 10+ minute) installer download.Frontend Changes
CodeViewer.tsx— a lightweight<pre><code>component (with line-number gutter) for read-only code/log display, replacing Monaco in non-editing contextsSnippetsWorkspace.tsx/ProblemsWorkspace.tsx: replaced Monaco Editor instances withCodeViewerfor read-only views (snippet preview, stack trace log, solution script); Monaco is no longer imported/initialized on default project loadProblemsWorkspace.tsx: removed dynamickey={"err-" + id}/key={"sol-" + id}props that forced full Monaco teardown/recreation on every list item clickSnippetForm.tsx(the only remaining Monaco instance, used for actual editing): addedonMountref capture and unmount cleanup callingmodel.dispose()+editor.dispose(); switchedautomaticLayout: true→falsewith a manualeditor.layout()call on window resize; addedclearTimeoutcleanup for the input-focus timeruseInactivityAutoLock.ts: throttledresetTimer()invocation to once per 5s (previously fired on everymousemove/scrollevent, generating a new timer closure 60–120×/sec)main.tsx: set global React QuerygcTimeto 60s (previously unset, defaulting to 5min)useSnippets.ts/useProblems.ts: setgcTime: 0on detail queries (useSnippetQuery,useProblemQuery) so closed item payloads are purged from memory immediatelySnippetForm.tsx/NoteForm.tsx/LinkForm.tsx/CredentialForm.tsx: addedclearTimeoutcleanup for input-focus timersMeasured Impact
All measurements taken on packaged
.debbuild, idle state, viajcmd <pid> GC.run && jcmd <pid> GC.heap_infoand/proc/<pid>/smaps_rollup.Backend changes reduced thread count by ~42% (115 → 67) and capped the heap ceiling (336MB → 210MB), without changing the live heap footprint — confirming the JVM was never the dominant contributor. Frontend changes addressed the actual majority contributor: eliminating Monaco's eager initialization dropped idle RSS by an additional ~122MB. With the editor legitimately open for editing, RSS returns to near-baseline (~983MB), isolating Monaco's own footprint at ~100MB when active — expected and acceptable, since it's no longer running when not needed.
Remaining Work (before merging out of draft)
DevaultyDesktop.java: releasesplashStagereference after close, wrap splash image load in try-with-resources, removeLoadWorkerlistener after page loadSnippetForm(dispose lifecycle) via repeated open/close measurementsmaps_rollupbreakdown post-WebView fixes to quantify remaining native/WebKitGTK baselineSummary by CodeRabbit
Novos Recursos
Correções
Desempenho e Estabilidade