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.
Files changed (224) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-path-routes-manifest.json +13 -0
  3. package/.next/build-manifest.json +20 -0
  4. package/.next/export-marker.json +6 -0
  5. package/.next/images-manifest.json +68 -0
  6. package/.next/next-minimal-server.js.nft.json +1 -0
  7. package/.next/next-server.js.nft.json +1 -0
  8. package/.next/package.json +1 -0
  9. package/.next/prerender-manifest.json +297 -0
  10. package/.next/react-loadable-manifest.json +1 -0
  11. package/.next/required-server-files.json +369 -0
  12. package/.next/routes-manifest.json +114 -0
  13. package/.next/server/app/_global-error/page.js +31 -0
  14. package/.next/server/app/_global-error/page.js.nft.json +1 -0
  15. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -0
  16. package/.next/server/app/_global-error.html +1 -0
  17. package/.next/server/app/_global-error.meta +35 -0
  18. package/.next/server/app/_global-error.rsc +18 -0
  19. package/.next/server/app/_global-error.segments/_full.segment.rsc +18 -0
  20. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +24 -0
  21. package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -0
  22. package/.next/server/app/_not-found/page.js +1 -0
  23. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  24. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  25. package/.next/server/app/_not-found.html +1 -0
  26. package/.next/server/app/_not-found.meta +35 -0
  27. package/.next/server/app/_not-found.rsc +24 -0
  28. package/.next/server/app/_not-found.segments/_full.segment.rsc +24 -0
  29. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +30 -0
  30. package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -0
  31. package/.next/server/app/administration/page.js +1 -0
  32. package/.next/server/app/administration/page.js.nft.json +1 -0
  33. package/.next/server/app/administration/page_client-reference-manifest.js +1 -0
  34. package/.next/server/app/administration.html +1 -0
  35. package/.next/server/app/administration.meta +34 -0
  36. package/.next/server/app/administration.rsc +29 -0
  37. package/.next/server/app/administration.segments/_full.segment.rsc +29 -0
  38. package/.next/server/app/administration.segments/_tree.segment.rsc +4 -0
  39. package/.next/server/app/administration.segments/administration/__PAGE__.segment.rsc +35 -0
  40. package/.next/server/app/alerts/page.js +1 -0
  41. package/.next/server/app/alerts/page.js.nft.json +1 -0
  42. package/.next/server/app/alerts/page_client-reference-manifest.js +1 -0
  43. package/.next/server/app/alerts.html +1 -0
  44. package/.next/server/app/alerts.meta +34 -0
  45. package/.next/server/app/alerts.rsc +28 -0
  46. package/.next/server/app/alerts.segments/_full.segment.rsc +28 -0
  47. package/.next/server/app/alerts.segments/_tree.segment.rsc +4 -0
  48. package/.next/server/app/alerts.segments/alerts/__PAGE__.segment.rsc +34 -0
  49. package/.next/server/app/connections/page.js +1 -0
  50. package/.next/server/app/connections/page.js.nft.json +1 -0
  51. package/.next/server/app/connections/page_client-reference-manifest.js +1 -0
  52. package/.next/server/app/connections.html +1 -0
  53. package/.next/server/app/connections.meta +34 -0
  54. package/.next/server/app/connections.rsc +29 -0
  55. package/.next/server/app/connections.segments/_full.segment.rsc +29 -0
  56. package/.next/server/app/connections.segments/_tree.segment.rsc +4 -0
  57. package/.next/server/app/connections.segments/connections/__PAGE__.segment.rsc +35 -0
  58. package/.next/server/app/history/page.js +2 -0
  59. package/.next/server/app/history/page.js.nft.json +1 -0
  60. package/.next/server/app/history/page_client-reference-manifest.js +1 -0
  61. package/.next/server/app/history.html +1 -0
  62. package/.next/server/app/history.meta +34 -0
  63. package/.next/server/app/history.rsc +33 -0
  64. package/.next/server/app/history.segments/_full.segment.rsc +33 -0
  65. package/.next/server/app/history.segments/_tree.segment.rsc +4 -0
  66. package/.next/server/app/history.segments/history/__PAGE__.segment.rsc +37 -0
  67. package/.next/server/app/icon.svg/route.js +1 -0
  68. package/.next/server/app/icon.svg/route.js.nft.json +1 -0
  69. package/.next/server/app/icon.svg.body +1 -0
  70. package/.next/server/app/icon.svg.meta +1 -0
  71. package/.next/server/app/index.html +1 -0
  72. package/.next/server/app/index.meta +29 -0
  73. package/.next/server/app/index.rsc +26 -0
  74. package/.next/server/app/index.segments/__PAGE__.segment.rsc +31 -0
  75. package/.next/server/app/index.segments/_full.segment.rsc +26 -0
  76. package/.next/server/app/index.segments/_tree.segment.rsc +3 -0
  77. package/.next/server/app/login/page.js +1 -0
  78. package/.next/server/app/login/page.js.nft.json +1 -0
  79. package/.next/server/app/login/page_client-reference-manifest.js +1 -0
  80. package/.next/server/app/login.html +1 -0
  81. package/.next/server/app/login.meta +34 -0
  82. package/.next/server/app/login.rsc +26 -0
  83. package/.next/server/app/login.segments/_full.segment.rsc +26 -0
  84. package/.next/server/app/login.segments/_tree.segment.rsc +4 -0
  85. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +32 -0
  86. package/.next/server/app/machines/[machineId]/page.js +1 -0
  87. package/.next/server/app/machines/[machineId]/page.js.nft.json +1 -0
  88. package/.next/server/app/machines/[machineId]/page_client-reference-manifest.js +1 -0
  89. package/.next/server/app/page.js +1 -0
  90. package/.next/server/app/page.js.nft.json +1 -0
  91. package/.next/server/app/page_client-reference-manifest.js +1 -0
  92. package/.next/server/app/processes/page.js +2 -0
  93. package/.next/server/app/processes/page.js.nft.json +1 -0
  94. package/.next/server/app/processes/page_client-reference-manifest.js +1 -0
  95. package/.next/server/app/processes.html +1 -0
  96. package/.next/server/app/processes.meta +34 -0
  97. package/.next/server/app/processes.rsc +30 -0
  98. package/.next/server/app/processes.segments/_full.segment.rsc +30 -0
  99. package/.next/server/app/processes.segments/_tree.segment.rsc +4 -0
  100. package/.next/server/app/processes.segments/processes/__PAGE__.segment.rsc +36 -0
  101. package/.next/server/app-paths-manifest.json +13 -0
  102. package/.next/server/chunks/161.js +1 -0
  103. package/.next/server/chunks/168.js +1 -0
  104. package/.next/server/chunks/266.js +68 -0
  105. package/.next/server/chunks/430.js +116 -0
  106. package/.next/server/chunks/721.js +1 -0
  107. package/.next/server/chunks/751.js +1 -0
  108. package/.next/server/chunks/877.js +1 -0
  109. package/.next/server/chunks/894.js +2 -0
  110. package/.next/server/functions-config-manifest.json +4 -0
  111. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  112. package/.next/server/middleware-build-manifest.js +1 -0
  113. package/.next/server/middleware-manifest.json +6 -0
  114. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  115. package/.next/server/next-font-manifest.js +1 -0
  116. package/.next/server/next-font-manifest.json +1 -0
  117. package/.next/server/pages/404.html +1 -0
  118. package/.next/server/pages/500.html +1 -0
  119. package/.next/server/pages-manifest.json +4 -0
  120. package/.next/server/prefetch-hints.json +123 -0
  121. package/.next/server/preview-props.json +5 -0
  122. package/.next/server/server-reference-manifest.js +1 -0
  123. package/.next/server/server-reference-manifest.json +1 -0
  124. package/.next/server/webpack-runtime.js +1 -0
  125. package/.next/static/NE6PGEiJxUnKRceuzUWPc/_buildManifest.js +1 -0
  126. package/.next/static/NE6PGEiJxUnKRceuzUWPc/_ssgManifest.js +1 -0
  127. package/.next/static/chunks/103-c47318242b24e9ac.js +1 -0
  128. package/.next/static/chunks/157-0d36ebf66af048ac.js +1 -0
  129. package/.next/static/chunks/227-417939c480f50747.js +1 -0
  130. package/.next/static/chunks/408-dc6158e0bb450ef6.js +1 -0
  131. package/.next/static/chunks/4bd1b696-c7408084347438ae.js +1 -0
  132. package/.next/static/chunks/794-ceeb732a1a81bc27.js +52 -0
  133. package/.next/static/chunks/939-2600d7c8e583d5ef.js +1 -0
  134. package/.next/static/chunks/app/_global-error/page-aba28f88d5781771.js +1 -0
  135. package/.next/static/chunks/app/_not-found/page-a2a510233e4836e2.js +1 -0
  136. package/.next/static/chunks/app/administration/page-4b16b19bb6a39c36.js +1 -0
  137. package/.next/static/chunks/app/alerts/page-fe0e877778c60af8.js +1 -0
  138. package/.next/static/chunks/app/connections/page-99cd54d6929b3f1f.js +1 -0
  139. package/.next/static/chunks/app/history/page-1475454737279ee0.js +3 -0
  140. package/.next/static/chunks/app/layout-4feff677c4071745.js +1 -0
  141. package/.next/static/chunks/app/login/page-c57e12dbe5146737.js +1 -0
  142. package/.next/static/chunks/app/machines/[machineId]/page-916ee73de5eb067d.js +1 -0
  143. package/.next/static/chunks/app/page-1ae9fc94c897785b.js +1 -0
  144. package/.next/static/chunks/app/processes/page-e471ed23a1d5632f.js +3 -0
  145. package/.next/static/chunks/framework-ea4f97ca039bd777.js +1 -0
  146. package/.next/static/chunks/main-a458d32948147b84.js +5 -0
  147. package/.next/static/chunks/main-app-f92328c8950d2e53.js +1 -0
  148. package/.next/static/chunks/next/dist/client/components/builtin/app-error-aba28f88d5781771.js +1 -0
  149. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-aba28f88d5781771.js +1 -0
  150. package/.next/static/chunks/next/dist/client/components/builtin/global-error-43cbaa3aa1576eb4.js +1 -0
  151. package/.next/static/chunks/next/dist/client/components/builtin/not-found-aba28f88d5781771.js +1 -0
  152. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-aba28f88d5781771.js +1 -0
  153. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  154. package/.next/static/chunks/webpack-9ca43f4ce1bb8938.js +1 -0
  155. package/.next/static/css/e45c0c7ef5ba0336.css +1 -0
  156. package/ARCHITECTURE.md +279 -0
  157. package/CHANGELOG.md +37 -0
  158. package/DESIGN.md +299 -0
  159. package/LICENSE +21 -0
  160. package/README.md +74 -2
  161. package/SECURITY.md +12 -0
  162. package/THIRD_PARTY_NOTICES.md +8847 -0
  163. package/bin/rigscope.cjs +6 -0
  164. package/dist/cli.cjs +9075 -0
  165. package/dist/collector.cjs +5679 -0
  166. package/dist/hub.cjs +8364 -0
  167. package/dist/ops.cjs +4976 -0
  168. package/dist/system.cjs +4333 -0
  169. package/docs/bottlenecks.md +62 -0
  170. package/docs/configuration.md +85 -0
  171. package/docs/development.md +46 -0
  172. package/docs/exports.md +39 -0
  173. package/docs/installation.md +306 -0
  174. package/docs/monitoring.md +112 -0
  175. package/docs/operations.md +550 -0
  176. package/docs/releasing.md +154 -0
  177. package/docs/scale-acceptance.json +412 -0
  178. package/docs/scale-validation.json +1898 -0
  179. package/docs/scale-validation.md +294 -0
  180. package/docs/stress-2026-11-04/180301-probe-m100-u1-s1-20000ms.json +412 -0
  181. package/docs/stress-2026-11-04/180335-probe-m200-u1-s1-20000ms.json +412 -0
  182. package/docs/stress-2026-11-04/180409-probe-m400-u1-s1-20000ms.json +412 -0
  183. package/docs/stress-2026-11-04/180443-probe-m800-u1-s1-20000ms.json +412 -0
  184. package/docs/stress-2026-11-04/180518-probe-m600-u1-s1-20000ms.json +412 -0
  185. package/docs/stress-2026-11-04/180553-probe-m500-u1-s1-20000ms.json +412 -0
  186. package/docs/stress-2026-11-04/180628-probe-m550-u1-s1-20000ms.json +412 -0
  187. package/docs/stress-2026-11-04/180716-probe-m800-u1-s0-20000ms.json +415 -0
  188. package/docs/stress-2026-11-04/180751-probe-m1600-u1-s0-20000ms.json +415 -0
  189. package/docs/stress-2026-11-04/180828-probe-m3200-u1-s0-20000ms.json +416 -0
  190. package/docs/stress-2026-11-04/180941-probe-m2400-u1-s0-20000ms.json +416 -0
  191. package/docs/stress-2026-11-04/181031-probe-m2000-u1-s0-20000ms.json +416 -0
  192. package/docs/stress-2026-11-04/181114-probe-m1800-u1-s0-20000ms.json +416 -0
  193. package/docs/stress-2026-11-04/181203-probe-m100-u64-s64-20000ms.json +418 -0
  194. package/docs/stress-2026-11-04/181304-probe-m200-u64-s64-20000ms.json +418 -0
  195. package/docs/stress-2026-11-04/181405-probe-m400-u64-s64-20000ms.json +418 -0
  196. package/docs/stress-2026-11-04/181507-probe-m500-u64-s64-20000ms.json +418 -0
  197. package/docs/stress-2026-11-04/181609-probe-m450-u64-s64-20000ms.json +418 -0
  198. package/docs/stress-2026-11-04/181845-probe-m2-u2-s2-3000ms.json +419 -0
  199. package/docs/stress-2026-11-04/181931-probe-m500-u64-s64-30000ms.json +420 -0
  200. package/docs/stress-2026-11-04/182108-probe-m1800-u1-s0-120000ms.json +418 -0
  201. package/docs/stress-2026-11-04/182354-probe-m1200-u1-s0-120000ms.json +418 -0
  202. package/docs/stress-2026-11-04/182625-probe-m900-u1-s0-120000ms.json +418 -0
  203. package/docs/stress-2026-11-04/182847-probe-m1050-u1-s0-120000ms.json +418 -0
  204. package/docs/stress-2026-11-04/183112-probe-m1125-u1-s0-120000ms.json +418 -0
  205. package/docs/stress-2026-11-04/193608-probe-m500-u64-s64-120000ms.json +420 -0
  206. package/docs/stress-2026-11-04/193909-probe-m529-u1-s1-15000ms.json +420 -0
  207. package/docs/stress-2026-11-04/193940-probe-m530-u1-s1-15000ms.json +420 -0
  208. package/docs/stress-2026-11-04/194011-probe-m100-u1-s65-10000ms.json +421 -0
  209. package/docs/stress-2026-11-04/README.md +205 -0
  210. package/docs/stress-2026-11-04/index.jsonl +29 -0
  211. package/docs/stress-2026-11-04/manifest.json +13 -0
  212. package/docs/stress-2026-11-04/results.html +22 -0
  213. package/docs/system-installation.md +195 -0
  214. package/next.config.js +38 -0
  215. package/package.json +117 -4
  216. package/packaging/README.md +5 -0
  217. package/packaging/collector.env.example +24 -0
  218. package/packaging/hub.env.example +31 -0
  219. package/packaging/system/90-rigscope-rapl.rules +7 -0
  220. package/packaging/system/README.md +5 -0
  221. package/packaging/system/rigscope-collector.service.example +53 -0
  222. package/packaging/system/rigscope-hub.service.example +50 -0
  223. package/packaging/system/rigscope-rapl.sysusers +5 -0
  224. 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.