rigscope 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -0
- package/.next/app-path-routes-manifest.json +13 -0
- package/.next/build-manifest.json +20 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +68 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +297 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.json +369 -0
- package/.next/routes-manifest.json +114 -0
- package/.next/server/app/_global-error/page.js +31 -0
- package/.next/server/app/_global-error/page.js.nft.json +1 -0
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_global-error.html +1 -0
- package/.next/server/app/_global-error.meta +35 -0
- package/.next/server/app/_global-error.rsc +18 -0
- package/.next/server/app/_global-error.segments/_full.segment.rsc +18 -0
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +24 -0
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -0
- package/.next/server/app/_not-found/page.js +1 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +35 -0
- package/.next/server/app/_not-found.rsc +24 -0
- package/.next/server/app/_not-found.segments/_full.segment.rsc +24 -0
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +30 -0
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -0
- package/.next/server/app/administration/page.js +1 -0
- package/.next/server/app/administration/page.js.nft.json +1 -0
- package/.next/server/app/administration/page_client-reference-manifest.js +1 -0
- package/.next/server/app/administration.html +1 -0
- package/.next/server/app/administration.meta +34 -0
- package/.next/server/app/administration.rsc +29 -0
- package/.next/server/app/administration.segments/_full.segment.rsc +29 -0
- package/.next/server/app/administration.segments/_tree.segment.rsc +4 -0
- package/.next/server/app/administration.segments/administration/__PAGE__.segment.rsc +35 -0
- package/.next/server/app/alerts/page.js +1 -0
- package/.next/server/app/alerts/page.js.nft.json +1 -0
- package/.next/server/app/alerts/page_client-reference-manifest.js +1 -0
- package/.next/server/app/alerts.html +1 -0
- package/.next/server/app/alerts.meta +34 -0
- package/.next/server/app/alerts.rsc +28 -0
- package/.next/server/app/alerts.segments/_full.segment.rsc +28 -0
- package/.next/server/app/alerts.segments/_tree.segment.rsc +4 -0
- package/.next/server/app/alerts.segments/alerts/__PAGE__.segment.rsc +34 -0
- package/.next/server/app/connections/page.js +1 -0
- package/.next/server/app/connections/page.js.nft.json +1 -0
- package/.next/server/app/connections/page_client-reference-manifest.js +1 -0
- package/.next/server/app/connections.html +1 -0
- package/.next/server/app/connections.meta +34 -0
- package/.next/server/app/connections.rsc +29 -0
- package/.next/server/app/connections.segments/_full.segment.rsc +29 -0
- package/.next/server/app/connections.segments/_tree.segment.rsc +4 -0
- package/.next/server/app/connections.segments/connections/__PAGE__.segment.rsc +35 -0
- package/.next/server/app/history/page.js +2 -0
- package/.next/server/app/history/page.js.nft.json +1 -0
- package/.next/server/app/history/page_client-reference-manifest.js +1 -0
- package/.next/server/app/history.html +1 -0
- package/.next/server/app/history.meta +34 -0
- package/.next/server/app/history.rsc +33 -0
- package/.next/server/app/history.segments/_full.segment.rsc +33 -0
- package/.next/server/app/history.segments/_tree.segment.rsc +4 -0
- package/.next/server/app/history.segments/history/__PAGE__.segment.rsc +37 -0
- package/.next/server/app/icon.svg/route.js +1 -0
- package/.next/server/app/icon.svg/route.js.nft.json +1 -0
- package/.next/server/app/icon.svg.body +1 -0
- package/.next/server/app/icon.svg.meta +1 -0
- package/.next/server/app/index.html +1 -0
- package/.next/server/app/index.meta +29 -0
- package/.next/server/app/index.rsc +26 -0
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +31 -0
- package/.next/server/app/index.segments/_full.segment.rsc +26 -0
- package/.next/server/app/index.segments/_tree.segment.rsc +3 -0
- package/.next/server/app/login/page.js +1 -0
- package/.next/server/app/login/page.js.nft.json +1 -0
- package/.next/server/app/login/page_client-reference-manifest.js +1 -0
- package/.next/server/app/login.html +1 -0
- package/.next/server/app/login.meta +34 -0
- package/.next/server/app/login.rsc +26 -0
- package/.next/server/app/login.segments/_full.segment.rsc +26 -0
- package/.next/server/app/login.segments/_tree.segment.rsc +4 -0
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +32 -0
- package/.next/server/app/machines/[machineId]/page.js +1 -0
- package/.next/server/app/machines/[machineId]/page.js.nft.json +1 -0
- package/.next/server/app/machines/[machineId]/page_client-reference-manifest.js +1 -0
- package/.next/server/app/page.js +1 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/processes/page.js +2 -0
- package/.next/server/app/processes/page.js.nft.json +1 -0
- package/.next/server/app/processes/page_client-reference-manifest.js +1 -0
- package/.next/server/app/processes.html +1 -0
- package/.next/server/app/processes.meta +34 -0
- package/.next/server/app/processes.rsc +30 -0
- package/.next/server/app/processes.segments/_full.segment.rsc +30 -0
- package/.next/server/app/processes.segments/_tree.segment.rsc +4 -0
- package/.next/server/app/processes.segments/processes/__PAGE__.segment.rsc +36 -0
- package/.next/server/app-paths-manifest.json +13 -0
- package/.next/server/chunks/161.js +1 -0
- package/.next/server/chunks/168.js +1 -0
- package/.next/server/chunks/266.js +68 -0
- package/.next/server/chunks/430.js +116 -0
- package/.next/server/chunks/721.js +1 -0
- package/.next/server/chunks/751.js +1 -0
- package/.next/server/chunks/877.js +1 -0
- package/.next/server/chunks/894.js +2 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages-manifest.json +4 -0
- package/.next/server/prefetch-hints.json +123 -0
- package/.next/server/preview-props.json +5 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/NE6PGEiJxUnKRceuzUWPc/_buildManifest.js +1 -0
- package/.next/static/NE6PGEiJxUnKRceuzUWPc/_ssgManifest.js +1 -0
- package/.next/static/chunks/103-c47318242b24e9ac.js +1 -0
- package/.next/static/chunks/157-0d36ebf66af048ac.js +1 -0
- package/.next/static/chunks/227-417939c480f50747.js +1 -0
- package/.next/static/chunks/408-dc6158e0bb450ef6.js +1 -0
- package/.next/static/chunks/4bd1b696-c7408084347438ae.js +1 -0
- package/.next/static/chunks/794-ceeb732a1a81bc27.js +52 -0
- package/.next/static/chunks/939-2600d7c8e583d5ef.js +1 -0
- package/.next/static/chunks/app/_global-error/page-aba28f88d5781771.js +1 -0
- package/.next/static/chunks/app/_not-found/page-a2a510233e4836e2.js +1 -0
- package/.next/static/chunks/app/administration/page-4b16b19bb6a39c36.js +1 -0
- package/.next/static/chunks/app/alerts/page-fe0e877778c60af8.js +1 -0
- package/.next/static/chunks/app/connections/page-99cd54d6929b3f1f.js +1 -0
- package/.next/static/chunks/app/history/page-1475454737279ee0.js +3 -0
- package/.next/static/chunks/app/layout-4feff677c4071745.js +1 -0
- package/.next/static/chunks/app/login/page-c57e12dbe5146737.js +1 -0
- package/.next/static/chunks/app/machines/[machineId]/page-916ee73de5eb067d.js +1 -0
- package/.next/static/chunks/app/page-1ae9fc94c897785b.js +1 -0
- package/.next/static/chunks/app/processes/page-e471ed23a1d5632f.js +3 -0
- package/.next/static/chunks/framework-ea4f97ca039bd777.js +1 -0
- package/.next/static/chunks/main-a458d32948147b84.js +5 -0
- package/.next/static/chunks/main-app-f92328c8950d2e53.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-aba28f88d5781771.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-aba28f88d5781771.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/global-error-43cbaa3aa1576eb4.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-aba28f88d5781771.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-aba28f88d5781771.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-9ca43f4ce1bb8938.js +1 -0
- package/.next/static/css/e45c0c7ef5ba0336.css +1 -0
- package/ARCHITECTURE.md +279 -0
- package/CHANGELOG.md +37 -0
- package/DESIGN.md +299 -0
- package/LICENSE +21 -0
- package/README.md +74 -2
- package/SECURITY.md +12 -0
- package/THIRD_PARTY_NOTICES.md +8847 -0
- package/bin/rigscope.cjs +6 -0
- package/dist/cli.cjs +9075 -0
- package/dist/collector.cjs +5679 -0
- package/dist/hub.cjs +8364 -0
- package/dist/ops.cjs +4976 -0
- package/dist/system.cjs +4333 -0
- package/docs/bottlenecks.md +62 -0
- package/docs/configuration.md +85 -0
- package/docs/development.md +46 -0
- package/docs/exports.md +39 -0
- package/docs/installation.md +306 -0
- package/docs/monitoring.md +112 -0
- package/docs/operations.md +550 -0
- package/docs/releasing.md +154 -0
- package/docs/scale-acceptance.json +412 -0
- package/docs/scale-validation.json +1898 -0
- package/docs/scale-validation.md +294 -0
- package/docs/stress-2026-11-04/180301-probe-m100-u1-s1-20000ms.json +412 -0
- package/docs/stress-2026-11-04/180335-probe-m200-u1-s1-20000ms.json +412 -0
- package/docs/stress-2026-11-04/180409-probe-m400-u1-s1-20000ms.json +412 -0
- package/docs/stress-2026-11-04/180443-probe-m800-u1-s1-20000ms.json +412 -0
- package/docs/stress-2026-11-04/180518-probe-m600-u1-s1-20000ms.json +412 -0
- package/docs/stress-2026-11-04/180553-probe-m500-u1-s1-20000ms.json +412 -0
- package/docs/stress-2026-11-04/180628-probe-m550-u1-s1-20000ms.json +412 -0
- package/docs/stress-2026-11-04/180716-probe-m800-u1-s0-20000ms.json +415 -0
- package/docs/stress-2026-11-04/180751-probe-m1600-u1-s0-20000ms.json +415 -0
- package/docs/stress-2026-11-04/180828-probe-m3200-u1-s0-20000ms.json +416 -0
- package/docs/stress-2026-11-04/180941-probe-m2400-u1-s0-20000ms.json +416 -0
- package/docs/stress-2026-11-04/181031-probe-m2000-u1-s0-20000ms.json +416 -0
- package/docs/stress-2026-11-04/181114-probe-m1800-u1-s0-20000ms.json +416 -0
- package/docs/stress-2026-11-04/181203-probe-m100-u64-s64-20000ms.json +418 -0
- package/docs/stress-2026-11-04/181304-probe-m200-u64-s64-20000ms.json +418 -0
- package/docs/stress-2026-11-04/181405-probe-m400-u64-s64-20000ms.json +418 -0
- package/docs/stress-2026-11-04/181507-probe-m500-u64-s64-20000ms.json +418 -0
- package/docs/stress-2026-11-04/181609-probe-m450-u64-s64-20000ms.json +418 -0
- package/docs/stress-2026-11-04/181845-probe-m2-u2-s2-3000ms.json +419 -0
- package/docs/stress-2026-11-04/181931-probe-m500-u64-s64-30000ms.json +420 -0
- package/docs/stress-2026-11-04/182108-probe-m1800-u1-s0-120000ms.json +418 -0
- package/docs/stress-2026-11-04/182354-probe-m1200-u1-s0-120000ms.json +418 -0
- package/docs/stress-2026-11-04/182625-probe-m900-u1-s0-120000ms.json +418 -0
- package/docs/stress-2026-11-04/182847-probe-m1050-u1-s0-120000ms.json +418 -0
- package/docs/stress-2026-11-04/183112-probe-m1125-u1-s0-120000ms.json +418 -0
- package/docs/stress-2026-11-04/193608-probe-m500-u64-s64-120000ms.json +420 -0
- package/docs/stress-2026-11-04/193909-probe-m529-u1-s1-15000ms.json +420 -0
- package/docs/stress-2026-11-04/193940-probe-m530-u1-s1-15000ms.json +420 -0
- package/docs/stress-2026-11-04/194011-probe-m100-u1-s65-10000ms.json +421 -0
- package/docs/stress-2026-11-04/README.md +205 -0
- package/docs/stress-2026-11-04/index.jsonl +29 -0
- package/docs/stress-2026-11-04/manifest.json +13 -0
- package/docs/stress-2026-11-04/results.html +22 -0
- package/docs/system-installation.md +195 -0
- package/next.config.js +38 -0
- package/package.json +117 -4
- package/packaging/README.md +5 -0
- package/packaging/collector.env.example +24 -0
- package/packaging/hub.env.example +31 -0
- package/packaging/system/90-rigscope-rapl.rules +7 -0
- package/packaging/system/README.md +5 -0
- package/packaging/system/rigscope-collector.service.example +53 -0
- package/packaging/system/rigscope-hub.service.example +50 -0
- package/packaging/system/rigscope-rapl.sysusers +5 -0
- package/packaging/system/rigscope.sysusers +7 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Sustained scale validation
|
|
2
|
+
|
|
3
|
+
Status: implementation and local sustained measurement, 2026-11-04.
|
|
4
|
+
|
|
5
|
+
This document records the isolated sustained workload added after the earlier
|
|
6
|
+
`ops measure` burst. It is deliberately **not** a capacity guarantee. The
|
|
7
|
+
numbers below describe one host, one payload shape, one database and the
|
|
8
|
+
concurrency listed for each case. They do not prove that a larger fleet,
|
|
9
|
+
different hardware, slower storage or a different browser workload will behave
|
|
10
|
+
the same way.
|
|
11
|
+
|
|
12
|
+
## Later capacity search
|
|
13
|
+
|
|
14
|
+
The [November 4 capacity stress test](stress-2026-11-04/README.md) extends these
|
|
15
|
+
measurements with an exponential/binary search and two-minute confirmations:
|
|
16
|
+
|
|
17
|
+
- 500 collectors plus 64 live browser sessions passed at one sample and one
|
|
18
|
+
History poll per second per participant.
|
|
19
|
+
- Ingestion without live viewers passed at 1,050 collectors; 1,125 exceeded the
|
|
20
|
+
experiment's 500 ms acknowledgment p95 target.
|
|
21
|
+
- Full-fleet streaming passed with 529 representative machines, but 530 exceeded
|
|
22
|
+
the live feed's 1 MiB buffer guard and disconnected the viewer.
|
|
23
|
+
|
|
24
|
+
That report, its measurement JSON and chart are included in the npm package.
|
|
25
|
+
These results describe a bounded synthetic workload, not long-term capacity.
|
|
26
|
+
The five-case suite below remains the earlier sustained measurement.
|
|
27
|
+
|
|
28
|
+
## What was missing
|
|
29
|
+
|
|
30
|
+
The earlier `ops measure` command was a bounded synthetic burst capped at 50
|
|
31
|
+
synthetic machines; it queues a fixed number of records and by default runs
|
|
32
|
+
History query iterations sequentially unless --history-concurrency is supplied. It never opens a service socket, so it
|
|
33
|
+
cannot show concurrent browser requests, SSE delivery, slow consumers,
|
|
34
|
+
collector reconnects or replay. Those paths are now measured by a standalone
|
|
35
|
+
harness instead of being inferred from the burst runner.
|
|
36
|
+
|
|
37
|
+
`src/operations/measure.ts` was also extended:
|
|
38
|
+
|
|
39
|
+
- the synthetic machine bound is now 1,000 instead of 50,
|
|
40
|
+
- `historyConcurrency` initiates overlapping History request rounds and reports
|
|
41
|
+
their wall time, p95 and query count,
|
|
42
|
+
- the existing CLI arguments and report fields remain compatible.
|
|
43
|
+
|
|
44
|
+
## Harness and isolation
|
|
45
|
+
|
|
46
|
+
The harness is `scripts/scale-harness.ts`. It is a standalone source-checkout
|
|
47
|
+
tool, separate from the installed operational CLI. With development dependencies
|
|
48
|
+
installed, run it with `tsx`:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npx tsx scripts/scale-harness.ts --suite \
|
|
52
|
+
--json-out docs/scale-validation.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The parent process:
|
|
56
|
+
|
|
57
|
+
1. creates a fresh `0700` temporary directory and a `0600` hub configuration,
|
|
58
|
+
2. writes a throwaway SQLite database path,
|
|
59
|
+
3. spawns the same script in `hub` child mode on an ephemeral `127.0.0.1`
|
|
60
|
+
port,
|
|
61
|
+
4. logs in real browser sessions through `/api/auth/login` (including the
|
|
62
|
+
normal scrypt work and login token bucket),
|
|
63
|
+
5. connects real collector WebSockets through `/api/collector/connect`,
|
|
64
|
+
6. reads authenticated `/api/live` SSE streams and `/api/history` responses,
|
|
65
|
+
7. stops the child, closes the database and removes the temporary directory
|
|
66
|
+
unless `--keep` is set.
|
|
67
|
+
|
|
68
|
+
The child is composed from the production modules: `HubState`,
|
|
69
|
+
`TelemetryStore`, `SampleIngestion`, `CollectorGateway`, `LiveFeed`,
|
|
70
|
+
`BrowserHttpAuth`, `BrowserSessionStore` and `MonitorHttp`. The database uses
|
|
71
|
+
WAL, `synchronous=FULL` and the real commit-before-ack path. The only synthetic
|
|
72
|
+
parts are the metric values and the cadence driver. No live hub, service
|
|
73
|
+
socket, production database or Tailscale endpoint is contacted.
|
|
74
|
+
|
|
75
|
+
The harness exposes a private `GET /__scale/stats` control endpoint on the
|
|
76
|
+
isolated child. It returns event-loop delay percentiles, event-loop
|
|
77
|
+
utilization, process CPU/RSS, database/WAL bytes, telemetry store counters,
|
|
78
|
+
connected collector count and SSE client count. The endpoint is not part of
|
|
79
|
+
the hub and is never reachable from a deployment.
|
|
80
|
+
|
|
81
|
+
## Workload
|
|
82
|
+
|
|
83
|
+
Each case uses representative metrics: eight CPU cores, two GPUs, two
|
|
84
|
+
filesystems, two disk devices, two network interfaces, plus the optional
|
|
85
|
+
bottleneck fields (temperature, package power, I/O wait, pressure, inode use,
|
|
86
|
+
disk latency and utilization). One record carries roughly 50 stored series
|
|
87
|
+
points before rollups.
|
|
88
|
+
|
|
89
|
+
| Case | Machines | Measured phase | Live cadence | History viewers | SSE viewers | Special workload |
|
|
90
|
+
| --- | ---: | ---: | ---: | ---: | ---: | --- |
|
|
91
|
+
| `small-5m` | 5 | 60 s | 1 s | 4 | 2 | small-fleet baseline |
|
|
92
|
+
| `fleet-75m` | 75 | 60 s | 1 s | 8 | 4 | beyond the old 50-machine cap |
|
|
93
|
+
| `fleet-75m-churn` | 75 | 60 s | 1 s | 8 | 4 | 15 collectors reconnect every 15 s with 20 replay records; SSE viewers reconnect with `Last-Event-ID` |
|
|
94
|
+
| `fleet-75m-slow-sse` | 75 | 60 s | 1 s | 8 | 6 | two SSE viewers pause reading for 15 s, measuring stale delivery and any buffer drop/reconnect |
|
|
95
|
+
| `fleet-75m-history` | 75 | 60 s | 1 s | 8 | 2 | broad History metric set every 300 ms per viewer |
|
|
96
|
+
|
|
97
|
+
Before each measured phase the harness connects all collectors and waits
|
|
98
|
+
1.5 seconds. The measured phase starts after that warmup. Records are counted
|
|
99
|
+
from collector startup, so the ingestion counters include the warmup and bounded drain tail; the
|
|
100
|
+
event-loop, CPU, RSS and database-growth windows are reset at the measured
|
|
101
|
+
phase boundary.
|
|
102
|
+
|
|
103
|
+
### Bounds
|
|
104
|
+
|
|
105
|
+
- machines: 1–8,192 (the total-record bound below still applies),
|
|
106
|
+
- measured phase: up to 1 hour,
|
|
107
|
+
- sample cadence: at least 100 ms (the protocol rate limit),
|
|
108
|
+
- records per live batch: 1–32,
|
|
109
|
+
- viewers: 1–200 authenticated sessions,
|
|
110
|
+
- SSE attempts: 0–65; the hub still admits at most 64 (zero isolates ingestion,
|
|
111
|
+
and the 65th attempt measures admission rejection),
|
|
112
|
+
- replay: at most 128 records per reconnect,
|
|
113
|
+
- estimated live plus replay records: at most 500,000 per case,
|
|
114
|
+
- database: 512 MiB main+WAL by default (`--database-budget-mb 0` disables the
|
|
115
|
+
cap for inspection only),
|
|
116
|
+
- temporary database/config directory: removed after the case unless
|
|
117
|
+
`--keep` is used.
|
|
118
|
+
|
|
119
|
+
## Host
|
|
120
|
+
|
|
121
|
+
The measurement host was:
|
|
122
|
+
|
|
123
|
+
- AMD EPYC 7402P 24-Core Processor, 48 logical CPUs,
|
|
124
|
+
- 121 GiB usable memory (130,351,214,592 bytes reported by the OS),
|
|
125
|
+
- Linux 7.3.0-9-generic, Ubuntu 26.10, x86_64,
|
|
126
|
+
- Node.js 26.13.0,
|
|
127
|
+
- local NVMe root filesystem (the harness database lived under `/tmp`),
|
|
128
|
+
- load average 0.92 / 0.76 / 0.84 around the run; unrelated user services were
|
|
129
|
+
active, so this is not a quiet benchmark host.
|
|
130
|
+
|
|
131
|
+
`monitorEventLoopDelay` used a 10 ms resolution. The reported p50 therefore
|
|
132
|
+
floors near 10 ms even when the process is mostly idle; p95/p99/max are the
|
|
133
|
+
meaningful tail values.
|
|
134
|
+
|
|
135
|
+
## Results
|
|
136
|
+
|
|
137
|
+
All five cases completed with zero rejected records, zero `retry` acknowledgments,
|
|
138
|
+
zero unacknowledged live records, zero write failures and no disk-budget
|
|
139
|
+
pressure. The event-loop window excludes the 1.5 s collector warmup. "History /
|
|
140
|
+
SSE" is authenticated History sessions / SSE clients. "Sample→commit" in the original report is capture-to-durable-ack latency: an
|
|
141
|
+
upper bound on commit lag that also includes the acknowledgment return path.
|
|
142
|
+
It is measured on the same host clock, not across uncalibrated remote clocks.
|
|
143
|
+
"Normal SSE lag" is the
|
|
144
|
+
age of the newest captured sample when an `update`/`snapshot` arrives at a
|
|
145
|
+
normal viewer; it includes the hub's 500 ms coalescing window. Hub CPU is the
|
|
146
|
+
hub child's process CPU as a percentage of one core, not the whole host.
|
|
147
|
+
|
|
148
|
+
| Case | Machines | History / SSE | Records accepted | Ack p95 | Sample→commit p95 | History p95 / max | Normal SSE lag p95 | Event-loop p99 / max | Hub CPU | RSS peak | DB+WAL growth |
|
|
149
|
+
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
150
|
+
| `small-5m` | 5 | 4 / 2 | 317 / 317 | 26.7 ms | 27 ms | 12.9 / 20.0 ms | 526 ms | 12.5 / 18.5 ms | 2.6% | 135.9 MiB | 4.63 MiB |
|
|
151
|
+
| `fleet-75m` | 75 | 8 / 4 | 4,789 / 4,789 | 55.6 ms | 56 ms | 20.9 / 35.2 ms | 439 ms | 30.4 / 44.6 ms | 12.4% | 188.7 MiB | 12.08 MiB |
|
|
152
|
+
| `fleet-75m-churn` | 75 | 8 / 4 | 4,845 / 4,845 | 52.2 ms | 52 ms | 23.4 / 35.0 ms | 438 ms | 29.0 / 42.4 ms | 14.2% | 203.2 MiB | 14.22 MiB |
|
|
153
|
+
| `fleet-75m-slow-sse` | 75 | 8 / 6 | 4,791 / 4,791 | 64.4 ms | 64 ms | 23.0 / 36.6 ms | 502 ms | 38.5 / 76.2 ms | 12.0% | 190.2 MiB | 12.95 MiB |
|
|
154
|
+
| `fleet-75m-history` | 75 | 8 / 2 | 4,785 / 4,785 | 70.7 ms | 71 ms | 50.3 / 60.5 ms | 453 ms | 48.0 / 72.7 ms | 31.2% | 229.7 MiB | 12.98 MiB |
|
|
155
|
+
|
|
156
|
+
Additional observations:
|
|
157
|
+
|
|
158
|
+
- **History authorization budget held.** The broad case ran 1,608
|
|
159
|
+
authenticated queries (8 sessions polling every 300 ms) with 1,608 `200`
|
|
160
|
+
responses, zero `429` responses and zero errors. Each response returned
|
|
161
|
+
about 25 series and 800 points; the 25,000-row/100 ms budget was not
|
|
162
|
+
truncated.
|
|
163
|
+
- **Collector reconnect/replay recovered.** The churn case performed 45
|
|
164
|
+
collector reconnects and replayed 900 historical records; all 900 were
|
|
165
|
+
accepted. First live acknowledgment after reconnect was 167.9 ms p95
|
|
166
|
+
(175.8 ms max). The durable queue finished empty.
|
|
167
|
+
- **SSE reconnect/reset recovered.** The churn case reconnected 48 SSE streams
|
|
168
|
+
and received 48 `reset` snapshots; all resets followed a reconnect. First
|
|
169
|
+
event after reconnect was 10.1 ms p95.
|
|
170
|
+
- **Slow consumers stayed connected but stale.** The two paused viewers
|
|
171
|
+
received no new bytes for 15 s and then read buffered snapshots. Their
|
|
172
|
+
delivery lag was 11.6 s p95 while normal viewers stayed at 502 ms p95. On
|
|
173
|
+
this loopback path no slow-viewer drop or reconnect was observed, so the
|
|
174
|
+
hub's 1 MiB writable-buffer guard did not fire; kernel socket buffering
|
|
175
|
+
likely absorbed the pause. The deliberate disconnect/reconnect path is
|
|
176
|
+
covered by the churn case instead.
|
|
177
|
+
- **Database/WAL stayed bounded.** The final main+WAL size was 14.8 MiB for
|
|
178
|
+
the 75-machine baseline, 16.9 MiB with churn, and 15.7 MiB with broad
|
|
179
|
+
History. The 512 MiB measurement budget was never approached, no checkpoint
|
|
180
|
+
pressure appeared, and the store queue was empty at the end of every case.
|
|
181
|
+
- **Small fleet stayed cheap.** Five machines, four History viewers and two SSE
|
|
182
|
+
viewers used 2.6% of one core and stayed at 12.5 ms event-loop p99.
|
|
183
|
+
|
|
184
|
+
The full machine-readable report, including per-case store counters, database
|
|
185
|
+
sizes, SSE byte counts, tier counts and host load averages, is
|
|
186
|
+
[scale-validation.json](scale-validation.json). The generated JSON is the raw
|
|
187
|
+
artifact; this page is the summary.
|
|
188
|
+
|
|
189
|
+
## Interpretation
|
|
190
|
+
|
|
191
|
+
These results support the following limited statements on this host:
|
|
192
|
+
|
|
193
|
+
- A 75-machine, 1-second-cadence synthetic fleet can be ingested with
|
|
194
|
+
commit-before-ack p95 under 100 ms while eight authenticated History viewers
|
|
195
|
+
and four full-fleet SSE viewers run concurrently.
|
|
196
|
+
- Collector reconnect plus 20-record replay per affected machine recovered
|
|
197
|
+
within a few hundred milliseconds in this run, without rejected or unacked
|
|
198
|
+
records.
|
|
199
|
+
- SSE reconnect with `Last-Event-ID` produced a fresh `reset` snapshot within
|
|
200
|
+
about 10 ms on loopback, and the next update was fresh again.
|
|
201
|
+
- Broad History polling near the per-session token budget raised the
|
|
202
|
+
event-loop tail to about 48 ms p99 and used about 31% of one core, but
|
|
203
|
+
produced no 429s and no truncated responses at this data size.
|
|
204
|
+
|
|
205
|
+
They do **not** support claims that:
|
|
206
|
+
|
|
207
|
+
- 75 machines is a capacity limit or a safe upper bound,
|
|
208
|
+
- a larger fleet, more viewers, slower storage or a different payload will meet
|
|
209
|
+
the same latencies,
|
|
210
|
+
- the hub is multi-core, clustered or horizontally scalable,
|
|
211
|
+
- slow consumers are always disconnected before they fall behind,
|
|
212
|
+
- the default 100 concurrent collector socket budget should be raised without
|
|
213
|
+
a deployment-specific measurement.
|
|
214
|
+
|
|
215
|
+
The 75-machine cases used the default 100-connection budget. A deployment that
|
|
216
|
+
raises `HUB_MAX_COLLECTOR_CONNECTIONS` must re-run the harness with its own
|
|
217
|
+
payloads, storage and viewer counts.
|
|
218
|
+
|
|
219
|
+
## Reproduction
|
|
220
|
+
|
|
221
|
+
For the later resource-guarded capacity search, including the distinction between
|
|
222
|
+
collector throughput and full-fleet stream limits, see
|
|
223
|
+
[the November 4 stress test](stress-2026-11-04/README.md).
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
# full five-case suite (about five minutes)
|
|
227
|
+
npx tsx scripts/scale-harness.ts --suite \
|
|
228
|
+
--json-out docs/scale-validation.json
|
|
229
|
+
|
|
230
|
+
# one case with overrides
|
|
231
|
+
npx tsx scripts/scale-harness.ts --case fleet-75m \
|
|
232
|
+
--machines 75 --viewers 8 --sse-clients 4 --duration-ms 60000 \
|
|
233
|
+
--json-out /tmp/rigscope-scale-75m.json
|
|
234
|
+
|
|
235
|
+
# retain the throwaway database and configuration for inspection
|
|
236
|
+
npx tsx scripts/scale-harness.ts --case small-5m --keep
|
|
237
|
+
|
|
238
|
+
# Keep stream parsing independent of collector sample generation
|
|
239
|
+
npx tsx scripts/scale-harness.ts --case fleet-75m \
|
|
240
|
+
--machines 500 --viewers 64 --sse-clients 64 --sse-workers 4 \
|
|
241
|
+
--duration-ms 60000 --json-out /tmp/rigscope-scale-mixed.json
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The short `tests/scale-harness.test.ts` smoke runs the same child with two
|
|
245
|
+
machines, collector reconnect/replay and an SSE reset. It is not a capacity
|
|
246
|
+
test; it proves the harness paths work in CI.
|
|
247
|
+
|
|
248
|
+
## Limits
|
|
249
|
+
|
|
250
|
+
- The hub child and all load generators share one host. The hub event loop uses
|
|
251
|
+
one thread; the load processes can run on other cores. This is
|
|
252
|
+
not a distributed-load test and the generator competes with the hub.
|
|
253
|
+
`--sse-workers N` separates stream readers into up to eight processes. With
|
|
254
|
+
workers enabled, a one-second client warmup precedes the measured window;
|
|
255
|
+
without workers, collector generation and stream parsing share an event loop,
|
|
256
|
+
which can under-drive large mixed workloads. Report configuration records
|
|
257
|
+
the worker count and client warmup. The original five results used no workers.
|
|
258
|
+
- History queries are synchronous SQLite work on the hub event loop.
|
|
259
|
+
"Concurrent viewers" means overlapping authenticated HTTP requests, not
|
|
260
|
+
parallel SQLite readers.
|
|
261
|
+
- The payload is representative but not captured from a real fleet. Different
|
|
262
|
+
process counts, GPUs, filesystems or optional fields change the stored row
|
|
263
|
+
count.
|
|
264
|
+
- The host may run unrelated services. Load averages and CPU are recorded in
|
|
265
|
+
the JSON artifact, but they are not a controlled benchmark environment.
|
|
266
|
+
- Passing these cases does not remove the concurrent collector socket budget
|
|
267
|
+
(default 100, configurable with `HUB_MAX_COLLECTOR_CONNECTIONS`) or the
|
|
268
|
+
history row/elapsed/frame/storage budgets.
|
|
269
|
+
- Authentication and machine scopes use the production modules with loopback
|
|
270
|
+
origin rules. This harness does not exercise the deployed HTTPS/private-socket
|
|
271
|
+
boundary; its separate runtime and actual-path checks remain distinct.
|
|
272
|
+
- A result is evidence for this host and workload only. It is not a claim of
|
|
273
|
+
unlimited machine, viewer or History capacity.
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
## Root integration acceptance
|
|
277
|
+
|
|
278
|
+
After integration, a separate 75-machine, eight-second churn run used eight
|
|
279
|
+
History sessions and four SSE consumers. Its authorization canary required
|
|
280
|
+
anonymous SSE 401, an allowed scoped History query 200, a forbidden machine query
|
|
281
|
+
403, and correctly filtered initial and reconnect/reset SSE snapshots. All passed.
|
|
282
|
+
The run accepted 930 live records plus 600 replay records, recorded 30 collector
|
|
283
|
+
reconnects, finished with zero unacked live records and zero write failures.
|
|
284
|
+
`scale-acceptance.json` records this shorter integration check. It is not another
|
|
285
|
+
60-second capacity measurement; the five sustained results above remain the
|
|
286
|
+
worker's original raw artifact from before the authorization canary was added.
|
|
287
|
+
|
|
288
|
+
The original `sampleToCommitLagMs` field names are retained for compatibility.
|
|
289
|
+
Those numbers are capture-to-durable-ack upper bounds on commit latency, including
|
|
290
|
+
the return path; they are not a server-side SQL commit timestamp. Queue/backpressure
|
|
291
|
+
counters and end-of-run queue emptiness are recorded, but the original report does
|
|
292
|
+
not sample an exact queue high-water mark. Slow viewers did not reach the server's
|
|
293
|
+
buffer-disconnect threshold in these runs, so that threshold is not a measured
|
|
294
|
+
fleet-capacity limit.
|