Skip to content

feat(usage): usage.get and simlock stats, the figures from the event history - #356

Draft
V3RON wants to merge 26 commits into
mainfrom
task/347
Draft

V3RON wants to merge 26 commits into
mainfrom
task/347

Conversation

@V3RON

@V3RON V3RON commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Closes #347

Status

Implement: done Review: round 5, 4 open (1 code, 3 claims), Rerun: none Mutate: 36 alive, explained Hardware: n/a Gate: not run
Next: parked after the last review round; the maintainer decides, then implement fix and review round 6

  • code: src/daemon/dispatcher.ts:253 the worker's own label in its usage.get workers entry is reached by no test. Class: a pass-through no test reaches is untested code.
  • claims: src/core/usage/read-events.ts:71-73 ReadEvents.carried doc omits gateway requests a restart before from already ended. Class: a doc that describes a mechanism by its old inputs.
  • claims: src/bus/event-file.ts:77-78 answeredBefore doc says a request in neither set "was still waiting"; on a gateway a daemon.started before sinceTs ended it. Class: same.
  • claims: docs/CLI.md:2004-2005 names "maintenance" as closing admission; only nuke does. Class: a doc that names a cause the code does not have.

Done when

  • After a scripted run against the fake driver with waits, a --no-wait rejection and a timeout, simlock stats --since 1h prints request count, grants by source, held time and turnaround, wait p50, p95 and max, provisioning and boot durations, peak utilisation, queue depth, rejections by reason and incidents; --json prints the same figures; and the counts match what a reader counts by hand from simlock events --since 1h.
  • The same window prints the same figures after simlock daemon stop and simlock daemon start.
  • On a gateway with two workers, the output has fleet totals and one row per worker; each worker's own output has one row, itself.
  • A request that waited shows in the wait figures; a request that timed out and one cancelled are counted under rejections, not grants.
  • The per-requester rows show each requester's leases in the window, with the token label beside the id for a lease taken over HTTP.
  • After a --no-wait refusal, simlock stats --since 1h shows requests and rejected.byReason.no-wait each one higher than before it and granted unchanged.
  • simlock stats --from <a day ago> --to <two days ago> is a usage error; --since 30d on a history whose oldest event is a minute old prints the figures with "Figures cover from " above them; a window that ends before the oldest held event prints the HISTORY_NOT_KEPT message and exits non-zero.
  • docs/CLI.md describes simlock stats, its flags, the partial note and the error.

Assumptions

  • Assumption: which rejections have no lease.requested — only already-leased, killed and lease-id-taken are counted as refused before admission, and only when no lease.requested with the rejection's request id exists anywhere in the history read; any other rejection without a request is a request from before the window and counts nowhere. This is the narrowest reading of "refused before admission".
  • Assumption: an open request in the per-requester rows — it counts as a request and a grant, with held time 0 until its end is seen.
  • Assumption: series[].waiting for a request made before the window — the history read carries each requester's latest lease.requested and the ids of the requests answered by then (lease.granted, lease.rejected, request.granted); a request counts as waiting at the start when its id is in neither the answered set nor ended by a gateway restart. The core allows one open request per requester, so the latest request of a requester is the only one that can be open.
  • Assumption: the gateway's latest own daemon.started at or before the window is carried with the other carried events, so a fleet request made before the window that a restart before it ended is not waiting at its start (ADR 0021 §5).
  • Assumption: a worker's request carrying fleetRequestId that was made before the window is not carried into waiting (a probe never waits, ADR 0021 §5), whether or not it was answered.
  • Assumption: a probe that was not granted gets no row under its gw: requester (it is nothing of that requester's but a decline).
  • Assumption: the e2e tests that the spec titles simlock stats --since 1h run --since 1h itself, after waiting for the minute that holds the scripted run to close (the window is rounded down to the series bucket, so the minute now running is left out until it is over); the three tests take up to a minute longer.
  • Assumption: ADR 0016 §6 is left as written, as the spec says; ADR 0021 §5 supersedes its join. The earlier edit of §6 on this branch is reverted.

Review

Round 3 (before this run): spec review 0 blocking; code review 10 blocking; claims review 4 blocking. Round 3 fixes applied after ADR 0021 (#433) landed; the fleet join is reworked, so the fix lines about the old join are moot: first relayed answer from any worker (#fleetOutcome, no longer exists), carried requests matched by requester (now by request id), a gateway request open at stop carried forever (now ended at the next daemon.started).
Mutate: 1462 mutants, 33 alive.

Rejected in round 3:

  • code: HISTORY_NOT_KEPT checked against the unrounded end breaches ADR 0016 §5 — the window asked for does not end before the oldest event
  • code: a sub-bucket window rounds to from === to — round-down-both-ends rule, no state left wrong
  • claims: #refusedAtAdmission doc is false — the condition is stated directly above it

Mutants left alive

Equivalent, nothing observable changes:

  • A default or fallback [] becomes a one-element array holding a string that is never an event name, a number, a token or a candidate: src/bus/event-file.ts (carry default and input.carry ?? []), src/core/usage/compute-usage.ts (the ?? [] fallbacks of the RAM limit and of waitingAt's made and settled lists), src/core/usage/read-events.ts (#restarts' initial value: "..." has no index, so find never matches it), src/daemon/dispatcher.ts and src/gateway/dispatcher.ts (the usage reader's tokenLabels default).
  • src/bus/event-file.ts: the carry key part for a missing worker or requester becomes another string, or the type check is dropped; Array.join turns undefined into the same empty text.
  • src/cli/index.ts: runStats receives rest instead of rest.slice(1); positionals are not read.
  • src/cli/stats.ts and src/core/usage/compute-usage.ts (requesters sort): < to <= in a sort over unique keys.
  • src/core/usage/compute-usage.ts turnaroundOf and spanOf: the undefined checks and fact.outcome?.at; every grant came from a stored request, so requestedAt and outcome are always set where they are read.
  • src/core/usage/compute-usage.ts waitingAt: madeBy < made.length to <=, and the bound dropped; reading one past the end compares undefined <= time, which is false.
  • src/core/usage/compute-usage.ts series: ramUsedBytes: undefined in place of leaving the key out; JSON drops it and toEqual reads it as absent.
  • src/core/usage/read-events.ts #relayedGrantFor: < to <= in choosing the relayed grant nearest the handover; two grants exactly as far apart in time on either side pick the later instead of the earlier, and a lease id reused after a release cannot have both within the same millisecond of the handover.
  • src/core/usage/read-events.ts #heldFor: e.index > grant.index to >=; the grant and an end are different events, so they never share an index.
  • src/core/usage/read-events.ts: the typeof check before Number.isFinite or !== null in number and objectOf; a missing request id stored under undefined (request.granted's three-way check and the worker grant's requestId), which no lookup reads; a worker-less device event adding undefined to a set only read by string keys, or testing worker !== undefined before a map write that is only read by string keys; !this.#counts(seen) || (incident === undefined && !failure) with its second half false, which then adds nothing; candidate.index >= request.index (two events never share an index).
  • src/core/usage/usage-reader.ts: oldestTs !== undefined && before oldestTs > asked.to (undefined > n is false); a missing prefix compared as the string "undefined"; a label stored as undefined; all read as no label.
  • src/contract/errors.ts: the value in a Record<_, true> whose keys are read, not its values.

Written by an agent.

V3RON added 18 commits October 5, 2026 16:28
…, on a worker and a gateway (#347)

Red: 50 failing. The contract (usage.get, HISTORY_NOT_KEPT, the output schema) is in
place; the figures module, the history reads, the handlers, the client method and the
CLI command are stubs.
… stats prints it

17 failing -> 0 failing (unit)
Adds tests for the usage error messages, the output schema's RAM and series fields, the no-token-store paths, the server route, the degraded client and the carry order; a step at the end of a window cuts no segment.
…ts request, give core/usage an index

window.to ceil -> floor; a killed rejection of a request made before the window is in no count (requestedBefore carried by the history read); core/usage/index.ts and a COMPONENTS row; a test for the unreadable oldestTs message.
…er, failures count by event name, waiting carries requests open at the window's start (#347)
…ucket

Whole file 4 tests, 6 s instead of minutes; no test timeout override.
0 failing -> 0 failing
@V3RON

V3RON commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Review notes

Not blocking, not verified. Each is one reviewer's claim.

  • code: src/core/usage/usage-reader.ts:67 HISTORY_NOT_KEPT is checked against the unrounded end, so in a new history's first bucket coversFrom can be later than window.to.
  • code: src/core/usage/usage-reader.ts:56-59 a window shorter than one bucket that crosses no edge rounds to from === to and answers no figures.
  • claims: src/core/usage/read-events.ts:481-482 the #refusedAtAdmission doc omits that a rejection of a pre-window request is skipped.
  • claims: docs/internal/COMPONENTS.md:65 lists computeUsage beside "surface: index.ts", which exports only UsageReader and tokenLabelMap.

Written by an agent.

V3RON added 8 commits October 9, 2026 22:39
The own-event guard was unreachable: a gateway's own event names no worker.
0 failing -> 0 failing
Fallow flagged countRequest for complexity.
0 failing -> 0 failing
Settle a carried fleet request at a restart whatever its time; drop the redundant
outcome filter, the grant kind and the ring re-sort; test a restart at the window
end, a rejected probe, an end with no relayed grant, worker order and label.
0 failing -> 0 failing
… --since 1h, fix stale docs

13 review findings: 13 open -> 0 open
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.

usage.get and simlock stats: the figures from the event history, on a worker and a gateway

1 participant