bsp: make the compile server explicable — heap at boot, and telemetry that charts what it records - #633
Merged
Merged
Conversation
The cap was invisible until something went wrong, at which point it surfaced in an OOM message. It belongs next to the mode and the socket, because the default is surprising: min(16g, max(4g, physicalMb/4)) clamps to its 4g floor on anything under 16GB of RAM, so a rule that sounds proportional hands the same 4g to a 14GB CI runner and a 16GB laptop. Both CI arches land there while ~21GB flows through the heap and ZGC runs ~100 major cycles. Reporting the machine's RAM beside it makes the ratio visible without prescribing anything. `bleep config compile-server max-memory 8g` is the lever and `max-memory-clear` puts it back; neither was discoverable from the outside. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Two gaps, both things someone would have to read source to learn. The heap default was described accurately — a quarter of RAM clamped to 4g..16g — without conveying what the clamp does. The floor swallows the entire low end: every machine with 16GB or less gets exactly 4g, so a 16GB laptop and a 14GB CI runner are sized identically and neither is getting a quarter of anything. Now stated with the table, next to the boot line that reports what you actually got, and with the reminder that raising it shrinks the fork budget. Telemetry had no documentation at all, despite being the thing that makes a server you never launched explicable. Where the files are, what the event types mean, and the distinction that matters when reading a slow build: a deferred compile is usually the scheduler staggering starts, not heap pressure, and only one of those is an argument for more memory. Says plainly that the HTML dashboard does not chart everything the file records — machine counters, per-compile allocation, analysis-cache sharing and admission decisions are recorded but not drawn — rather than implying `server-metrics` is the whole picture. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
`bleep server-metrics` and the CI summariser read the same file and had drifted apart. Of the twenty event types the server writes, the dashboard handled fourteen and dropped six on the floor via `case _ => ()` — and the six were the ones that answer the questions people arrive with: machine was the machine full, or was work queued behind idle cores admission_defer why a compile did not start, and for how long compile_allocation what actually allocates analysis_cache the largest single retainer in the server heap workspace_state how many builds are held compile_phase (still not charted: zinc internals, not this build) Five new charts and six summary cards. The scheduling chart draws CPU in use against cores with running and queued on top, because saturated and starved look identical from outside — a slow build — and have opposite fixes. The memory chart draws the fork budget rather than the machine's RAM, since that budget already has the server's footprint and an OS reserve subtracted and is retuned as other processes come and go. Deferred compiles are split by reason. `stagger` is the scheduler spreading first starts apart and says nothing about memory; `heap_pressure` is a compile actually waiting for heap. Only the second is an argument for a bigger `-Xmx`, and folding them together is how a build that never approached its ceiling still looks memory-bound. Legacy `heap_pressure_stall` events are read too. Servers predating the rename are still writing them — the released M9 spawned one while this was being tested — and they carry no reason, so they are charted as `unlabelled (pre-rename)` rather than being assigned a cause the data does not state. The CI summariser already did this; the dashboard silently dropping half its input is how the two drifted in the first place. Verified against a metrics file covering every type, including both formats: all six charts render, all six cards populate, and the three defer reasons draw separately. Old files missing the newer machine fields degrade to fewer cards rather than failing. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The compile server recorded what it compiled; what it *ran* was invisible. A slow or flaky test run raises questions none of the telemetry could answer: how many JVMs were started, how many were reused rather than respawned, how long each lived, which were killed rather than finishing, and which suite was placed on which JVM. Five events, joined on pid: fork_start, fork_reused, fork_end, suite_scheduled, suite_finished. The pool lives in bleep-core and the telemetry in bleep-bsp, which depends on it, so the pool cannot call the recorder — and a singleton it could reach is the global mutable state this codebase refuses. JvmPoolListener inverts it: the pool announces, and whoever builds it decides who listens. The default is a no-op, so nothing outside the server pays for this. fork_end carries killed_by separately from the exit summary because destroyForcibly sends SIGKILL: a fork bleep terminated and one the OS killed both report exit 137, and only one of them means the machine ran out of memory. Suite events are recorded in TestRunner rather than the pool, because that is the only place that knows both which JVM was handed out and what is about to run on it. Also: server-metrics --file opens any metrics.jsonl, so one downloaded from CI is read by the same dashboard as a local one. Same file, same viewer — verified against a real artifact of 353 events. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
shutdown kills its JVMs directly rather than going through destroy, so the forks that survive to the end of a run — usually most of them — had a fork_start and never a fork_end, and their lifetimes were unknowable. Found by running a suite and counting: 2 starts, 0 ends. Also strips the trailing $ getSimpleName leaves on a case object, so the outcome reads Success rather than Success$. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
`process.info().startInstant()` was read at announce time, which is after the process has been killed — the OS stops reporting a start instant for a process that no longer exists, so every fork_end carried lifetime_ms: -1. Recorded at construction instead. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Added mid-list, its default silently shifted every positional argument after it, so `new ManagedJvm(process, stdin, ...)` bound stdin to the timestamp and bleep-core stopped compiling. I had already committed it, having read a compile result that was still running. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
One lane per forked JVM: the pale bar is the fork's life, the segments on it are the suites that ran there. That is the view which answers 'which suite was on the JVM that got killed' — otherwise a hand-join across fork_start, fork_end and suite_finished. A fork killed by bleep is how the pool shuts down, not a failure, so only an unexplained end is coloured as trouble. Suites are coloured by outcome. Plus a slowest-suites bar and two cards: JVMs started vs reused, which is what the pool actually saves and appears nowhere else, and suites run with a count of any that did not succeed. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The buffers and the charts were both there; nothing filled the buffers. The dispatch edit had silently matched nothing — bleep fmt realigns the case arrows, so a literal written against the pre-format text no longer occurs — and I did not assert, so it passed quietly and the charts were guarded off on real data. Second time the same way in this file. The wiring is now derived from the buffer names and checked after writing. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The compile server is a long-lived JVM you never launched and cannot see. This makes it say what it is doing, and makes the dashboard show what it already records.
Grew well past its original one-line change — worth reading as three pieces.
1. The heap, at boot
The cap was invisible until something went wrong, at which point it surfaced in an OOM message. It is worth stating up front because the default surprises:
min(16g, max(4g, RAM/4))clamps to its 4g floor on anything at or below 16GB, so a 16GB laptop and a 14GB CI runner are sized identically and neither gets "a quarter of RAM".max, because-Xmxis a ceiling and ZGC withZUncommitDelay=30hands memory back; a bare size next to the machine's RAM reads like a reservation.Verified against both paths: default,
max-memory 6g, andmax-memory-clear.2. Telemetry the dashboard was throwing away
bleep server-metricsand the CI summariser read the same file and had drifted. Of twenty event types the server writes, the dashboard handled fourteen and dropped six viacase _ => ()— and the six were the ones people arrive with questions about:machine,admission_defer,compile_allocation,analysis_cache,workspace_state.Five new charts, six new cards. Two worth calling out:
staggeris the scheduler spreading first starts apart and says nothing about memory;heap_pressureis a compile actually waiting for heap. Only the second argues for a bigger-Xmx. On a real 18-core dlab run: 115 defers, every one a stagger, zero heap pressure — precisely the case that used to read as memory-starved under the old label.Legacy
heap_pressure_stallevents are read too, and shown asunlabelled (pre-rename)rather than assigned a cause the data does not state. Not theoretical: the released M9 spawned a server mid-testing that wrote 42 of them.server-metrics --file <path>opens anymetrics.jsonl, so one downloaded from CI is read by the same dashboard as a local one — same file, same viewer. Verified on a real artifact of 353 events.3. Forked test JVMs, which was recorded nowhere
The server recorded what it compiled; what it ran was invisible. Five events joined on pid:
fork_start,fork_reused,fork_end,suite_scheduled,suite_finished.bleep-corecannot callbleep-bsp, and a singleton it could reach is the global mutable state this codebase refuses — soJvmPoolListenerinverts it: the pool announces, whoever builds it decides who listens, default no-op.fork_endcarrieskilled_byseparately from the exit summary becausedestroyForciblysends SIGKILL and a fork bleep killed is otherwise indistinguishable from one the OS killed.Charted as one lane per JVM — the pale bar is the fork's life, the segments on it are the suites that ran there. That answers "which suite was on the JVM that got killed", otherwise a hand-join across three event types.
What it immediately showed on dlab (73 suites, 500 tests):
The reuse rate may be intentional — the pool destroys rather than caches under memory contention — but no metric expressed it before.
Docs
docs/guides/compile-servers.mdxgains the floor table, a telemetry section (where the files are, what the event types mean, stagger vs pressure), a CI recipe with testedjqone-liners, and an explicit note thatmetrics.jsonlrecords nothing about individual tests —bleep test --junit-reportis where per-test timing lives.Verification
Everything here was checked by running it and reading the output, not by a green compile. That caught:
fork_endnever firing (shutdown bypassesdestroy),lifetime_ms: -1(start instant read after the process died),outcome: "Success$", a default parameter added mid-list that rebound every positional argument, and — twice — a dispatch edit that silently matched nothing, leaving charts guarded off on real data.Final state exercised end to end on a 42-project dlab build: all nine charts and eight cards render.
🤖 Generated with Claude Code