rigscope 0.1.0 → 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 (123) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +3 -3
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  5. package/.next/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_not-found.html +1 -1
  12. package/.next/server/app/_not-found.rsc +2 -2
  13. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  14. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  15. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  16. package/.next/server/app/administration/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/administration.html +1 -1
  18. package/.next/server/app/administration.rsc +2 -2
  19. package/.next/server/app/administration.segments/_full.segment.rsc +2 -2
  20. package/.next/server/app/administration.segments/_tree.segment.rsc +2 -2
  21. package/.next/server/app/administration.segments/administration/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/alerts/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/alerts.html +1 -1
  24. package/.next/server/app/alerts.rsc +2 -2
  25. package/.next/server/app/alerts.segments/_full.segment.rsc +2 -2
  26. package/.next/server/app/alerts.segments/_tree.segment.rsc +2 -2
  27. package/.next/server/app/alerts.segments/alerts/__PAGE__.segment.rsc +2 -2
  28. package/.next/server/app/connections/page_client-reference-manifest.js +1 -1
  29. package/.next/server/app/connections.html +1 -1
  30. package/.next/server/app/connections.rsc +2 -2
  31. package/.next/server/app/connections.segments/_full.segment.rsc +2 -2
  32. package/.next/server/app/connections.segments/_tree.segment.rsc +2 -2
  33. package/.next/server/app/connections.segments/connections/__PAGE__.segment.rsc +2 -2
  34. package/.next/server/app/history/page.js +2 -2
  35. package/.next/server/app/history/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/history.html +1 -1
  37. package/.next/server/app/history.rsc +4 -4
  38. package/.next/server/app/history.segments/_full.segment.rsc +4 -4
  39. package/.next/server/app/history.segments/_tree.segment.rsc +2 -2
  40. package/.next/server/app/history.segments/history/__PAGE__.segment.rsc +4 -4
  41. package/.next/server/app/index.html +1 -1
  42. package/.next/server/app/index.rsc +2 -2
  43. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  44. package/.next/server/app/index.segments/_full.segment.rsc +2 -2
  45. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  46. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  47. package/.next/server/app/login.html +1 -1
  48. package/.next/server/app/login.rsc +2 -2
  49. package/.next/server/app/login.segments/_full.segment.rsc +2 -2
  50. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  51. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  52. package/.next/server/app/machines/[machineId]/page.js +1 -1
  53. package/.next/server/app/machines/[machineId]/page_client-reference-manifest.js +1 -1
  54. package/.next/server/app/page_client-reference-manifest.js +1 -1
  55. package/.next/server/app/processes/page_client-reference-manifest.js +1 -1
  56. package/.next/server/app/processes.html +1 -1
  57. package/.next/server/app/processes.rsc +2 -2
  58. package/.next/server/app/processes.segments/_full.segment.rsc +2 -2
  59. package/.next/server/app/processes.segments/_tree.segment.rsc +2 -2
  60. package/.next/server/app/processes.segments/processes/__PAGE__.segment.rsc +2 -2
  61. package/.next/server/app-paths-manifest.json +3 -3
  62. package/.next/server/chunks/168.js +1 -1
  63. package/.next/server/middleware-build-manifest.js +1 -1
  64. package/.next/server/pages/404.html +1 -1
  65. package/.next/server/pages/500.html +1 -1
  66. package/.next/static/chunks/408-dc6158e0bb450ef6.js +1 -0
  67. package/.next/static/chunks/app/history/{page-3fa5846eca26b7f7.js → page-1475454737279ee0.js} +2 -2
  68. package/.next/static/chunks/app/machines/[machineId]/page-916ee73de5eb067d.js +1 -0
  69. package/.next/static/css/e45c0c7ef5ba0336.css +1 -0
  70. package/CHANGELOG.md +12 -0
  71. package/README.md +53 -136
  72. package/dist/collector.cjs +9 -1
  73. package/dist/hub.cjs +12 -3
  74. package/dist/ops.cjs +6 -2
  75. package/docs/bottlenecks.md +1 -1
  76. package/docs/configuration.md +85 -0
  77. package/docs/development.md +1 -1
  78. package/docs/installation.md +205 -376
  79. package/docs/monitoring.md +112 -0
  80. package/docs/operations.md +103 -68
  81. package/docs/releasing.md +16 -14
  82. package/docs/scale-validation.md +34 -3
  83. package/docs/stress-2026-11-04/180301-probe-m100-u1-s1-20000ms.json +412 -0
  84. package/docs/stress-2026-11-04/180335-probe-m200-u1-s1-20000ms.json +412 -0
  85. package/docs/stress-2026-11-04/180409-probe-m400-u1-s1-20000ms.json +412 -0
  86. package/docs/stress-2026-11-04/180443-probe-m800-u1-s1-20000ms.json +412 -0
  87. package/docs/stress-2026-11-04/180518-probe-m600-u1-s1-20000ms.json +412 -0
  88. package/docs/stress-2026-11-04/180553-probe-m500-u1-s1-20000ms.json +412 -0
  89. package/docs/stress-2026-11-04/180628-probe-m550-u1-s1-20000ms.json +412 -0
  90. package/docs/stress-2026-11-04/180716-probe-m800-u1-s0-20000ms.json +415 -0
  91. package/docs/stress-2026-11-04/180751-probe-m1600-u1-s0-20000ms.json +415 -0
  92. package/docs/stress-2026-11-04/180828-probe-m3200-u1-s0-20000ms.json +416 -0
  93. package/docs/stress-2026-11-04/180941-probe-m2400-u1-s0-20000ms.json +416 -0
  94. package/docs/stress-2026-11-04/181031-probe-m2000-u1-s0-20000ms.json +416 -0
  95. package/docs/stress-2026-11-04/181114-probe-m1800-u1-s0-20000ms.json +416 -0
  96. package/docs/stress-2026-11-04/181203-probe-m100-u64-s64-20000ms.json +418 -0
  97. package/docs/stress-2026-11-04/181304-probe-m200-u64-s64-20000ms.json +418 -0
  98. package/docs/stress-2026-11-04/181405-probe-m400-u64-s64-20000ms.json +418 -0
  99. package/docs/stress-2026-11-04/181507-probe-m500-u64-s64-20000ms.json +418 -0
  100. package/docs/stress-2026-11-04/181609-probe-m450-u64-s64-20000ms.json +418 -0
  101. package/docs/stress-2026-11-04/181845-probe-m2-u2-s2-3000ms.json +419 -0
  102. package/docs/stress-2026-11-04/181931-probe-m500-u64-s64-30000ms.json +420 -0
  103. package/docs/stress-2026-11-04/182108-probe-m1800-u1-s0-120000ms.json +418 -0
  104. package/docs/stress-2026-11-04/182354-probe-m1200-u1-s0-120000ms.json +418 -0
  105. package/docs/stress-2026-11-04/182625-probe-m900-u1-s0-120000ms.json +418 -0
  106. package/docs/stress-2026-11-04/182847-probe-m1050-u1-s0-120000ms.json +418 -0
  107. package/docs/stress-2026-11-04/183112-probe-m1125-u1-s0-120000ms.json +418 -0
  108. package/docs/stress-2026-11-04/193608-probe-m500-u64-s64-120000ms.json +420 -0
  109. package/docs/stress-2026-11-04/193909-probe-m529-u1-s1-15000ms.json +420 -0
  110. package/docs/stress-2026-11-04/193940-probe-m530-u1-s1-15000ms.json +420 -0
  111. package/docs/stress-2026-11-04/194011-probe-m100-u1-s65-10000ms.json +421 -0
  112. package/docs/stress-2026-11-04/README.md +205 -0
  113. package/docs/stress-2026-11-04/index.jsonl +29 -0
  114. package/docs/stress-2026-11-04/manifest.json +13 -0
  115. package/docs/stress-2026-11-04/results.html +22 -0
  116. package/docs/system-installation.md +195 -0
  117. package/package.json +9 -1
  118. package/packaging/system/README.md +1 -1
  119. package/.next/static/chunks/408-18819c6490a5f8d3.js +0 -1
  120. package/.next/static/chunks/app/machines/[machineId]/page-a5aa1d8676272f38.js +0 -1
  121. package/.next/static/css/e41ccbccc3afaae5.css +0 -1
  122. /package/.next/static/{DZKWF9ur7H5wI0c7RAhdq → NE6PGEiJxUnKRceuzUWPc}/_buildManifest.js +0 -0
  123. /package/.next/static/{DZKWF9ur7H5wI0c7RAhdq → NE6PGEiJxUnKRceuzUWPc}/_ssgManifest.js +0 -0
@@ -0,0 +1,421 @@
1
+ {
2
+ "tool": "rigscope-scale-harness",
3
+ "synthetic": true,
4
+ "disclaimer": "Synthetic bounded workload on an isolated temporary hub/database, not a capacity, latency, or hardware guarantee. Values depend on this host, disk, Node version, payload, cadence, viewer count and concurrent activity.",
5
+ "generatedAt": "2026-11-04T19:40:30.382Z",
6
+ "node": "v26.13.0",
7
+ "host": {
8
+ "platform": "linux",
9
+ "release": "7.3.0-9-generic",
10
+ "arch": "x64",
11
+ "cpuModel": "AMD EPYC 7402P 24-Core Processor",
12
+ "logicalCores": 48,
13
+ "totalMemoryBytes": 130351214592,
14
+ "loadAverage": [
15
+ 0.92,
16
+ 0.93,
17
+ 0.57
18
+ ],
19
+ "node": "v26.13.0"
20
+ },
21
+ "command": "npx tsx scripts/scale-harness.ts --case fleet-75m --machines 100 --viewers 1 --sse-clients 65 --sse-workers 4 --duration-ms 10000 --history-interval-ms 1000 --database-budget-mb 1024 --json-out /home/triet/Project/rigscope/docs/stress-2026-11-04/194011-probe-m100-u1-s65-10000ms.json",
22
+ "suite": [
23
+ {
24
+ "name": "fleet-75m",
25
+ "description": "75 machines with eight concurrent History viewers and four SSE viewers.",
26
+ "machines": 100,
27
+ "durationMs": 10000,
28
+ "sampleIntervalMs": 1000,
29
+ "recordsPerBatch": 1,
30
+ "viewers": 1,
31
+ "sseClients": 65,
32
+ "slowSseClients": 0,
33
+ "slowSsePauseAfterMs": 0,
34
+ "slowSsePauseMs": 0,
35
+ "historyIntervalMs": 1000,
36
+ "historyMetricSet": "standard",
37
+ "churnEveryMs": null,
38
+ "churnMachines": 0,
39
+ "replayRecords": 0
40
+ }
41
+ ],
42
+ "cases": [
43
+ {
44
+ "name": "fleet-75m",
45
+ "description": "75 machines with eight concurrent History viewers and four SSE viewers.",
46
+ "config": {
47
+ "name": "fleet-75m",
48
+ "description": "75 machines with eight concurrent History viewers and four SSE viewers.",
49
+ "machines": 100,
50
+ "durationMs": 10000,
51
+ "sampleIntervalMs": 1000,
52
+ "recordsPerBatch": 1,
53
+ "viewers": 1,
54
+ "sseClients": 65,
55
+ "slowSseClients": 0,
56
+ "slowSsePauseAfterMs": 0,
57
+ "slowSsePauseMs": 0,
58
+ "historyIntervalMs": 1000,
59
+ "historyMetricSet": "standard",
60
+ "churnEveryMs": null,
61
+ "churnMachines": 0,
62
+ "replayRecords": 0,
63
+ "warmupMs": 1500,
64
+ "clientWarmupMs": 1000,
65
+ "sseWorkers": 4,
66
+ "maxCollectorConnections": 110,
67
+ "databaseBudgetBytes": 1073741824
68
+ },
69
+ "host": {
70
+ "platform": "linux",
71
+ "release": "7.3.0-9-generic",
72
+ "arch": "x64",
73
+ "cpuModel": "AMD EPYC 7402P 24-Core Processor",
74
+ "logicalCores": 48,
75
+ "totalMemoryBytes": 130351214592,
76
+ "loadAverage": [
77
+ 1,
78
+ 0.94,
79
+ 0.57
80
+ ],
81
+ "node": "v26.13.0"
82
+ },
83
+ "authorization": {
84
+ "anonymousDenied": true,
85
+ "scopedHistory": true,
86
+ "scopedSnapshot": true,
87
+ "scopedReset": true
88
+ },
89
+ "ingestion": {
90
+ "recordsSent": 1500,
91
+ "recordsAccepted": 1500,
92
+ "recordsRejected": 0,
93
+ "replayRecordsSent": 0,
94
+ "replayRecordsAccepted": 0,
95
+ "retryAcks": 0,
96
+ "unackedLiveRecords": 0,
97
+ "sendErrors": 0,
98
+ "ackLatencyMs": {
99
+ "count": 1500,
100
+ "min": 3.32,
101
+ "avg": 39.4,
102
+ "p50": 38.02,
103
+ "p95": 63.78,
104
+ "p99": 67.24,
105
+ "max": 71.89
106
+ },
107
+ "sampleToCommitLagMs": {
108
+ "count": 1500,
109
+ "min": 3,
110
+ "avg": 39.39,
111
+ "p50": 38,
112
+ "p95": 64,
113
+ "p99": 67,
114
+ "max": 72
115
+ },
116
+ "messagesSent": 1600,
117
+ "backpressureSkips": 0,
118
+ "connects": 100,
119
+ "reconnects": 0,
120
+ "reconnectFailures": 0,
121
+ "collectorRecoveryMs": {
122
+ "count": 0,
123
+ "min": 0,
124
+ "avg": 0,
125
+ "p50": 0,
126
+ "p95": 0,
127
+ "p99": 0,
128
+ "max": 0
129
+ }
130
+ },
131
+ "history": {
132
+ "queries": 10,
133
+ "ok": 10,
134
+ "rateLimited": 0,
135
+ "errors": 0,
136
+ "latencyMs": {
137
+ "count": 10,
138
+ "min": 3.83,
139
+ "avg": 4.49,
140
+ "p50": 4.66,
141
+ "p95": 5.28,
142
+ "p99": 5.28,
143
+ "max": 5.28
144
+ },
145
+ "series": {
146
+ "count": 10,
147
+ "min": 4,
148
+ "avg": 4,
149
+ "p50": 4,
150
+ "p95": 4,
151
+ "p99": 4,
152
+ "max": 4
153
+ },
154
+ "points": {
155
+ "count": 10,
156
+ "min": 12,
157
+ "avg": 30,
158
+ "p50": 32,
159
+ "p95": 48,
160
+ "p99": 48,
161
+ "max": 48
162
+ },
163
+ "truncated": 0,
164
+ "tiers": {
165
+ "raw": 10
166
+ }
167
+ },
168
+ "stream": {
169
+ "clients": 65,
170
+ "httpStatuses": {
171
+ "200": 64,
172
+ "503": 1
173
+ },
174
+ "slowClients": 0,
175
+ "events": 896,
176
+ "snapshots": 64,
177
+ "updates": 832,
178
+ "resets": 0,
179
+ "bytes": 176808198,
180
+ "eventLagMs": {
181
+ "count": 896,
182
+ "min": 24,
183
+ "avg": 428.26,
184
+ "p50": 440,
185
+ "p95": 481,
186
+ "p99": 847,
187
+ "max": 874
188
+ },
189
+ "normalEventLagMs": {
190
+ "count": 896,
191
+ "min": 24,
192
+ "avg": 428.26,
193
+ "p50": 440,
194
+ "p95": 481,
195
+ "p99": 847,
196
+ "max": 874
197
+ },
198
+ "slowEventLagMs": {
199
+ "count": 0,
200
+ "min": 0,
201
+ "avg": 0,
202
+ "p50": 0,
203
+ "p95": 0,
204
+ "p99": 0,
205
+ "max": 0
206
+ },
207
+ "firstEventMs": {
208
+ "count": 64,
209
+ "min": 28.8,
210
+ "avg": 100.63,
211
+ "p50": 108,
212
+ "p95": 145.31,
213
+ "p99": 151.84,
214
+ "max": 151.84
215
+ },
216
+ "reconnects": 0,
217
+ "resetsAfterReconnect": 0,
218
+ "slowPauses": 0,
219
+ "slowDrops": 1,
220
+ "sseRecoveryMs": {
221
+ "count": 0,
222
+ "min": 0,
223
+ "avg": 0,
224
+ "p50": 0,
225
+ "p95": 0,
226
+ "p99": 0,
227
+ "max": 0
228
+ }
229
+ },
230
+ "hub": {
231
+ "baseline": {
232
+ "pid": 204091,
233
+ "uptimeMs": 3300.52,
234
+ "hubRunId": "bf98eaa6-f0f6-4b1b-b64c-3d92f712bdcf",
235
+ "eventLoopDelayMs": {
236
+ "p50": 10.11,
237
+ "p95": 13.43,
238
+ "p99": 24.13,
239
+ "max": 25.85
240
+ },
241
+ "eventLoopUtilization": 0.1791,
242
+ "cpuPercent": 22.05,
243
+ "cpuMs": {
244
+ "user": 219.44,
245
+ "system": 50.09
246
+ },
247
+ "rssBytes": 151478272,
248
+ "rssPeakBytes": 151478272,
249
+ "heapUsedBytes": 42367504,
250
+ "database": {
251
+ "bytes": 6317192,
252
+ "walBytes": 4412552,
253
+ "mainBytes": 1904640
254
+ },
255
+ "store": {
256
+ "queuedRecords": 0,
257
+ "queuedBatches": 0,
258
+ "lastFlushMs": 1793821214429,
259
+ "lastFlushDurationMs": 7,
260
+ "committedRecords": 300,
261
+ "duplicateRecords": 0,
262
+ "rejectedRecords": 0,
263
+ "writeFailures": 0,
264
+ "lastWriteError": null,
265
+ "diskFullFailures": 0,
266
+ "dbBytes": 6317192,
267
+ "dbWalBytes": 4412552,
268
+ "dbBytesLimit": 1073741824,
269
+ "retention": {
270
+ "lastRunAtMs": 0,
271
+ "lastRunDurationMs": 0,
272
+ "deletedRows": 0,
273
+ "pressure": false,
274
+ "effectiveRawCutoffMs": null,
275
+ "physicalBytes": 6317192,
276
+ "physicalLimitBytes": 1073741824,
277
+ "physicalPressure": false,
278
+ "walBytes": 4412552
279
+ }
280
+ },
281
+ "liveClients": 64,
282
+ "sessions": 2,
283
+ "connectedMachines": 100,
284
+ "configuredMachines": 100,
285
+ "snapshotBytes": 197185
286
+ },
287
+ "window": {
288
+ "pid": 204091,
289
+ "uptimeMs": 13306.01,
290
+ "hubRunId": "bf98eaa6-f0f6-4b1b-b64c-3d92f712bdcf",
291
+ "eventLoopDelayMs": {
292
+ "p50": 10.11,
293
+ "p95": 17.2,
294
+ "p99": 37.03,
295
+ "max": 48.04
296
+ },
297
+ "eventLoopUtilization": 0.1304,
298
+ "cpuPercent": 15.65,
299
+ "cpuMs": {
300
+ "user": 1233.71,
301
+ "system": 331.49
302
+ },
303
+ "rssBytes": 170266624,
304
+ "rssPeakBytes": 170266624,
305
+ "heapUsedBytes": 53632832,
306
+ "database": {
307
+ "bytes": 8240520,
308
+ "walBytes": 4808072,
309
+ "mainBytes": 3432448
310
+ },
311
+ "store": {
312
+ "queuedRecords": 0,
313
+ "queuedBatches": 0,
314
+ "lastFlushMs": 1793821224449,
315
+ "lastFlushDurationMs": 14,
316
+ "committedRecords": 1300,
317
+ "duplicateRecords": 0,
318
+ "rejectedRecords": 0,
319
+ "writeFailures": 0,
320
+ "lastWriteError": null,
321
+ "diskFullFailures": 0,
322
+ "dbBytes": 8240520,
323
+ "dbWalBytes": 4808072,
324
+ "dbBytesLimit": 1073741824,
325
+ "retention": {
326
+ "lastRunAtMs": 0,
327
+ "lastRunDurationMs": 0,
328
+ "deletedRows": 0,
329
+ "pressure": false,
330
+ "effectiveRawCutoffMs": null,
331
+ "physicalBytes": 8240520,
332
+ "physicalLimitBytes": 1073741824,
333
+ "physicalPressure": false,
334
+ "walBytes": 4808072
335
+ }
336
+ },
337
+ "liveClients": 64,
338
+ "sessions": 2,
339
+ "connectedMachines": 100,
340
+ "configuredMachines": 100,
341
+ "snapshotBytes": 197285
342
+ },
343
+ "final": {
344
+ "pid": 204091,
345
+ "uptimeMs": 16312.78,
346
+ "hubRunId": "bf98eaa6-f0f6-4b1b-b64c-3d92f712bdcf",
347
+ "eventLoopDelayMs": {
348
+ "p50": 10.11,
349
+ "p95": 11.39,
350
+ "p99": 36.01,
351
+ "max": 59.15
352
+ },
353
+ "eventLoopUtilization": 0.1049,
354
+ "cpuPercent": 12.59,
355
+ "cpuMs": {
356
+ "user": 278.31,
357
+ "system": 99.94
358
+ },
359
+ "rssBytes": 157626368,
360
+ "rssPeakBytes": 170659840,
361
+ "heapUsedBytes": 33033944,
362
+ "database": {
363
+ "bytes": 8678792,
364
+ "walBytes": 4808072,
365
+ "mainBytes": 3870720
366
+ },
367
+ "store": {
368
+ "queuedRecords": 0,
369
+ "queuedBatches": 0,
370
+ "lastFlushMs": 1793821226444,
371
+ "lastFlushDurationMs": 1,
372
+ "committedRecords": 1500,
373
+ "duplicateRecords": 0,
374
+ "rejectedRecords": 0,
375
+ "writeFailures": 0,
376
+ "lastWriteError": null,
377
+ "diskFullFailures": 0,
378
+ "dbBytes": 8678792,
379
+ "dbWalBytes": 4808072,
380
+ "dbBytesLimit": 1073741824,
381
+ "retention": {
382
+ "lastRunAtMs": 0,
383
+ "lastRunDurationMs": 0,
384
+ "deletedRows": 0,
385
+ "pressure": false,
386
+ "effectiveRawCutoffMs": null,
387
+ "physicalBytes": 8678792,
388
+ "physicalLimitBytes": 1073741824,
389
+ "physicalPressure": false,
390
+ "walBytes": 4808072
391
+ }
392
+ },
393
+ "liveClients": 64,
394
+ "sessions": 2,
395
+ "connectedMachines": 100,
396
+ "configuredMachines": 100,
397
+ "snapshotBytes": 197285
398
+ },
399
+ "databaseGrowth": {
400
+ "bytes": 2361600,
401
+ "walBytes": 395520,
402
+ "mainBytes": 1966080
403
+ }
404
+ },
405
+ "elapsedMs": 10000.82,
406
+ "artifacts": {
407
+ "directory": "/tmp/rigscope-scale-8QIQV0",
408
+ "kept": false,
409
+ "configPath": "/tmp/rigscope-scale-8QIQV0/hub-config.json",
410
+ "databasePath": "/tmp/rigscope-scale-8QIQV0/scale.sqlite"
411
+ }
412
+ }
413
+ ],
414
+ "limitations": [
415
+ "Collector payload values and cadence are synthetic; the hub, database, auth, SSE and History paths are real modules.",
416
+ "The hub child and all load generators share one host; the hub event loop is single-threaded, while load processes can use other cores. This is not distributed load.",
417
+ "History queries are synchronous on the hub event loop, so 'concurrent' viewers model overlapping HTTP requests, not parallel SQLite readers.",
418
+ "The measurement never contacts the live service; it uses a fresh temporary database and loopback ephemeral port.",
419
+ "A passing case is evidence for this host and workload, not an unlimited-capacity or latency guarantee."
420
+ ]
421
+ }
@@ -0,0 +1,205 @@
1
+ # Hub capacity stress test — 2026-11-04
2
+
3
+ On this AMD EPYC 7402P host (24 cores / 48 threads, 121 GiB RAM), the isolated
4
+ hub passed **500 collectors plus 64 concurrent live browser sessions** for two
5
+ minutes. Every collector sent one representative sample per second; every
6
+ session also polled History once per second. Ack p95 was **223 ms**, History p95
7
+ was **181 ms**, hub CPU averaged **75% of one core**, and sampled hub RSS peaked
8
+ at **560 MiB**. All 64 streams stayed connected. All 7,744 History requests
9
+ succeeded, all submitted live records were acknowledged, and the final queue
10
+ was empty.
11
+
12
+ These are synthetic measurements on this machine, using production hub modules.
13
+ They are **not a long-term capacity guarantee or a test of the deployed HTTPS
14
+ service, real browser rendering, or a mature multi-day history database**.
15
+
16
+ ## Three different limits
17
+
18
+ - **Application limits:** 100 collector sockets by default, configurable with
19
+ `HUB_MAX_COLLECTOR_CONNECTIONS`; 50 user accounts; 64 simultaneous live browser
20
+ streams; 256 stored browser sessions. Accounts, logged-in sessions, and open
21
+ streams are different counts. The tests used independent sessions on one
22
+ shared all-machines account, plus a scoped account for authorization checks.
23
+ - **Ingestion without live streams:** 1,050 collectors passed the two-minute
24
+ target; 1,125 exceeded the chosen 500 ms ack p95 target (538 ms). Thus the
25
+ observed two-minute latency boundary is **between 1,050 and 1,125** at 1 Hz.
26
+ At 1,200, ack p95 rose to 2.15 seconds, retries appeared, and the final queue
27
+ held 2,321 records. This is an ingestion measurement with one light History
28
+ session, not a usable full-fleet dashboard capacity.
29
+ - **Full-fleet snapshot delivery:** 529 collectors passed, but 530 disconnected
30
+ even one fast loopback viewer after its initial snapshot. The 529-machine
31
+ snapshot was 1,047,111 bytes; at 530 it was 1,049,092 bytes. The live feed calls
32
+ `res.write()` and then destroys the connection when `writableLength` exceeds
33
+ its **1,048,576-byte guard**. A single sufficiently large snapshot triggers
34
+ that guard. The 530-machine run kept ingestion healthy (~532 commits/s,
35
+ 58 ms ack p95) but delivered **zero live updates**, confirming that the failure
36
+ is in streaming. The exact machine count depends on payload size and access
37
+ scope; the 1 MiB guard is the relevant constraint.
38
+
39
+ A separate admission probe opened 65 streams: **64 returned HTTP 200, and one
40
+ returned HTTP 503**. That is the configured stream cap, not exhausted hardware.
41
+
42
+ Source references: [collector socket budget](https://github.com/Triet1402/rigscope/blob/main/src/hub/collector-ws.ts),
43
+ [collector configuration](https://github.com/Triet1402/rigscope/blob/main/src/hub/config.ts),
44
+ [user account limit](https://github.com/Triet1402/rigscope/blob/main/src/hub/auth/config.ts),
45
+ [session capacity](https://github.com/Triet1402/rigscope/blob/main/src/hub/auth/sessions.ts), and
46
+ [live feed admission and buffer guard](https://github.com/Triet1402/rigscope/blob/main/src/hub/sse.ts).
47
+
48
+ ## Measurements
49
+
50
+ | Collectors | History / live sessions | Seconds | Ack p95 | History p95 | Hub CPU¹ | Hub RSS peak | Result | Evidence |
51
+ | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | --- |
52
+ | 900 | 1 / 0 | 120 | 72 ms | 27 ms | 75.1% | 225 MiB | Pass | [182625-probe-m900-u1-s0-120000ms.json](182625-probe-m900-u1-s0-120000ms.json) |
53
+ | 1,050 | 1 / 0 | 120 | 140 ms | 28 ms | 82.2% | 260 MiB | Pass | [182847-probe-m1050-u1-s0-120000ms.json](182847-probe-m1050-u1-s0-120000ms.json) |
54
+ | 1,125 | 1 / 0 | 120 | 538 ms | 31 ms | 86.7% | 259 MiB | Outside target | [183112-probe-m1125-u1-s0-120000ms.json](183112-probe-m1125-u1-s0-120000ms.json) |
55
+ | 1,200 | 1 / 0 | 120 | 2,154 ms | 30 ms | 92.4% | 262 MiB | Outside target | [182354-probe-m1200-u1-s0-120000ms.json](182354-probe-m1200-u1-s0-120000ms.json) |
56
+ | 1,800 | 1 / 0 | 20 | 88 ms | 24 ms | 103.4% | 291 MiB | Pass | [181114-probe-m1800-u1-s0-20000ms.json](181114-probe-m1800-u1-s0-20000ms.json) |
57
+ | 1,800 | 1 / 0 | 120 | 3,872 ms | 34 ms | 108.3% | 311 MiB | Outside target | [182108-probe-m1800-u1-s0-120000ms.json](182108-probe-m1800-u1-s0-120000ms.json) |
58
+ | 500 | 64 / 64 | 120 | 223 ms | 181 ms | 74.7% | 560 MiB | Pass | [193608-probe-m500-u64-s64-120000ms.json](193608-probe-m500-u64-s64-120000ms.json) |
59
+ | 529 | 1 / 1 | 15 | 58 ms | 9 ms | 45.7% | 200 MiB | Pass | [193909-probe-m529-u1-s1-15000ms.json](193909-probe-m529-u1-s1-15000ms.json) |
60
+ | 530 | 1 / 1 | 15 | 58 ms | 18 ms | 45.0% | 189 MiB | Outside target | [193940-probe-m530-u1-s1-15000ms.json](193940-probe-m530-u1-s1-15000ms.json) |
61
+ | 100 | 1 / 65 | 10 | 64 ms | 5 ms | 15.7% | 162 MiB | Outside target | [194011-probe-m100-u1-s65-10000ms.json](194011-probe-m100-u1-s65-10000ms.json) |
62
+
63
+ ¹ Hub CPU is process CPU as a percentage of one core, including its helper
64
+ threads. It is not the percentage of the 48-thread machine. Windows and
65
+ percentiles have the boundaries described below. “Outside target” includes
66
+ intentionally exceeding admission limits; it does not mean a hub crash.
67
+
68
+ The 1,800-collector result is included to show why a short burst is insufficient:
69
+ it passed for 20 seconds, then failed over 120 seconds with 3.87-second ack p95,
70
+ retry acknowledgments and a growing queue. The 1,125 run crossed only the ack
71
+ latency target; it did not lose submitted records or finish with backlog.
72
+
73
+ ## Method and pass criteria
74
+
75
+ The existing `scripts/scale-harness.ts` creates a fresh private temporary
76
+ configuration and SQLite database for every run and binds a throwaway child to
77
+ an ephemeral loopback port. It drives real authenticated collector WebSockets,
78
+ SSE and History HTTP requests through production modules, including durable
79
+ commit-before-ack, WAL and `synchronous=FULL`. The installed service and its
80
+ configuration/database are never used as load targets.
81
+
82
+ The payload represents eight CPU cores, two GPUs, two filesystems, two disk
83
+ devices and two network interfaces, with optional bottleneck counters. Each
84
+ collector sends one record every 1,000 ms. History requests select one machine,
85
+ four standard series and a five-minute range, though the fresh database only
86
+ contains the run's data. All live sessions receive the full fleet. Enrollment,
87
+ process detail, active alert rules/webhooks, static page delivery and TLS are
88
+ outside this workload.
89
+
90
+ The isolated collector admission budget is raised automatically above the
91
+ requested fleet size. This does not change a deployed hub's 100-connection
92
+ default. The queue was aligned with production at 4,096 records and transactions
93
+ at 32 records. The session store uses the production 256-session default. The
94
+ live-stream cap remains 64. Database main+WAL was capped at 1 GiB for these
95
+ experiments; no run reached the cap or reported SQLite write failures.
96
+
97
+ An initial exponential search doubled load, then bisected the passing/failing
98
+ interval to about 12.5%. Follow-up two-minute runs narrowed the ingestion
99
+ boundary, and single-machine probes narrowed the snapshot boundary. Targets
100
+ were chosen for this experiment, not taken from a published product SLA:
101
+
102
+ - ack p95 ≤500 ms; History p95 ≤1,000 ms;
103
+ - hub event-loop p99 ≤200 ms;
104
+ - normal SSE newest-sample age p95 ≤2,000 ms;
105
+ - measured durable commit rate ≥95% of requested cadence;
106
+ - successful History polls ≥90% of requested cadence;
107
+ - no retries, rejected/unacked records, collector send errors, or cadence skips;
108
+ - no History errors, HTTP 429s or truncated queries;
109
+ - all requested live streams remain present and receive ongoing updates;
110
+ - final queue empty, with no write failures or physical-budget pressure.
111
+
112
+ Durable throughput is computed from the difference in the hub's commit counters
113
+ across the measured window. Ack latency is send-to-durable-ack round-trip time,
114
+ including startup and the drain tail. SSE lag is the age of the newest machine
115
+ sample in each received snapshot; it does not establish freshness of every
116
+ machine. History/SSE totals and percentile samples include the bounded tail.
117
+
118
+ ### Correcting the load generator
119
+
120
+ The first mixed-load search used a single parent event loop for collectors,
121
+ History polling and parsing all 64 streams. It appeared to stop around 400–450
122
+ collectors, but it was under-producing samples while every submitted record
123
+ was still accepted. Those cases remain in the raw index as diagnostic evidence;
124
+ they are **not** the mixed-load capacity conclusion.
125
+
126
+ The harness now supports `--sse-workers N`. The final mixed runs use four
127
+ independent stream-reader processes, with full JSON parsing, and a one-second
128
+ client warmup before resetting the measured window. The collector driver and
129
+ History polling stay in the parent process. The final 500/64 run achieved
130
+ 99.96% of requested durable sampling throughput. Single-stream boundary probes
131
+ use one reader worker. Older results without workers include stream startup
132
+ in the measured window. A three-second, two-machine worker smoke is also in
133
+ the index; its cadence classification is not a capacity result.
134
+
135
+ ### Resource protection
136
+
137
+ `scripts/stress-hub.py` supervises the entire test process group at one-second
138
+ intervals, including the hub, driver and stream readers, at reduced scheduling
139
+ priority. It stops the group for:
140
+
141
+ - less than 16 GiB available host RAM;
142
+ - more than 4 GiB summed test-process RSS;
143
+ - less than 10 GiB free disk;
144
+ - more than 256 MiB of new host swap-out;
145
+ - over 80% host CPU for five consecutive samples;
146
+ - excessive setup/measurement time.
147
+
148
+ Across the 29 measured attempts, sampled test-process RSS peaked at **1,475 MiB**,
149
+ host CPU peaked at **9.69%**, available RAM stayed above **108.2 GiB**, and new
150
+ swap-out was **zero**. No resource cutoff fired. Existing used swap was about
151
+ 7.6 GiB before testing; it was not new pressure from these tests.
152
+
153
+ The hub's CPU and synchronous storage work become limiting while most host
154
+ cores and RAM remain available. The final mixed run received about 11.7 GB of
155
+ SSE data including warmup/drain; loopback does not establish capacity over a
156
+ real network link. Different CPU-core counts, devices, scopes, History breadth,
157
+ retention age or network conditions can change these results substantially.
158
+
159
+ ## Reproduce
160
+
161
+ Requires Linux, Python 3, Node 26+ and the checkout's development dependencies.
162
+ Run from the repository root. Each attempt saves raw JSON, a log and a resource
163
+ trace under the dated output directory; `--out PATH` overrides it. Logs and
164
+ per-process resource traces are ignored by Git and stay local. Measurement JSON
165
+ can be committed with a reviewed report.
166
+
167
+ ```sh
168
+ # Quick exponential/binary ingestion search; follow with longer confirmation
169
+ python3 scripts/stress-hub.py --search collectors --machines 100 --sse 0 --duration-ms 20000
170
+
171
+ # Passing two-minute ingestion load and its nearby failing target
172
+ python3 scripts/stress-hub.py --machines 1050 --users 1 --sse 0 --duration-ms 120000
173
+ python3 scripts/stress-hub.py --machines 1125 --users 1 --sse 0 --duration-ms 120000
174
+
175
+ # Confirmed two-minute mixed workload; stream workers default to four
176
+ python3 scripts/stress-hub.py --machines 500 --users 64 --sse 64 --duration-ms 120000
177
+
178
+ # Snapshot-size and stream-admission probes (expected failures at 530 and 65)
179
+ python3 scripts/stress-hub.py --machines 529 --users 1 --sse 1 --sse-workers 1 --duration-ms 15000
180
+ python3 scripts/stress-hub.py --machines 530 --users 1 --sse 1 --sse-workers 1 --duration-ms 15000
181
+ python3 scripts/stress-hub.py --machines 100 --users 1 --sse 65 --duration-ms 10000
182
+ ```
183
+
184
+ The runner records a failed target as `"pass": false` in its report and continues
185
+ a search; intentional over-limit probes are ordinary successful runner
186
+ executions. Read the report rather than using the Python exit status as a
187
+ performance assertion.
188
+
189
+ ## Artifacts and verification
190
+
191
+ [All 29 attempts](index.jsonl), [chart](results.html), and
192
+ [source hashes and cleanup manifest](manifest.json). Each index entry identifies
193
+ its raw report and resource trace; a sibling `.log` holds runner output. Raw
194
+ measurement reports are versioned, including failed and superseded generator
195
+ configurations. Detailed resource traces and logs are retained locally and
196
+ excluded from Git; regenerate them with the commands above when needed.
197
+
198
+ Verification: `npm run typecheck`, `git diff --check`, and both
199
+ `node --import tsx --test tests/scale-harness.test.ts` cases passed. The tests
200
+ exercise authenticated scope checks, collector reconnect/replay, SSE reset and
201
+ cleanup with zero and two independent reader workers. Python syntax was checked.
202
+ All recorded temporary hub/database directories were removed, and the installed
203
+ hub returned HTTP 200 on its existing Unix-socket health endpoint afterward.
204
+ The installed hub and collector processes were left running. Only harness,
205
+ supervisor, tests and documentation files changed.