Skip to content

Add a memory usage view - #263

Merged
sproctor merged 2 commits into
masterfrom
memory-usage-view
Aug 10, 2026
Merged

Add a memory usage view#263
sproctor merged 2 commits into
masterfrom
memory-usage-view

Conversation

@sproctor

@sproctor sproctor commented Aug 10, 2026

Copy link
Copy Markdown
Owner

A user reports large memory growth and there was no way for them to tell us where it was going. Help > Memory usage breaks down what the app is retaining, in the app's own terms.

What it shows

Per connection, one row per game window:

column meaning
Shown lines currently visible, after any name filter
Buffered / max lines in the scrollback buffer against its cap
Held lines still referenced, including ones evicted from the buffer but still pinned by the displayed list's lazily-compacted backing
Component refs entries in the stream's component index
Est. size modelled bytes, for ranking windows against each other

Plus window/panel/script counts per connection, and one heap used/max line for scale.

Counts are exact. Sizes are a documented cost model (MemoryEstimate) rather than measurements — the JVM offers no in-process way to size an object graph — so they are for comparing windows, not for reporting heap.

  • Copy report puts a plain-text version on the clipboard to paste into an issue.
  • Save heap dump writes a live-objects .hprof for when the breakdown is not enough. That goes through HotSpotDiagnosticMXBean, so jdk.management joins the packaged runtime image.

Notes

Streams measure themselves on the work queue that owns their buffers rather than racing the UI thread, which is why the registry call suspends. Both the title and the usage read are timeout-bounded, so a wedged connection degrades to "did not respond" instead of hanging the dialog.

"Component refs" is a column rather than an internal detail because removeLines deliberately never prunes componentLocations:

// Intentionally leak components here. They don't exist in the main window,
// and no other windows get long enough

On a stream that does receive components that index grows for the life of the connection. This PR does not change that behaviour, only surfaces it; memoryUsageReportsUnprunedComponentIndex pins it (40 lines appended into a 4-line buffer leaves 4 buffered lines and 40 component references).

Testing

Three new stream tests over the accounting, plus the existing suites. Desktop app launched and the dialog verified on screen. jdk.management confirmed present in the packaged runtime image via createDistributable, with the packaged binary's --version exiting 0. dumpHeap(path, live = true) checked on JDK 25, producing a valid JAVA PROFILE 1.0.2 file.

Not exercised: the mobile UI (desktop-only feature), and the heap dump button itself was not clicked in a GUI session — the MXBean call it makes was verified separately.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a Memory Usage dialog accessible from the Help menu.
    • View heap usage, per-connection memory estimates, panels, component references, and running scripts.
    • Refresh usage data, copy reports, and save heap dumps with progress and error feedback.
    • Added bounded memory accounting for buffered, displayed, cached, and styled stream content.
    • Reports remain responsive when individual connections are unavailable or slow.
  • Tests

    • Added coverage for memory estimates, capacity limits, and retained component references.

A user reports large memory growth, and there was no way for them to say
where it was going. Help > Memory usage now breaks down what each
connection retains: per window, the lines shown, the lines buffered
against the cap, the lines still held (including ones evicted from the
buffer but still pinned by the displayed list's lazily-compacted
backing), the size of the component index, and an estimated size. Plus
panels, running scripts, and one heap used/max line for scale.

Counts are exact. Sizes are a documented cost model - the JVM has no
in-process way to size an object graph - so they rank windows against
each other rather than reporting heap. "Copy report" produces a
plain-text version to paste into an issue, and "Save heap dump" writes a
live-objects hprof for when the breakdown is not enough. That needs
HotSpotDiagnosticMXBean, so jdk.management joins the packaged runtime.

Streams measure themselves on the work queue that owns their buffers,
rather than racing the UI thread, which makes the registry call suspend.
Both the title and usage reads are timeout-bounded, so a wedged
connection degrades to "did not respond" instead of hanging the dialog.

The component index count is a column because removeLines deliberately
never prunes it, so on a stream that receives components it grows for
the life of the connection; the new test pins that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b13ad2d0-0d85-4d61-bba5-15d606e5095b

📥 Commits

Reviewing files that changed from the base of the PR and between e2daa7f and 02d1e8c.

📒 Files selected for processing (2)
  • compose/src/commonMain/kotlin/warlockfe/warlock3/compose/ui/window/ComposeTextStream.kt
  • desktopApp/src/main/kotlin/warlockfe/warlock3/app/MemoryUsageDialog.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • compose/src/commonMain/kotlin/warlockfe/warlock3/compose/ui/window/ComposeTextStream.kt
  • desktopApp/src/main/kotlin/warlockfe/warlock3/app/MemoryUsageDialog.kt

📝 Walkthrough

Walkthrough

The change adds memory usage models and APIs for streams, windows, and games. It adds stream accounting tests and a desktop dialog that displays metrics, copies reports, refreshes data, and saves live heap dumps.

Changes

Memory usage reporting

Layer / File(s) Summary
Memory usage contracts
core/src/commonMain/kotlin/warlockfe/warlock3/core/window/MemoryUsage.kt, core/src/commonMain/kotlin/warlockfe/warlock3/core/window/WindowRegistry.kt
Defines stream and window memory metrics, byte estimation constants, and the suspending registry API.
Stream memory accounting
compose/src/commonMain/kotlin/warlockfe/warlock3/compose/ui/window/ComposeTextStream.kt, compose/src/jvmTest/kotlin/ComposeTextStreamTest.kt
Calculates retained stream content, references, spans, image URLs, and estimated bytes. Tests cover exact metrics, bounded growth, and retained component references.
Window and game reporting
compose/src/commonMain/kotlin/warlockfe/warlock3/compose/ui/window/WindowRegistryImpl.kt, compose/src/jvmBenchmark/kotlin/warlockfe/warlock3/compose/util/StreamNetworkBenchmark.kt, compose/src/commonMain/kotlin/warlockfe/warlock3/compose/ui/game/GameViewModel.kt
Aggregates stream usage and panel counts. Exposes window memory usage and running script counts.
Desktop memory usage dialog
desktopApp/src/main/kotlin/warlockfe/warlock3/app/MemoryUsageDialog.kt, desktopApp/src/main/kotlin/warlockfe/warlock3/app/WarlockApp.kt, desktopApp/src/main/kotlin/warlockfe/warlock3/app/TitleBarView.kt, desktopApp/src/main/kotlin/warlockfe/warlock3/app/Main.kt, desktopApp/build.gradle.kts
Adds the Help-menu action, dialog rendering, bounded report collection, clipboard output, heap-dump generation, shared game-state wiring, and the jdk.management runtime module.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant TitleBarView
  participant WarlockApp
  participant MemoryUsageDialog
  participant GameViewModel
  participant WindowRegistryImpl
  participant ComposeTextStream

  User->>TitleBarView: Select “Memory usage...”
  TitleBarView->>WarlockApp: showMemoryDialog()
  WarlockApp->>MemoryUsageDialog: Render with open games
  MemoryUsageDialog->>GameViewModel: memoryUsage()
  GameViewModel->>WindowRegistryImpl: memoryUsage()
  WindowRegistryImpl->>ComposeTextStream: memoryUsage()
  ComposeTextStream-->>WindowRegistryImpl: StreamMemoryUsage
  WindowRegistryImpl-->>GameViewModel: WindowMemoryUsage
  GameViewModel-->>MemoryUsageDialog: Memory report data
  MemoryUsageDialog-->>User: Display usage and report actions
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a memory usage view.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch memory-usage-view

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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
`@compose/src/commonMain/kotlin/warlockfe/warlock3/compose/ui/window/ComposeTextStream.kt`:
- Around line 534-537: Update memoryUsage so the queued “memory” task checks
result.isActive before invoking computeMemoryUsage, and cancel result in a
finally block around result.await(). Preserve completion for active requests
while ensuring timed-out callers cancel pending scans; already-running
non-suspending scans need not be interrupted.

In `@desktopApp/src/main/kotlin/warlockfe/warlock3/app/MemoryUsageDialog.kt`:
- Around line 276-294: Update collectReport to snapshot games with
games.toList(), launch one child coroutine per stable entry, and await all child
results together so connection reports are collected concurrently. Preserve the
existing title and memoryUsage timeouts, and keep stream reads serialized within
each connection’s coroutine.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8215da8a-56bf-4786-b917-46db64f85625

📥 Commits

Reviewing files that changed from the base of the PR and between d9bf620 and e2daa7f.

📒 Files selected for processing (12)
  • compose/src/commonMain/kotlin/warlockfe/warlock3/compose/ui/game/GameViewModel.kt
  • compose/src/commonMain/kotlin/warlockfe/warlock3/compose/ui/window/ComposeTextStream.kt
  • compose/src/commonMain/kotlin/warlockfe/warlock3/compose/ui/window/WindowRegistryImpl.kt
  • compose/src/jvmBenchmark/kotlin/warlockfe/warlock3/compose/util/StreamNetworkBenchmark.kt
  • compose/src/jvmTest/kotlin/ComposeTextStreamTest.kt
  • core/src/commonMain/kotlin/warlockfe/warlock3/core/window/MemoryUsage.kt
  • core/src/commonMain/kotlin/warlockfe/warlock3/core/window/WindowRegistry.kt
  • desktopApp/build.gradle.kts
  • desktopApp/src/main/kotlin/warlockfe/warlock3/app/Main.kt
  • desktopApp/src/main/kotlin/warlockfe/warlock3/app/MemoryUsageDialog.kt
  • desktopApp/src/main/kotlin/warlockfe/warlock3/app/TitleBarView.kt
  • desktopApp/src/main/kotlin/warlockfe/warlock3/app/WarlockApp.kt

Comment thread desktopApp/src/main/kotlin/warlockfe/warlock3/app/MemoryUsageDialog.kt Outdated
Two review points on the report path.

Cancelling the caller does not cancel a standalone CompletableDeferred,
so a dialog that timed out left its "memory" op still queued, and the
op ran an O(buffer) walk on the queue that feeds the windows - the queue
being busy is exactly why the caller timed out. Cancel the deferred when
the caller unwinds and skip the walk when it is no longer active. A scan
already under way still finishes; computeMemoryUsage never suspends.

Collecting the connections was serial, so each unresponsive connection
added its own timeout to the wait before the dialog showed anything.
Give each connection its own coroutine: they have separate work queues,
so only the streams within one connection need to serialize. The games
list is snapshotted first, since it can change while the report is
gathered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sproctor
sproctor merged commit f471dfe into master Aug 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant