Skip to content

Add a realm load harness - #6128

Open
backspace wants to merge 7 commits into
mainfrom
cs-12946-realm-load-harness
Open

backspace wants to merge 7 commits into
mainfrom
cs-12946-realm-load-harness

Conversation

@backspace

@backspace backspace commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

A load harness for the realm server, under packages/realm-server/scripts/load-harness/, plus a Claude skill covering how to run it so the numbers mean something.

What it does

Authenticates N real Matrix users against a non-production realm server, then holds _federated-search queries open from --readers sessions while --writers sessions POST cards into the same realm. It reports per-query-shape payload bytes, headers-vs-body latency split, write latency, and — under --subscribe — realm-event re-run counts.

The load it reproduces is write-driven, which is the part most easily got wrong. Each write invalidates the index, the realm broadcasts it, and every connected client re-runs its live searches, so a read-only replay reproduces none of the interesting behaviour.

Which path a run measures

_federated-search serves two documents from the same filter, selected by the fields[entry] sparse fieldset, and they are not close in cost:

--fieldset on the wire comes back models measured
entries (default) no fieldset prerendered renderings (html + css) a grid, card list, search panel 405–537 KB
item fields: { entry: ['item'] } card serializations only a query-backed field 94 KB
item-html fields: { entry: ['item','html'] } both 497 KB

store.search sends fields: { entry: ['item'] } (packages/host/app/services/store.ts) and instantiates cards live from the result, so item is the path a query-backed field takes — and the path a search-saturation investigation is usually about. A driver that sends no fieldset measures what a grid costs, whatever its workload's queries say.

A workload file pins the choice with a "fieldset" member and --fieldset overrides it; both committed workloads pin a value rather than leaving it implicit. item-html is its own value rather than a flag combination, since that is the only other pairing the wire accepts. The default stays entries so existing numbers remain comparable, and every run prints the path it modelled in its header and its summary — a payload or latency figure quoted without its path is not interpretable.

A workload's filter is passed through whole, so eq / contains / range / any ride alongside the item.on anchor. That is what a run needs to A/B "add a predicate to a query that has none"; workload.example.json carries committed examples and a test pins the pass-through.

--derive-workload produces the unmitigated shape by construction — the type summary carries names and counts, so every derived query is type-only with no predicate. Faithful as a reproduction, useless as a measurement of a mitigation that adds one. The docs say so where the flag is described.

Where the queries come from

Three modes, in increasing order of specificity. Nobody has to pass a config file around to get comparable numbers.

workload.experiments.json — the standard workload, and the recommended start. It targets the experiments realm, which ships in the repo as packages/experiments-realm and exists in every deployed environment, so it is the one realm everybody can point at. Its eight shapes are that realm's highest-count instance types, picked by count rather than by interest.

It is comparable across runs against the same target, not across targets, and the file says so with both columns side by side — the repo realm and a deployed one are not kept in step:

deployed in repo
Spec 117 28
FileDefFormatPreview 37 37
Author 30 5
CardListing 26 0

--derive-workload — for the realm you actually care about. It reads the realm's own GET <realm>/_types summary, keeps the kind: 'instance' entries, ranks by attributes.total (id as tiebreak, so a tie cannot reorder the selection between runs), splits each id at the last / into an item.on anchor, and queries the top --derive-top (default 8) at --derive-page-size (default 20; 0 is unbounded). Both id spellings in circulation split correctly — the prefix form @cardstack/base/spec/Spec and the URL form https://…/experiments/author/Author.

--emit-workload <path> (- for stdout) writes the derived workload and exits, which is how a derived workload becomes a shared one. Realm-local modules are emitted as ${realm}…, so the file travels between clones rather than pinning to the realm it came from. _types needs the realm's own JWT — a bare session token gets a 401 — and a failure there stops the run rather than falling back to a built-in workload: two people believing they ran the same test and not having done is worse than a failed run.

A derived workload can legitimately come back read-only. The write target has to be a type defined in the realm itself; an external module is not addressable relative to the realm, so a POST naming one would be a guess discovered at write time. When no selected type qualifies the write block is omitted, stderr names the types that did rank and says why, and the run needs --writers 0. That is the expected outcome on a deployment's shared realms, where every top type is @cardstack/… — and it matches those realms being granted read-only, which makes --writers 0 the only run that works there rather than a convenience. The README and skill say so where they recommend a target.

A hand-written file for reproducing a specific card's query pattern. workload.example.json is the shape.

All three are written in the _federated-search entry wire grammar: the type anchor is item.on, field paths carry an item. prefix.

Other notable decisions

Where it lives and how it runs. scripts/ beside bench-realm/, the existing precedent for a multi-file operational tool. The files are .ts run directly through Node's type stripping, like every sibling script; pnpm load-harness and pnpm load-harness:setup are the package entry points.

It imports nothing from the workspace. The driver has to run from inside the realm server's own AWS region, on a box with no checkout and no node_modules, so installing it is a copy rather than a build. Global fetch and node: built-ins only. The repo's existing erasable-syntax lint rule is what keeps that copy runnable.

Headers and body are timed separately, and connection setup is kept out of both. fetch resolves when response headers arrive; reading the body is a second wait. Splitting them separates the server's own work from bytes crossing the network — a driver on the far side of a home connection measures mostly its own link, and the summary warns when the body leg dominates.

But fetch also resolves only after any TCP and TLS handshake the request had to perform, so headers would silently contain connection setup. Readers re-run on an interval far longer than undici's keep-alive, so on a realm nobody is writing to — what --writers 0 makes normal — nearly every sample pays it: measured against a local server, 59 of 60 timed searches opened a connection. The driver now opens each batch's connections before starting the clock, and that becomes 0 of 60, with no increase in total connections — the setup just moves outside the timed window.

Priming matches the batch's concurrency, because N concurrent requests want N sockets and undici hands a request to an already-free client in preference to opening another; a single primer would funnel the batch onto one socket and change the concurrency being measured. A socket also returns to the pool a tick after its response settles, so the primer yields before the batch dispatches — without that the last search still opens its own connection. The primer is a CORS preflight, answered by @koa/cors ahead of the router, so it costs the server nothing beyond the connection it exists to open.

What a cold socket costs on the current link is measured at startup and printed, and --prime-connections=false relabels headers as including setup rather than quietly containing it.

That measurement carries the same trap as the primer, so it lives in the same module: the first request after process start is cold, and so is the second, because the socket has not returned to the pool yet. A cold-minus-next subtraction therefore compares two handshakes and reports the jitter between them — against a control server charging a known 295 ms handshake it reads 11 ms on one run and 48 ms on another. The warm figure is now the cheapest of several samples taken after a yield, which reads 295–296 ms against the same control; both raw samples are printed beside the difference so a wrong subtraction is visible, and the figure is not clamped at zero, since in-region it belongs in the noise and should say so. The docs no longer call headers "the platform number": it is server work plus a round trip, comparable between runs from one place and from a fixed --readers count, since headers rises with reader count — that is the server under concurrent load, which is the thing being measured rather than an artifact to correct for. Byte counts remain trustworthy wherever the driver runs; latency is not.

The summary states two things its per-shape table contained but did not say: what one pass over every shape transfers (that is what opening a screen costs, not any single row), and how far server time spreads across shapes when the spread is wide enough that an aggregate describes none of them. On a deployed experiments realm that spread is 6×, and it does not track instance count — the 117-instance type is the fastest of the six.

--model-calls is bounded by where the handler rejects. It puts a _request-forward call before each write, aimed at a destination the realm server refuses, so it spends no tokens. Worth knowing precisely how far that gets: handleRequestForward verifies the JWT, parses the body, and looks the destination up in AllowedProxyDestinations — and rejects there, in front of withUserCostLock. So the flag adds a second authenticated round trip per write but does not reproduce per-user cost-lock contention; reaching the lock needs an allowlisted destination and therefore real spend. The comments, README and skill all say so, because a run read as evidence about that lock would be wrong.

Two invalidation modes, and the difference matters when quoting a result. By default readers re-run on this driver's own writes, which models the fan-out. --subscribe reads the realm's app.boxel.realm-event stream and applies the host's skip test, which measures it — but the harness compares type keys literally where the host resolves them through its module loader, so its re-run counts are indicative rather than authoritative. Payload and latency are faithful in both modes.

Boolean options take an explicit value. An option that defaults to on can only be disabled by --flag=false, and reading that string as truthy would leave it on while the operator believed otherwise.

Unknown options are rejected. A flag the spec does not define used to be written to the parsed args and never read, so a mistyped --duration 45 ran for the default ten minutes with nothing on stderr. It now fails, listing every unrecognised option and the valid spellings — which also catches --writer 0, a typo that would otherwise leave two writers against a realm you may only be able to read.

The production guard has no override. Any boxel.ai host is refused. This drives a realm server to saturation on purpose; an operator under time pressure will reach for a --force that exists.

No credential file or realm-specific workload can be committed from that directory. Its .gitignore refuses *.csv and any workload*.json other than the two shared ones.

Tests

packages/realm-server/tests/load-harness-test.ts covers what a run cannot: the production guard, credential parsing, argument parsing (including unknown-option rejection), workload validation and ${realm} / ${n} substitution, the _types → workload derivation (ranking, file-kind exclusion, both id spellings, page sizing, tie stability, label collisions, write-target selection, and the read-only case where nothing realm-local ranks), the realm-event skip test, and the summary statistics. It also parses both committed workload files, so neither can drift out of the format and both are checked to pin a fieldset; the fieldset wire members, name validation and descriptions are covered; an arbitrary wire filter is asserted to survive the workload loader untouched; and the connection-setup sampler is driven through a replayed sequence of request durations so the cold-second-sample trap is pinned by a test rather than by prose. 58 tests, no database, no network.

The fieldset resolution is additionally verified on the wire: the real driver is run against a server that records search request bodies, confirming that the default sends no fields member, a workload's fieldset is honoured, an explicit --fieldset beats the file, and a predicate survives alongside the type anchor.

The connection-priming behaviour is verified separately by running the real driver against a local server that records which TCP socket each request arrived on — that is where the 59/60 and 0/60 figures come from.

🤖 Generated with Claude Code

Drives a non-production realm server with the load a cohort of users
produces: many sessions holding unbounded dashboard queries open while a
few sessions write cards into the same realm. That shape is write-driven
— each write invalidates the index, the realm broadcasts it, and every
connected client re-runs its live searches — so a read-only replay
reproduces none of it.

The harness lives in scripts/ beside bench-realm, runs as .ts through
Node's type stripping like its siblings, and imports nothing from the
workspace: it has to run from inside the realm server's own AWS region,
where there is no checkout, so installing it is a copy rather than a
build. A driver on the far side of a home connection measures its own
link, which is why every search reports headers and body separately and
the summary warns when transfer dominates.

Which queries the readers issue and what the writers write is a workload
file rather than code, written in the `_federated-search` entry wire
grammar; workload.example.json is the shape.

The scripts refuse to run against any boxel.ai host, with no override
flag. tests/load-harness-test.ts covers the parts a run cannot: the
guard, credential parsing, argument parsing, workload validation, and
the realm-event skip test.

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

chatgpt-codex-connector Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-15T14:06:01.072850Z 092cf5c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

`handleRequestForward` looks the destination up in AllowedProxyDestinations
and rejects an unlisted one before it takes `withUserCostLock`. So a call
aimed at a refused destination never reaches the lock, and cannot stand in
for the serialization between one user's successive generations.

State what the flag does reach — JWT verification, body parsing, and the
`proxy_endpoints` lookup — and name the boundary, so a run is not read as
evidence about lock contention.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 092cf5ceba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Authorization: session.serverToken,
},
body: JSON.stringify({
url: 'https://load-harness.invalid/v1/chat/completions',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Send the lock probe to a configured destination

With --model-calls, this .invalid URL is rejected by AllowedProxyDestinations.getDestinationConfig in packages/realm-server/handlers/handle-request-forward.ts:179-191, before the handler reaches withUserCostLock at line 293. Consequently the recorded 400s measure only allowlist rejection and never acquire or contend on the user cost lock, so this mode cannot support its stated conclusion; use a configured no-cost destination or another path that reaches the lock.

Useful? React with 👍 / 👎.

Comment on lines +49 to +53
**One manual step remains.** The other users need read access to the realm.
Granting it is not a `boxel realm` subcommand, so it is a UI action or a direct
API call. Each session authenticates as its own user: searches authorize per
realm, and the per-user cost lock that serializes model calls is keyed by Matrix
user, so a single shared account reproduces neither.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Grant write access to every configured writer

Following these setup instructions with the default --writers 2, only the first credential owns the realm while every other user is granted read access. run-load.ts then assigns the first two authenticated sessions as writers, so the second writer's POSTs receive 403 responses and the default run produces roughly half its intended write and invalidation rate; the setup must grant write access to the first N writer accounts or distinguish writer credentials explicitly.

Useful? React with 👍 / 👎.

Comment on lines +354 to +356
function announceInvalidation() {
let waiters = invalidationWaiters;
invalidationWaiters = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve invalidations that arrive during searches

In the default mode, announceInvalidation clears and resolves only readers currently blocked in nextInvalidation; if a write completes during the initial Promise.all or while a reader is executing a previous search set, there is no registered waiter and the invalidation is discarded. Once searches take longer than the write interval—the saturation scenario this harness targets—the driver therefore underproduces the browser's follow-up searches; retain a pending generation/count per reader so an in-flight reader runs again after it finishes.

Useful? React with 👍 / 👎.

Comment on lines +426 to +427
stats.eventsSkipped += batch.length * (specs.length - due.length);
stats.eventsMatched += batch.length * due.length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Count the reruns actually issued for an event batch

When multiple realm events accumulate, each due query is executed only once at line 429, but these counters add batch.length * due.length and the summary later labels that value as reruns "performed." A query matching one event in a five-event batch is therefore reported as five reruns even though only one request was made, corrupting the primary --subscribe result; count the actual due.length executions separately from event/query match decisions.

Useful? React with 👍 / 👎.

`stamped with an x-boxel-logging-correlation-id, so a slow one here can be\n` +
`joined to the server's own timing by its corr= id.`,
);
process.exit(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wait for in-flight operations before summarizing

When the duration expires or Ctrl-C invokes finish, this immediate exit terminates any searches, writes, or model calls still in flight after printing the summary. Under saturation those outstanding operations are disproportionately the slowest ones, so omitting them biases tail latency and error counts downward; track the loop promises and await or explicitly abort and account for them before exiting.

Useful? React with 👍 / 👎.

Comment on lines +612 to +613
let minutes = args.minutes || 1;
console.log(`\nrate: ${Math.round(stats.searches / minutes)} searches/min`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Base the reported rate on actual elapsed time

The README explicitly supports ending a run early with Ctrl-C, but this denominator remains the configured duration. For example, stopping a ten-minute run after one minute reports one tenth of the actual searches per minute, which can invalidate comparisons between runs; record the start time and divide by the elapsed duration instead.

Useful? React with 👍 / 👎.

}
return;
}
record(spec.label, headersMs, bodyMs, text.length);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Measure response bytes rather than string length

text.length counts UTF-16 code units in the decoded response, not bytes transferred, so card data containing non-ASCII text yields incorrect per-query KB and throughput figures; transparent content decoding can widen the discrepancy further from the claimed network-byte measurement. Read/count the response as bytes, or clearly report an uncompressed encoded payload size instead.

Useful? React with 👍 / 👎.

@habdelra habdelra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Claude Code 🤖] This review went after one question: does each number the harness prints mean what the README, the skill and the inline comments say it means — plus the auth/permission path a run depends on. The prose here is load-bearing (the whole point is that a number quoted wrong becomes a wrong platform conclusion), so I traced every such claim to the code it rests on: handleRequestForward, search-entries.ts's subscription callback and #indexEventCannotMatch, wireFilterTypeAnchors, and fetch's own encoding behaviour. I did not run the harness against a realm server, so nothing here speaks to the reference numbers in the skill.

One blocking defect in the default configuration (the second writer is a read-only user, so the default run writes at half its stated rate) and one in the --subscribe headline count (re-runs are counted per event × query, but issued once per batch). The rest is accuracy of the claims around otherwise sound machinery.

On the bot's --model-calls thread: no action needed. The head commit already states exactly that bound — in modelCall's comment, the summary's own printed note, the README and the skill — and the bot is reviewing the commit before it. I confirmed the ordering it describes (getDestinationConfig rejects at handle-request-forward.ts well ahead of withUserCostLock), which is what the code now says.

Recommendations, in the order I'd take them:

  1. Make the setup grant write to the first --writers accounts, or default --writers to 1 — see the thread on the README's manual step.
  2. Count re-runs actually issued for the --subscribe line — see the thread on the eventsMatched/eventsSkipped accumulation.
  3. Extend the skip-test caveat (or the skip test) to cover prerender_html, full-index events and nested any/every anchors; it currently errs in both directions while the caveat names only one — see the thread on eventCannotMatch.
  4. Measure bytes as bytes, and decide what "transferred" means under gzip — see the thread on record(..., text.length).
  5. Don't drop an invalidation announced mid-search-set — see the thread on announceInvalidation.
  6. Base the rate on elapsed time, and account for in-flight requests at exit — see the thread on the rate line.
  7. Reject NaN from a numeric flag — see the thread on parseArgs.
  8. Non-blocking, structural: the two caveats that decide how a result may be quoted (the --subscribe divergence, the --model-calls boundary) are now written out in four places each — run-load.ts's header, lib/realm-events.ts, the README and the skill. They're the paragraphs most likely to go stale and the most expensive to have stale. Consider one canonical home (the code comment) with the README and skill pointing at it, so a future fix to the skip test doesn't leave three copies claiming the old behaviour.

Adjacent, out of scope: writerLoop seeds n = index * 1000, so two writers collide after 1000 writes each — unreachable at the documented cadences, worth a note if the interval ever drops.


Generated by Claude Code

Comment on lines +49 to +53
**One manual step remains.** The other users need read access to the realm.
Granting it is not a `boxel realm` subcommand, so it is a UI action or a direct
API call. Each session authenticates as its own user: searches authorize per
realm and realm events are broadcast into each user's own session room, so a
single shared account reproduces neither.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Claude Code 🤖] Following this step as written leaves the default run writing at half its configured rate, and the invalidation rate is the load. setup-realm.ts makes creds[0] the owner and this paragraph grants everyone else read; run-load.ts then takes the writers off the front of the session list (sessions.slice(0, writers)), so with the default --writers 2 the second writer is a read-only user and every one of its POSTs comes back 403. The 403s land in writeErrors, which is printed — but a run still produces a summary that looks complete.

Either grant write to the first --writers accounts here (and say so in the skill's step 2, which carries the same "grant the other users read access" wording), or default --writers to 1 so the documented setup and the default flags agree.

Regression, blocking — it's the default configuration. This confirms the bot's read on the same lines; I traced the assignment to writerSessions in run-load.ts and the owner selection in setup-realm.ts.


Generated by Claude Code

Comment on lines +432 to +433
stats.eventsSkipped += batch.length * (specs.length - due.length);
stats.eventsMatched += batch.length * due.length;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Claude Code 🤖] These two counters are (event × query) decision pairs, but the summary prints eventsMatched as query re-runs: N performed — and the line below it executes each due spec exactly once per batch, not once per event. A batch of five events where one query is due reports 5 re-runs against 1 search actually issued, so the headline --subscribe number overstates by the batch factor, which grows precisely as the run saturates and batches get bigger.

Count the executions separately from the decisions:

stats.reRunsPerformed += due.length;
stats.pairsSkipped += batch.length * (specs.length - due.length);
stats.pairsMatched += batch.length * due.length;

and print reRunsPerformed on the "re-runs performed" line, keeping the pair counts for the skip percentage (which is a coherent ratio as it stands — just not a count of requests).

Regression, blocking: this is the number the mode exists to produce.


Generated by Claude Code

Comment on lines +158 to +166
// ONE DIFFERENCE, AND IT DECIDES WHAT A RESULT MEANS. The host resolves a
// query's type keys through its module loader, so a filter that names a type
// through a re-exporting module still matches rows stamped with that type's
// canonical defining-module spelling. The harness has no loader: it compares
// the literal `module/name` the workload file gave it. On a realm whose queries
// name their types directly the two agree. On a realm that filters through a
// re-export, the harness skips where a browser re-runs, and understates the
// traffic. Check which kind of realm you are pointed at before trusting a low
// re-run count.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Claude Code 🤖] The caveat names one divergence and gives it a direction ("skips where a browser re-runs, and understates"). Reading search-entries.ts against this file, there are two more, and one of them runs the other way — so a reader who trusts this paragraph knows the sign of the error and has it wrong.

  • prerender_html events. The host's subscription callback re-runs on eventName === 'prerender_html' as well as on incremental index events (search-entries.ts, the isIncrementalIndex/prerender_html guard), and the skip test is applied only to the incremental-index branch. Here every non-index event returns true, so those never produce a request — and load-harness-test.ts's "a non-index event never triggers a re-run" pins that as intended. Understates.
  • A full index event. The host takes the early return for indexType !== 'incremental' (no re-run); here eventName === 'index' with no invalidatedTypes returns false, i.e. re-run. Overstates.
  • Nested type anchors. wireFilterTypeAnchors walks every/any branches; typeKeysOf in workload.ts reads only the top-level filter['item.on'], so a workload query anchored inside an every gets undefined keys and re-runs unconditionally where the host would skip. Overstates.

Cheapest fix for the first two is in this function (gate on indexType, and treat prerender_html as a re-run trigger); the third could be a wireFilterTypeAnchors-shaped walk in typeKeysOf. If any stays as-is, extend this comment, the README's second bullet and the skill's caveat to say the count can err in both directions and name which events do it.

Regression (the claim is new with this PR), non-blocking if the caveat is corrected — the counts are already labelled indicative.


Generated by Claude Code

}
return;
}
record(spec.label, headersMs, bodyMs, text.length);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Claude Code 🤖] text.length is UTF-16 code units of the decoded body, which is the one number the README and skill both stake their reputation on ("Byte counts are trustworthy regardless of where the driver runs"). Two gaps, both measured against a local server just now rather than argued:

  • Node's fetch sends accept-encoding: gzip, deflate by default and decompresses transparently. On a gzip'd response my probe read text.length 208 where the wire carried 42 bytes.
  • Non-ASCII inflates the other way: the same body was 458 UTF-8 bytes against those 208 code units. Card titles with an em dash or an emoji are enough.

Buffer.byteLength(text) fixes the second and makes the number an honest decoded payload size, which is what the per-shape KB column is for. For the first, either read response.headers.get('content-encoding') and say in the summary which one is being reported, or send Accept-Encoding: identity so decoded and transferred coincide — worth deciding explicitly, since MB/s is currently decoded bytes over transfer seconds and will read high wherever a hop compresses.

Regression, non-blocking, but it undercuts the claim the tool is built around.


Generated by Claude Code

Comment on lines +360 to +372
function announceInvalidation() {
let waiters = invalidationWaiters;
invalidationWaiters = [];
for (let resolve of waiters) {
resolve();
}
}
function nextInvalidation(timeoutMs: number): Promise<void> {
return new Promise<void>((resolve) => {
invalidationWaiters.push(resolve);
setTimeout(resolve, timeoutMs);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Claude Code 🤖] An invalidation announced while a reader is inside its Promise.all has no registered waiter and is dropped — the reader then sits in nextInvalidation until the next write or the 60 s idle floor. The loss rate is proportional to how long a query set takes relative to --write-every-ms, so the default mode underproduces load exactly in the saturation regime the harness is for, and quietly: nothing counts a dropped wake-up.

The --subscribe path already gets this right with its queue + wake. The same shape here is a per-reader pending flag:

let pending = false; // per reader
// announce: pending = true for each reader, then resolve any waiter
// loop: if (pending) { pending = false; } else { await nextInvalidation(idleReRunMs); }

Regression, non-blocking — the run still produces load, just less of it than --write-every-ms implies. Worth a counter either way so a run can report how many wake-ups it coalesced.


Generated by Claude Code

Comment on lines +617 to +618
let minutes = args.minutes || 1;
console.log(`\nrate: ${Math.round(stats.searches / minutes)} searches/min`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Claude Code 🤖] Two things bias the summary when a run doesn't end exactly on schedule, and the README advertises both endings.

The rate divides by the configured --minutes, so a ten-minute run stopped after one with Ctrl-C reports a tenth of the real searches/min — and the skill quotes "889 searches/min sustained" as a comparable figure across runs. Stamp a start time and divide by the elapsed wall clock.

And finish() ends with process.exit(0), killing whatever searches and writes are in flight. Under saturation those are the slowest requests by selection, so p90/p99 and the error counts are biased low precisely where the tail is the thing being measured. Tracking the loop promises and awaiting them with a bounded grace period (or counting the abandoned ones in the summary) would keep the tail honest.

Regression, non-blocking.


Generated by Claude Code

Comment on lines +152 to +154
out[camel(key)] =
typeof spec[camel(key)] === 'number' ? Number(value) : value;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Claude Code 🤖] A mistyped numeric flag becomes NaN and the run reports it as a success. --minutes lots (or --minutes with the value swallowed by a preceding valueless flag) gives setTimeout(finish, NaN), which fires on the next tick: the harness authenticates, prints the summary with searches: 0, and exits 0 — and the rate line reads normally because NaN || 1 is 1. --readers/--writers as NaN go the same way through slice.

load-harness-test.ts pins the NaN as the behaviour ("a number flag given a non-number yields NaN"); a run is expensive to set up and this failure mode looks like a completed run, so it's worth rejecting instead:

let n = Number(value);
if (Number.isNaN(n)) {
  throw new Error(`--${key} needs a number, got: ${String(value)}`);
}

Also worth refusing an unknown --flag here: one that isn't in the spec and isn't boolean silently consumes the next argv entry as its value, so a typo can eat a real flag's argument.

Regression, non-blocking.


Generated by Claude Code

Two ways to run comparable load tests without passing a config file
around.

workload.experiments.json targets the experiments realm, which ships in
the repo and exists in every deployed environment, so it is the one realm
everybody can point at. Its types are that realm's highest-count instance
types, picked by count rather than by interest. The repo realm and a
deployed one are not kept in step — Spec is 117 instances deployed
against 28 in the repo, CardListing 26 against 0 — so the file records
both columns and says what that buys: comparability across runs against
one target, not across targets.

--derive-workload builds the queries from the realm under test instead,
ranking its own /_types summary by instance count and querying the top
--derive-top. Two people testing one realm then need no shared config,
and --emit-workload turns the result into a committable file whose
realm-local modules are written back as ${realm} so it travels between
clones. A 401 or 404 from /_types stops the run rather than falling back
to a built-in workload: two people believing they ran the same test, and
not having done, is worse than a failed run.

The per-shape summary now states the two things its table contained but
did not say — what one pass over every shape transfers, and how far
server time spreads across shapes when the spread is wide enough that an
aggregate describes none of them.

Readers outnumber writers when sessions are short, so --writers 0 is a
read-only run against a realm nobody wants dirtied.

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

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Host Test Results

    1 files      1 suites   2h 9m 29s ⏱️
4 803 tests 4 789 ✅ 14 💤 0 ❌
4 818 runs  4 804 ✅ 14 💤 0 ❌

Results for commit 545df34.

Realm Server Test Results

    1 files    225 suites   1h 21m 34s ⏱️
3 108 tests 3 108 ✅ 0 💤 0 ❌
3 147 runs  3 147 ✅ 0 💤 0 ❌

Results for commit 545df34.

backspace and others added 4 commits September 15, 2026 16:44
Two ways the harness could report a number for a run nobody asked for.

A derived workload picked the highest-count type, and fell back to one
defined outside the realm when no realm-local type ranked — which the
comment directly above it said was not a valid target, because such a
module is not addressable relative to the realm. On a deployment's shared
realms every top type is external, so the common case emitted a write
block that fails at the first POST. The write target is now realm-local
or absent: a workload may carry no write block, the derivation says on
stderr which types ranked and why none qualified, and run-load refuses to
start writers without one rather than inventing a target. Those realms
are granted read-only anyway, so --writers 0 is the run that works there,
and the README and skill say so where they recommend a target.

A flag the spec does not define was written to the parsed args and never
read, so `--duration 45` ran for the default ten minutes with nothing on
stderr. Unknown options are now rejected, listing every one of them and
the valid spellings — which also protects `--writer 0`, a typo that
otherwise leaves two writers against a realm you may only be able to
read.

The standard workload now says roughly what one pass transfers on a
deployed realm, so the cost of a long run from CloudShell is known before
it starts rather than after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`fetch` resolves when response headers arrive, so a request that had to
open a socket first has a TCP and a TLS handshake inside the promise —
and `headers` reported all of it as if the server had been thinking.
Readers re-run on an interval far longer than undici's keep-alive, so on
a realm nobody is writing to, which --writers 0 makes the normal case,
nearly every sample paid it. Against a local server, 59 of 60 timed
searches opened a connection.

The driver now opens a batch's connections before starting the clock, and
0 of 60 timed searches open one. Priming matches the batch's concurrency:
N concurrent requests want N sockets, and undici hands a request to an
already-free client in preference to opening another, so a single primer
would funnel the batch onto one socket and change the concurrency under
measurement. A socket also returns to the pool a tick after its response
settles, so the primer yields before the batch dispatches — without that,
the last search still opens its own connection. The primer is a CORS
preflight, answered ahead of the router, so it costs the server nothing
beyond the connection it exists to open.

What a cold socket costs on the current link is measured at startup and
printed, so the correction is visible either way, and
--prime-connections=false relabels `headers` as including setup rather
than quietly containing it. The docs no longer call `headers` the
platform number: it is server work plus a round trip, comparable between
runs from one place.

Turning an on-by-default option off needs `--flag=false`, so boolean
options now take an explicit value instead of reading 'false' as truthy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reported setup cost subtracted the second request from the first. The
first is cold, but so is the second: a socket returns to undici's pool a
tick after its response settles, so a request issued immediately after
opens its own connection. The subtraction compared two handshakes against
each other and reported the jitter between them as the cost of a
handshake — against a control server charging a known 295 ms, it read 11
ms, and a rerun read 48 ms. With different jitter it can land near zero,
and a clamp at zero hid the negative case entirely.

The warm sample now comes after a yield, and is the cheapest of several
rather than a single one, so a slow warm sample cannot shrink the figure
— under-reporting is the damaging direction, since the number exists to
stop someone reading setup as server time. The same control now reads 295
and 296 ms. Both raw samples are printed beside the difference, so a
wrong subtraction is visible rather than authoritative, and the figure is
no longer clamped: in-region it belongs in the noise and says so.

The pool property both the primer and the measurement depend on now lives
in one module with them. Written apart, one yielded and the other did
not.

Also states, where the harness claims what `headers` means, that it rises
with --readers: that is the server under concurrent load, which is the
thing being measured rather than an artifact to correct for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`_federated-search` serves two documents from one filter, selected by the
`fields[entry]` sparse fieldset: the prerendered renderings a grid
displays, or the card serializations a query-backed field instantiates
from. `store.search` sends `fields: { entry: ['item'] }`, so the second is
the query-backed-field path — and on a deployed realm the first costs
four to six times as much for the same filter, 94 KB against 405 and 537.

The driver sent no fieldset, so every number it produced described a
grid, whatever the workload's queries said. `--fieldset` and a workload
`fieldset` member now select the path, an explicit flag beating a
committed file; `item-html` is its own value rather than a combination,
since the wire accepts that pair and nothing else alongside `item`. The
default stays `entries` so existing numbers remain comparable, and both
committed workloads pin a value rather than leaving it implicit. Every
run states the path it modelled in its header and its summary: a payload
or latency figure quoted without its path is not interpretable.

A workload's filter was already passed through whole, so `eq` /
`contains` / `range` / `any` ride alongside the `item.on` anchor — which
is what a run needs to A/B adding a predicate to a query that has none.
That is now pinned by a test and shown by committed examples.

A derived workload is the unmitigated shape by construction: the type
summary carries names and counts, so every derived query is type-only
with no predicate. Faithful as a reproduction, useless as a measurement
of a mitigation that adds one, and the docs say so where the flag is
described.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@habdelra
habdelra requested a review from a team September 15, 2026 20:13
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.

3 participants