loadscope 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +292 -3
- package/dist/chunk-7L4SENK7.js +269 -0
- package/dist/chunk-7L4SENK7.js.map +1 -0
- package/dist/chunk-OFND2NUO.js +3007 -0
- package/dist/chunk-OFND2NUO.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +361 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +720 -0
- package/dist/index.js +87 -0
- package/dist/index.js.map +1 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.js +199 -0
- package/dist/worker.js.map +1 -0
- package/package.json +67 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LoadScope Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,292 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# ⚡ LoadScope
|
|
4
|
+
|
|
5
|
+
**A high-performance HTTP load tester with automated breaking-point finder, Coordinated Omission correction, CI performance regression gating, and zero-browser PDF/HTML reports.**
|
|
6
|
+
|
|
7
|
+
[](https://github.com/shaikafridd/loadscope/actions)
|
|
8
|
+
[](https://www.npmjs.com/package/loadscope)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
|
+
[](https://nodejs.org/)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 🚀 Why LoadScope?
|
|
18
|
+
|
|
19
|
+
Most modern load testing tools suffer from one of three major engineering shortcomings:
|
|
20
|
+
1. **Coordinated Omission**: Closed concurrency models stall the client when the server stalls, hiding catastrophic queueing delays behind polite client waits.
|
|
21
|
+
2. **Missing Automated Capacity Detection**: Finding maximum server capacity requires guessing parameters and manually running dozens of tests.
|
|
22
|
+
3. **Fragile CI & Heavy Reporting**: Generating visual reports traditionally pulls in 200MB+ Chromium binaries (Puppeteer/Playwright) or flaky remote CDNs.
|
|
23
|
+
|
|
24
|
+
**LoadScope fixes all three.** It provides:
|
|
25
|
+
- **Automated Breaking-Point Finder**: Stepped ramps combined with binary search bisection to discover the exact load where SLAs breach.
|
|
26
|
+
- **Honest Latency Modeling**: Open-model Poisson/Uniform arrival scheduling with Gil Tene Coordinated Omission correction.
|
|
27
|
+
- **CI Performance Regression Gate**: Save reference baselines, enforce directional tolerance budgets (e.g. max 10% p95 increase), and fail CI builds before merging slow code.
|
|
28
|
+
- **Zero-Browser Vector PDF & Interactive HTML Reports**: Lightning-fast (<100ms) vector PDFs via PDFKit and air-gapped, zero-CDN interactive HTML reports with embedded SVG charts.
|
|
29
|
+
- **Constant Memory ($O(1)$)**: Custom 260-bucket logarithmic histogram eliminating garbage collection pauses regardless of test duration or request count.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## ⚡ Quick Start
|
|
34
|
+
|
|
35
|
+
Execute immediately without installation using `npx`:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Standard 10-second benchmark with 50 concurrent connections
|
|
39
|
+
npx loadscope http://localhost:3000/api -c 50 -d 10s
|
|
40
|
+
|
|
41
|
+
# Find maximum breaking-point capacity automatically
|
|
42
|
+
npx loadscope break http://localhost:3000/api --start 10 --max 200 --step 20 --max-p95 250
|
|
43
|
+
|
|
44
|
+
# Run open arrival model with Poisson distribution and Honest Latency
|
|
45
|
+
npx loadscope http://localhost:3000/api --rate 500 --distribution poisson --honest
|
|
46
|
+
|
|
47
|
+
# Generate a publication-grade vector PDF report
|
|
48
|
+
npx loadscope http://localhost:3000/api -c 50 -d 10s --pdf report.pdf
|
|
49
|
+
|
|
50
|
+
# Generate a self-contained interactive HTML report
|
|
51
|
+
npx loadscope http://localhost:3000/api -c 50 -d 10s --html
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 🖥️ Live Terminal Dashboard Experience
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
🚀 LoadScope v0.1.0 • Target: http://localhost:3000/api • Workers: 4
|
|
58
|
+
────────────────────────────────────────────────────────────────────────
|
|
59
|
+
Throughput: 3,480 req/s [████████████████████░░░░░░░░░░] 70%
|
|
60
|
+
Active Conns: 50 / 50 Dispatched: 34,800 reqs
|
|
61
|
+
Latency (p50): 12.4 ms Latency (p95): 38.6 ms (p99: 89.2 ms)
|
|
62
|
+
Error Rate: 0.00% Event Loop Lag: 1.2 ms (Optimal)
|
|
63
|
+
|
|
64
|
+
History (p95): 18ms ─── 22ms ─── 28ms ─── 35ms ─── 38ms ─── 39ms ─── 38ms
|
|
65
|
+
────────────────────────────────────────────────────────────────────────
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 🎯 Automated Breaking-Point Finder Experience
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
🔍 LoadScope Breaking-Point Capacity Detection:
|
|
72
|
+
────────────────────────────────────────────────────────────────────────
|
|
73
|
+
[Step 1] Load: 20 conn │ 3,850 req/s │ p95: 18.2ms │ Errors: 0.0% │ ✅ PASS
|
|
74
|
+
[Step 2] Load: 40 conn │ 7,620 req/s │ p95: 24.5ms │ Errors: 0.0% │ ✅ PASS
|
|
75
|
+
[Step 3] Load: 60 conn │ 11,240 req/s │ p95: 32.1ms │ Errors: 0.0% │ ✅ PASS
|
|
76
|
+
[Step 4] Load: 80 conn │ 14,810 req/s │ p95: 45.8ms │ Errors: 0.0% │ ✅ PASS
|
|
77
|
+
[Step 5] Load: 100 conn │ 16,920 req/s │ p95: 142.0ms │ Errors: 0.0% │ ✅ PASS
|
|
78
|
+
[Step 6] Load: 120 conn │ 17,100 req/s │ p95: 385.2ms │ Errors: 0.2% │ ❌ BREACH
|
|
79
|
+
────────────────────────────────────────────────────────────────────────
|
|
80
|
+
⚡ Binary Search Refinement: Narrowing capacity between 100 and 120...
|
|
81
|
+
[Refine] Load: 110 conn │ 17,250 req/s │ p95: 220.4ms │ Errors: 0.0% │ ✅ PASS
|
|
82
|
+
[Refine] Load: 115 conn │ 17,180 req/s │ p95: 295.1ms │ Errors: 0.0% │ ✅ PASS
|
|
83
|
+
[Refine] Load: 117 conn │ 17,110 req/s │ p95: 324.8ms │ Errors: 0.1% │ ❌ BREACH
|
|
84
|
+
────────────────────────────────────────────────────────────────────────
|
|
85
|
+
🎯 Result: Maximum Safe Capacity is 115 concurrent connections (p95: 295.1ms)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 📊 Feature Comparison Matrix
|
|
91
|
+
|
|
92
|
+
| Feature | LoadScope | autocannon | wrk | k6 |
|
|
93
|
+
| :--- | :---: | :---: | :---: | :---: |
|
|
94
|
+
| **Breaking-Point Finder** | ✅ Auto Stepped + Binary Search | ❌ Manual | ❌ Manual | ⚠️ Scripted |
|
|
95
|
+
| **Coordinated Omission Correction** | ✅ Built-in (Gil Tene) | ❌ Closed only | ❌ Closed only | ⚠️ Custom |
|
|
96
|
+
| **Open Arrival Model (Poisson)** | ✅ Native | ❌ | ❌ | ✅ |
|
|
97
|
+
| **CI Regression Gating & Step Summary**| ✅ Native (Exit 1 + PR Table) | ❌ | ❌ | ⚠️ Cloud/Thresholds |
|
|
98
|
+
| **Zero-Browser Vector PDF Reports** | ✅ Built-in (<100ms) | ❌ | ❌ | ❌ |
|
|
99
|
+
| **Air-Gapped Zero-CDN HTML Reports** | ✅ Built-in (SVG) | ❌ | ❌ | ⚠️ Cloud / Plugin |
|
|
100
|
+
| **Multi-Core Worker Threads** | ✅ Auto Core Scaling | ⚠️ Single thread | ✅ Multi-thread | ✅ Multi-thread |
|
|
101
|
+
| **Constant-Memory Histogram** | ✅ $O(1)$ Log Bucketed | ⚠️ HDR JS | ✅ C Struct | ⚠️ Go Runtime |
|
|
102
|
+
| **AI Performance Diagnostics** | ✅ Native Gemini/OpenAI | ❌ | ❌ | ❌ |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 🏗️ Architecture
|
|
107
|
+
|
|
108
|
+
### 1. Multi-Core Worker Pool Dispatch
|
|
109
|
+
LoadScope scales traffic generation linearly across available physical CPU cores using Node.js Worker Threads with zero shared memory contention.
|
|
110
|
+
|
|
111
|
+
```mermaid
|
|
112
|
+
graph TD
|
|
113
|
+
CLI[LoadScope Orchestrator] -->|WorkerInitData| W1[Worker Thread 1]
|
|
114
|
+
CLI -->|WorkerInitData| W2[Worker Thread 2]
|
|
115
|
+
CLI -->|WorkerInitData| W3[Worker Thread N]
|
|
116
|
+
|
|
117
|
+
W1 -->|Non-blocking undici pool| Target[Target Web Server]
|
|
118
|
+
W2 -->|Non-blocking undici pool| Target
|
|
119
|
+
W3 -->|Non-blocking undici pool| Target
|
|
120
|
+
|
|
121
|
+
W1 -.->|Tick Messages 4Hz| CLI
|
|
122
|
+
W2 -.->|Tick Messages 4Hz| CLI
|
|
123
|
+
W3 -.->|Tick Messages 4Hz| CLI
|
|
124
|
+
|
|
125
|
+
CLI --> UI[Live Terminal Dashboard / HTML / PDF]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 2. Gil Tene Coordinated Omission Correction
|
|
129
|
+
When a server stalls for 1 second, a closed-model load tester stops sending requests, recording a single slow sample while omitting hundreds of virtual users that should have queued. LoadScope backfills virtual requests according to the schedule:
|
|
130
|
+
|
|
131
|
+
```mermaid
|
|
132
|
+
sequenceDiagram
|
|
133
|
+
autonumber
|
|
134
|
+
actor Client as LoadScope Scheduler
|
|
135
|
+
participant Server as Target Service
|
|
136
|
+
|
|
137
|
+
Note over Client,Server: Scheduled Arrival Interval: 10ms (100 req/s)
|
|
138
|
+
Client->>Server: Request 1 (T = 0ms)
|
|
139
|
+
Server-->>Client: Response 1 (T = 10ms) - Latency: 10ms
|
|
140
|
+
|
|
141
|
+
Client->>Server: Request 2 (T = 10ms)
|
|
142
|
+
Note over Server: Server GC Pause or Database Stall (100ms)
|
|
143
|
+
Server-->>Client: Response 2 (T = 110ms) - Service Time: 100ms
|
|
144
|
+
|
|
145
|
+
Note over Client: Closed Model records 1 slow sample (100ms).<br/>LoadScope Honest Correction backfills 9 virtual requests<br/>queued at 20ms, 30ms, ... with true honest latency up to 90ms.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 3. Automated Capacity Detection (Stepped Ramp + Binary Search)
|
|
149
|
+
```mermaid
|
|
150
|
+
graph LR
|
|
151
|
+
A[Start Load: 20] --> B[Step Load: +20]
|
|
152
|
+
B --> C{SLA Breach?}
|
|
153
|
+
C -- No --> B
|
|
154
|
+
C -- Yes (Breach at 120) --> D[Binary Search Refinement]
|
|
155
|
+
D --> E[Test Midpoint: 110]
|
|
156
|
+
E --> F{SLA Breach?}
|
|
157
|
+
F -- No --> G[Test Upper: 115]
|
|
158
|
+
F -- Yes --> H[Test Lower: 105]
|
|
159
|
+
G --> I[Pinpointed Capacity: 118]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 📖 CLI Usage & Command Reference
|
|
165
|
+
|
|
166
|
+
### Standard Load Test
|
|
167
|
+
```bash
|
|
168
|
+
loadscope <url> [options]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
| Option | Flag | Default | Description |
|
|
172
|
+
| :--- | :--- | :---: | :--- |
|
|
173
|
+
| Concurrency | `-c, --concurrency <n>` | `10` | Number of simultaneous HTTP connections |
|
|
174
|
+
| Duration | `-d, --duration <duration>`| `10s` | Test duration (e.g. `10s`, `1m`, `300s`) |
|
|
175
|
+
| Workers | `-W, --workers <n\|auto>` | `1` | Worker threads count (`auto` detects logical cores) |
|
|
176
|
+
| Method | `--method <verb>` | `GET` | HTTP method (GET, POST, PUT, DELETE, etc.) |
|
|
177
|
+
| Headers | `-H, --header <h...>` | - | Custom HTTP headers (`-H "Authorization: Bearer token"`) |
|
|
178
|
+
| Body | `-b, --body <data>` | - | HTTP payload data string |
|
|
179
|
+
| Timeout | `--timeout <duration>` | `10s` | Individual socket timeout |
|
|
180
|
+
| Warmup | `--warmup <duration>` | `0s` | Warmup duration excluded from summary metrics |
|
|
181
|
+
|
|
182
|
+
### Open Arrival Model & Honest Latency
|
|
183
|
+
```bash
|
|
184
|
+
# Uniform open arrival at 400 req/s with Coordinated Omission correction
|
|
185
|
+
loadscope http://localhost:3000/api --rate 400 --honest
|
|
186
|
+
|
|
187
|
+
# Poisson arrival distribution (stochastic modeling) with TTFB tracking
|
|
188
|
+
loadscope http://localhost:3000/api --rate 500 --distribution poisson --ttfb --honest
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Breaking-Point Capacity Detection
|
|
192
|
+
```bash
|
|
193
|
+
loadscope break <url> [options]
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Automatically ramp from 10 to 200 connections, stepping by 10 every 5 seconds.
|
|
198
|
+
# Breaches if p95 latency exceeds 300ms or error rate exceeds 1%.
|
|
199
|
+
loadscope break http://localhost:3000/api \
|
|
200
|
+
--start 10 \
|
|
201
|
+
--max 200 \
|
|
202
|
+
--step 10 \
|
|
203
|
+
--step-duration 5s \
|
|
204
|
+
--max-p95 300 \
|
|
205
|
+
--max-errors 1.0 \
|
|
206
|
+
--tolerance 2
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### CI/CD Regression Gate
|
|
210
|
+
```bash
|
|
211
|
+
# 1. Capture reference performance baseline on main branch
|
|
212
|
+
loadscope http://localhost:3000/api -c 50 -d 15s --save-baseline baseline.json
|
|
213
|
+
|
|
214
|
+
# 2. In PR CI pipeline: evaluate against baseline (fails with exit code 1 if degraded)
|
|
215
|
+
loadscope http://localhost:3000/api -c 50 -d 15s \
|
|
216
|
+
--baseline baseline.json \
|
|
217
|
+
--max-p95-increase 10 \
|
|
218
|
+
--max-rps-drop 5
|
|
219
|
+
|
|
220
|
+
# 3. Offline comparison between two JSON runs
|
|
221
|
+
loadscope compare current-run.json baseline.json --max-p95-increase 10
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Visual & Machine-Readable Exports
|
|
225
|
+
```bash
|
|
226
|
+
# Self-contained interactive HTML report (Zero CDN, air-gapped ready)
|
|
227
|
+
loadscope http://localhost:3000/api -c 50 -d 10s --html report.html
|
|
228
|
+
|
|
229
|
+
# Multi-page vector PDF benchmark report (<100ms render, zero Puppeteer)
|
|
230
|
+
loadscope http://localhost:3000/api -c 50 -d 10s --pdf report.pdf
|
|
231
|
+
|
|
232
|
+
# Vector PDF report with automated AI Performance Diagnostics
|
|
233
|
+
export GEMINI_API_KEY="your-gemini-key"
|
|
234
|
+
loadscope http://localhost:3000/api -c 50 -d 10s --ai-report report-ai.pdf
|
|
235
|
+
|
|
236
|
+
# Machine-readable JSON or CSV for monitoring pipelines
|
|
237
|
+
loadscope http://localhost:3000/api -c 50 -d 10s --json results.json --csv results.csv
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 💻 Programmatic TypeScript SDK
|
|
243
|
+
|
|
244
|
+
LoadScope provides a clean, fully-typed programmatic API for custom Node.js automation:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { run, runBreakingPoint, generatePdfReport, generateHtmlReport } from "loadscope";
|
|
248
|
+
|
|
249
|
+
// 1. Standard Benchmark Run
|
|
250
|
+
const summary = await run({
|
|
251
|
+
url: "https://api.example.com/checkout",
|
|
252
|
+
concurrency: 50,
|
|
253
|
+
duration: 10,
|
|
254
|
+
honest: true,
|
|
255
|
+
workers: "auto",
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
console.log(`Throughput: ${summary.rps} req/s`);
|
|
259
|
+
console.log(`p95 Latency: ${summary.p95} ms`);
|
|
260
|
+
console.log(`Error Rate: ${(summary.errorRate * 100).toFixed(2)}%`);
|
|
261
|
+
|
|
262
|
+
// 2. Generate Interactive HTML & Vector PDF Reports
|
|
263
|
+
await generateHtmlReport(summary, "artifacts/report.html");
|
|
264
|
+
await generatePdfReport(summary, "artifacts/report.pdf");
|
|
265
|
+
|
|
266
|
+
// 3. Automated Breaking-Point Search
|
|
267
|
+
const bpResult = await runBreakingPoint({
|
|
268
|
+
url: "https://api.example.com/checkout",
|
|
269
|
+
start: 10,
|
|
270
|
+
max: 300,
|
|
271
|
+
step: 25,
|
|
272
|
+
maxP95: 250,
|
|
273
|
+
maxErrorRate: 0.01,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (bpResult.breakingPointReached) {
|
|
277
|
+
console.log(`Service saturated at ${bpResult.maxSustainedLoad} connections!`);
|
|
278
|
+
console.log(`Breach reason: ${bpResult.breachReason}`);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 🤝 Contributing
|
|
285
|
+
|
|
286
|
+
We welcome pull requests and issues! Please read our [Contributing Guide](CONTRIBUTING.md) for details on development workflows, test execution, and code style.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 📄 License
|
|
291
|
+
|
|
292
|
+
This project is open-source software licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// src/stats/histogram.ts
|
|
2
|
+
var GROWTH = 1.02;
|
|
3
|
+
var BUCKETS = 1200;
|
|
4
|
+
var Histogram = class {
|
|
5
|
+
/** Fixed 4.8 KB bucket array of counts */
|
|
6
|
+
counts = new Uint32Array(BUCKETS);
|
|
7
|
+
/** Total number of recorded samples */
|
|
8
|
+
total = 0;
|
|
9
|
+
minVal = Infinity;
|
|
10
|
+
maxVal = 0;
|
|
11
|
+
sumUs = 0;
|
|
12
|
+
/**
|
|
13
|
+
* Calculate bucket index from latency in microseconds.
|
|
14
|
+
* O(1) mathematical mapping.
|
|
15
|
+
*/
|
|
16
|
+
idx(us) {
|
|
17
|
+
if (us <= 1) return 0;
|
|
18
|
+
const bucket = Math.floor(Math.log(us) / Math.log(GROWTH));
|
|
19
|
+
return Math.min(BUCKETS - 1, Math.max(0, bucket));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reconstruct microsecond value from bucket index.
|
|
23
|
+
*/
|
|
24
|
+
valFromIdx(i) {
|
|
25
|
+
return Math.pow(GROWTH, i + 1);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Record a latency value in microseconds (us).
|
|
29
|
+
*/
|
|
30
|
+
record(us) {
|
|
31
|
+
const val = Math.max(0, us);
|
|
32
|
+
const i = this.idx(val);
|
|
33
|
+
this.counts[i]++;
|
|
34
|
+
this.total++;
|
|
35
|
+
this.sumUs += val;
|
|
36
|
+
if (val < this.minVal) this.minVal = val;
|
|
37
|
+
if (val > this.maxVal) this.maxVal = val;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Merge another histogram into this one.
|
|
41
|
+
* Merging is simple O(B) vector addition across the fixed bucket array.
|
|
42
|
+
*/
|
|
43
|
+
merge(other) {
|
|
44
|
+
for (let i = 0; i < BUCKETS; i++) {
|
|
45
|
+
this.counts[i] += other.counts[i];
|
|
46
|
+
}
|
|
47
|
+
this.total += other.total;
|
|
48
|
+
this.sumUs += other.sumUs;
|
|
49
|
+
if (other.minVal < this.minVal) this.minVal = other.minVal;
|
|
50
|
+
if (other.maxVal > this.maxVal) this.maxVal = other.maxVal;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Calculate the given percentile (0..100) in microseconds (us).
|
|
54
|
+
*/
|
|
55
|
+
percentile(p) {
|
|
56
|
+
if (this.total === 0) return 0;
|
|
57
|
+
if (p <= 0) return this.min();
|
|
58
|
+
if (p >= 100) return this.max();
|
|
59
|
+
const target = Math.ceil(p / 100 * this.total);
|
|
60
|
+
let seen = 0;
|
|
61
|
+
for (let i = 0; i < BUCKETS; i++) {
|
|
62
|
+
seen += this.counts[i];
|
|
63
|
+
if (seen >= target) {
|
|
64
|
+
const val = this.valFromIdx(i);
|
|
65
|
+
return Math.min(Math.max(val, this.minVal), this.maxVal);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return this.max();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Return percentile in milliseconds (ms).
|
|
72
|
+
*/
|
|
73
|
+
percentileMs(p) {
|
|
74
|
+
return this.percentile(p) / 1e3;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Minimum recorded latency in microseconds (us).
|
|
78
|
+
*/
|
|
79
|
+
min() {
|
|
80
|
+
return this.total === 0 ? 0 : this.minVal;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Minimum recorded latency in milliseconds (ms).
|
|
84
|
+
*/
|
|
85
|
+
minMs() {
|
|
86
|
+
return this.min() / 1e3;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Maximum recorded latency in microseconds (us).
|
|
90
|
+
*/
|
|
91
|
+
max() {
|
|
92
|
+
return this.total === 0 ? 0 : this.maxVal;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Maximum recorded latency in milliseconds (ms).
|
|
96
|
+
*/
|
|
97
|
+
maxMs() {
|
|
98
|
+
return this.max() / 1e3;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Mean (average) latency in microseconds (us).
|
|
102
|
+
*/
|
|
103
|
+
mean() {
|
|
104
|
+
return this.total === 0 ? 0 : this.sumUs / this.total;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Mean (average) latency in milliseconds (ms).
|
|
108
|
+
*/
|
|
109
|
+
meanMs() {
|
|
110
|
+
return this.mean() / 1e3;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Reset all bucket counts and statistics.
|
|
114
|
+
*/
|
|
115
|
+
reset() {
|
|
116
|
+
this.counts.fill(0);
|
|
117
|
+
this.total = 0;
|
|
118
|
+
this.sumUs = 0;
|
|
119
|
+
this.minVal = Infinity;
|
|
120
|
+
this.maxVal = 0;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Export raw snapshot data for inter-thread messaging (worker_threads).
|
|
124
|
+
*/
|
|
125
|
+
toSnapshot() {
|
|
126
|
+
return {
|
|
127
|
+
counts: new Uint32Array(this.counts),
|
|
128
|
+
total: this.total,
|
|
129
|
+
minVal: this.minVal,
|
|
130
|
+
maxVal: this.maxVal,
|
|
131
|
+
sumUs: this.sumUs
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Restore or merge from a worker snapshot.
|
|
136
|
+
*/
|
|
137
|
+
mergeSnapshot(snapshot) {
|
|
138
|
+
for (let i = 0; i < BUCKETS; i++) {
|
|
139
|
+
this.counts[i] += snapshot.counts[i];
|
|
140
|
+
}
|
|
141
|
+
this.total += snapshot.total;
|
|
142
|
+
this.sumUs += snapshot.sumUs;
|
|
143
|
+
if (snapshot.minVal < this.minVal) this.minVal = snapshot.minVal;
|
|
144
|
+
if (snapshot.maxVal > this.maxVal) this.maxVal = snapshot.maxVal;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/stats/co-corrector.ts
|
|
149
|
+
function recordWithCorrection(histogram, latencyUs, expectedIntervalUs) {
|
|
150
|
+
const primaryVal = Math.max(0, latencyUs);
|
|
151
|
+
histogram.record(primaryVal);
|
|
152
|
+
if (expectedIntervalUs <= 0 || primaryVal <= expectedIntervalUs) {
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
let totalSamplesAdded = 1;
|
|
156
|
+
let virtualLatencyUs = primaryVal - expectedIntervalUs;
|
|
157
|
+
while (virtualLatencyUs > 0) {
|
|
158
|
+
histogram.record(virtualLatencyUs);
|
|
159
|
+
totalSamplesAdded++;
|
|
160
|
+
virtualLatencyUs -= expectedIntervalUs;
|
|
161
|
+
}
|
|
162
|
+
return totalSamplesAdded;
|
|
163
|
+
}
|
|
164
|
+
function extractDistribution(h) {
|
|
165
|
+
return {
|
|
166
|
+
p50: h.percentileMs(50),
|
|
167
|
+
p90: h.percentileMs(90),
|
|
168
|
+
p95: h.percentileMs(95),
|
|
169
|
+
p99: h.percentileMs(99),
|
|
170
|
+
min: h.minMs(),
|
|
171
|
+
max: h.maxMs(),
|
|
172
|
+
mean: h.meanMs()
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function computeQueueDelay(response, service) {
|
|
176
|
+
return {
|
|
177
|
+
p50: Math.max(0, response.p50 - service.p50),
|
|
178
|
+
p90: Math.max(0, response.p90 - service.p90),
|
|
179
|
+
p95: Math.max(0, response.p95 - service.p95),
|
|
180
|
+
p99: Math.max(0, response.p99 - service.p99),
|
|
181
|
+
min: Math.max(0, response.min - service.min),
|
|
182
|
+
max: Math.max(0, response.max - service.max),
|
|
183
|
+
mean: Math.max(0, response.mean - service.mean)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/core/scheduler.ts
|
|
188
|
+
var ArrivalScheduler = class {
|
|
189
|
+
rate;
|
|
190
|
+
distribution;
|
|
191
|
+
startNs;
|
|
192
|
+
currentScheduledNs;
|
|
193
|
+
totalDriftNs = 0n;
|
|
194
|
+
sampleCount = 0;
|
|
195
|
+
maxDriftNs = 0n;
|
|
196
|
+
constructor(rate, distribution = "uniform", startNs) {
|
|
197
|
+
this.rate = Math.max(0.1, rate);
|
|
198
|
+
this.distribution = distribution;
|
|
199
|
+
this.startNs = startNs ?? process.hrtime.bigint();
|
|
200
|
+
this.currentScheduledNs = this.startNs;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Generates the next scheduled arrival timestamp (in nanoseconds).
|
|
204
|
+
*/
|
|
205
|
+
nextArrivalNs() {
|
|
206
|
+
if (this.distribution === "poisson") {
|
|
207
|
+
const u = Math.max(1e-10, 1 - Math.random());
|
|
208
|
+
const intervalSec = -Math.log(u) / this.rate;
|
|
209
|
+
const intervalNs = BigInt(Math.max(1, Math.floor(intervalSec * 1e9)));
|
|
210
|
+
this.currentScheduledNs += intervalNs;
|
|
211
|
+
} else {
|
|
212
|
+
const intervalNs = BigInt(Math.max(1, Math.floor(1e9 / this.rate)));
|
|
213
|
+
this.currentScheduledNs += intervalNs;
|
|
214
|
+
}
|
|
215
|
+
return this.currentScheduledNs;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* High-resolution pause until the target timestamp is reached.
|
|
219
|
+
* Uses setTimeout for long waits and micro-spinning for sub-millisecond precision.
|
|
220
|
+
*/
|
|
221
|
+
async waitUntil(targetNs) {
|
|
222
|
+
const now = process.hrtime.bigint();
|
|
223
|
+
if (now >= targetNs) {
|
|
224
|
+
this.recordDrift(now - targetNs);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const diffNs = targetNs - now;
|
|
228
|
+
const diffMs = Number(diffNs) / 1e6;
|
|
229
|
+
if (diffMs > 2) {
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, Math.floor(diffMs - 1)));
|
|
231
|
+
}
|
|
232
|
+
while (process.hrtime.bigint() < targetNs) {
|
|
233
|
+
if (Number(targetNs - process.hrtime.bigint()) > 5e5) {
|
|
234
|
+
await new Promise((r) => setImmediate(r));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const actual = process.hrtime.bigint();
|
|
238
|
+
this.recordDrift(actual > targetNs ? actual - targetNs : 0n);
|
|
239
|
+
}
|
|
240
|
+
recordDrift(driftNs) {
|
|
241
|
+
this.totalDriftNs += driftNs;
|
|
242
|
+
this.sampleCount++;
|
|
243
|
+
if (driftNs > this.maxDriftNs) {
|
|
244
|
+
this.maxDriftNs = driftNs;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Average schedule drift in milliseconds.
|
|
249
|
+
*/
|
|
250
|
+
get averageDriftMs() {
|
|
251
|
+
if (this.sampleCount === 0) return 0;
|
|
252
|
+
return Number(this.totalDriftNs / BigInt(this.sampleCount)) / 1e6;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Maximum schedule drift in milliseconds.
|
|
256
|
+
*/
|
|
257
|
+
get maxDriftMs() {
|
|
258
|
+
return Number(this.maxDriftNs) / 1e6;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export {
|
|
263
|
+
Histogram,
|
|
264
|
+
recordWithCorrection,
|
|
265
|
+
extractDistribution,
|
|
266
|
+
computeQueueDelay,
|
|
267
|
+
ArrivalScheduler
|
|
268
|
+
};
|
|
269
|
+
//# sourceMappingURL=chunk-7L4SENK7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stats/histogram.ts","../src/stats/co-corrector.ts","../src/core/scheduler.ts"],"sourcesContent":["export const GROWTH = 1.02;\r\nexport const BUCKETS = 1200; // 1.02^1200 us ≈ 21,114 seconds (~5.8 hours)\r\n\r\n/**\r\n * Fixed-Memory Log-Bucketed Histogram.\r\n *\r\n * Storing raw latency arrays creates O(N) memory usage and triggers GC pauses.\r\n * This histogram uses a fixed Uint32Array(1200) (~4.8 KB of memory forever).\r\n * Relative error is bounded by (GROWTH - 1) ≈ 2%.\r\n */\r\nexport class Histogram {\r\n /** Fixed 4.8 KB bucket array of counts */\r\n readonly counts = new Uint32Array(BUCKETS);\r\n /** Total number of recorded samples */\r\n total = 0;\r\n\r\n private minVal = Infinity;\r\n private maxVal = 0;\r\n private sumUs = 0;\r\n\r\n /**\r\n * Calculate bucket index from latency in microseconds.\r\n * O(1) mathematical mapping.\r\n */\r\n private idx(us: number): number {\r\n if (us <= 1) return 0;\r\n const bucket = Math.floor(Math.log(us) / Math.log(GROWTH));\r\n return Math.min(BUCKETS - 1, Math.max(0, bucket));\r\n }\r\n\r\n /**\r\n * Reconstruct microsecond value from bucket index.\r\n */\r\n private valFromIdx(i: number): number {\r\n return Math.pow(GROWTH, i + 1);\r\n }\r\n\r\n /**\r\n * Record a latency value in microseconds (us).\r\n */\r\n record(us: number): void {\r\n const val = Math.max(0, us);\r\n const i = this.idx(val);\r\n this.counts[i]++;\r\n this.total++;\r\n this.sumUs += val;\r\n\r\n if (val < this.minVal) this.minVal = val;\r\n if (val > this.maxVal) this.maxVal = val;\r\n }\r\n\r\n /**\r\n * Merge another histogram into this one.\r\n * Merging is simple O(B) vector addition across the fixed bucket array.\r\n */\r\n merge(other: Histogram): void {\r\n for (let i = 0; i < BUCKETS; i++) {\r\n this.counts[i] += other.counts[i];\r\n }\r\n this.total += other.total;\r\n this.sumUs += other.sumUs;\r\n if (other.minVal < this.minVal) this.minVal = other.minVal;\r\n if (other.maxVal > this.maxVal) this.maxVal = other.maxVal;\r\n }\r\n\r\n /**\r\n * Calculate the given percentile (0..100) in microseconds (us).\r\n */\r\n percentile(p: number): number {\r\n if (this.total === 0) return 0;\r\n if (p <= 0) return this.min();\r\n if (p >= 100) return this.max();\r\n\r\n const target = Math.ceil((p / 100) * this.total);\r\n let seen = 0;\r\n\r\n for (let i = 0; i < BUCKETS; i++) {\r\n seen += this.counts[i];\r\n if (seen >= target) {\r\n const val = this.valFromIdx(i);\r\n return Math.min(Math.max(val, this.minVal), this.maxVal);\r\n }\r\n }\r\n\r\n return this.max();\r\n }\r\n\r\n /**\r\n * Return percentile in milliseconds (ms).\r\n */\r\n percentileMs(p: number): number {\r\n return this.percentile(p) / 1000;\r\n }\r\n\r\n /**\r\n * Minimum recorded latency in microseconds (us).\r\n */\r\n min(): number {\r\n return this.total === 0 ? 0 : this.minVal;\r\n }\r\n\r\n /**\r\n * Minimum recorded latency in milliseconds (ms).\r\n */\r\n minMs(): number {\r\n return this.min() / 1000;\r\n }\r\n\r\n /**\r\n * Maximum recorded latency in microseconds (us).\r\n */\r\n max(): number {\r\n return this.total === 0 ? 0 : this.maxVal;\r\n }\r\n\r\n /**\r\n * Maximum recorded latency in milliseconds (ms).\r\n */\r\n maxMs(): number {\r\n return this.max() / 1000;\r\n }\r\n\r\n /**\r\n * Mean (average) latency in microseconds (us).\r\n */\r\n mean(): number {\r\n return this.total === 0 ? 0 : this.sumUs / this.total;\r\n }\r\n\r\n /**\r\n * Mean (average) latency in milliseconds (ms).\r\n */\r\n meanMs(): number {\r\n return this.mean() / 1000;\r\n }\r\n\r\n /**\r\n * Reset all bucket counts and statistics.\r\n */\r\n reset(): void {\r\n this.counts.fill(0);\r\n this.total = 0;\r\n this.sumUs = 0;\r\n this.minVal = Infinity;\r\n this.maxVal = 0;\r\n }\r\n\r\n /**\r\n * Export raw snapshot data for inter-thread messaging (worker_threads).\r\n */\r\n toSnapshot(): { counts: Uint32Array; total: number; minVal: number; maxVal: number; sumUs: number } {\r\n return {\r\n counts: new Uint32Array(this.counts),\r\n total: this.total,\r\n minVal: this.minVal,\r\n maxVal: this.maxVal,\r\n sumUs: this.sumUs,\r\n };\r\n }\r\n\r\n /**\r\n * Restore or merge from a worker snapshot.\r\n */\r\n mergeSnapshot(snapshot: { counts: Uint32Array; total: number; minVal: number; maxVal: number; sumUs: number }): void {\r\n for (let i = 0; i < BUCKETS; i++) {\r\n this.counts[i] += snapshot.counts[i];\r\n }\r\n this.total += snapshot.total;\r\n this.sumUs += snapshot.sumUs;\r\n if (snapshot.minVal < this.minVal) this.minVal = snapshot.minVal;\r\n if (snapshot.maxVal > this.maxVal) this.maxVal = snapshot.maxVal;\r\n }\r\n}\r\n","import { Histogram } from \"./histogram.js\";\r\nimport type { LatencyDistribution } from \"../types.js\";\r\n\r\n/**\r\n * Gil Tene's Coordinated Omission Correction algorithm (from wrk2 / HdrHistogram).\r\n *\r\n * In load testing, when a request stalls or experiences high latency L exceeding\r\n * the expected inter-arrival interval I, all requests that would have arrived\r\n * behind it during that stall are omitted from the sample pool.\r\n *\r\n * This function injects the missing virtual samples (L - I, L - 2I, ...) into\r\n * the histogram, accurately penalizing high percentiles (p95, p99) to reflect\r\n * what real users experienced while the system was stalled.\r\n */\r\nexport function recordWithCorrection(\r\n histogram: Histogram,\r\n latencyUs: number,\r\n expectedIntervalUs: number\r\n): number {\r\n const primaryVal = Math.max(0, latencyUs);\r\n histogram.record(primaryVal);\r\n\r\n if (expectedIntervalUs <= 0 || primaryVal <= expectedIntervalUs) {\r\n return 1;\r\n }\r\n\r\n let totalSamplesAdded = 1;\r\n let virtualLatencyUs = primaryVal - expectedIntervalUs;\r\n\r\n // Synthesize missing requests queued behind the stall\r\n while (virtualLatencyUs > 0) {\r\n histogram.record(virtualLatencyUs);\r\n totalSamplesAdded++;\r\n virtualLatencyUs -= expectedIntervalUs;\r\n }\r\n\r\n return totalSamplesAdded;\r\n}\r\n\r\n/**\r\n * Helper to extract formatted LatencyDistribution metrics from a Histogram.\r\n */\r\nexport function extractDistribution(h: Histogram): LatencyDistribution {\r\n return {\r\n p50: h.percentileMs(50),\r\n p90: h.percentileMs(90),\r\n p95: h.percentileMs(95),\r\n p99: h.percentileMs(99),\r\n min: h.minMs(),\r\n max: h.maxMs(),\r\n mean: h.meanMs(),\r\n };\r\n}\r\n\r\n/**\r\n * Calculates the queue delay distribution by subtracting service time from response time.\r\n */\r\nexport function computeQueueDelay(\r\n response: LatencyDistribution,\r\n service: LatencyDistribution\r\n): LatencyDistribution {\r\n return {\r\n p50: Math.max(0, response.p50 - service.p50),\r\n p90: Math.max(0, response.p90 - service.p90),\r\n p95: Math.max(0, response.p95 - service.p95),\r\n p99: Math.max(0, response.p99 - service.p99),\r\n min: Math.max(0, response.min - service.min),\r\n max: Math.max(0, response.max - service.max),\r\n mean: Math.max(0, response.mean - service.mean),\r\n };\r\n}\r\n","/**\r\n * High-precision arrival scheduler for open-model load testing.\r\n * Supports deterministic uniform intervals and stochastic Poisson arrival processes.\r\n */\r\nexport class ArrivalScheduler {\r\n private readonly rate: number;\r\n private readonly distribution: \"uniform\" | \"poisson\";\r\n private readonly startNs: bigint;\r\n private currentScheduledNs: bigint;\r\n private totalDriftNs = 0n;\r\n private sampleCount = 0;\r\n private maxDriftNs = 0n;\r\n\r\n constructor(rate: number, distribution: \"uniform\" | \"poisson\" = \"uniform\", startNs?: bigint) {\r\n this.rate = Math.max(0.1, rate);\r\n this.distribution = distribution;\r\n this.startNs = startNs ?? process.hrtime.bigint();\r\n this.currentScheduledNs = this.startNs;\r\n }\r\n\r\n /**\r\n * Generates the next scheduled arrival timestamp (in nanoseconds).\r\n */\r\n nextArrivalNs(): bigint {\r\n if (this.distribution === \"poisson\") {\r\n // Poisson process: inter-arrival time is exponentially distributed\r\n // Delta_t = -ln(U) / lambda\r\n const u = Math.max(1e-10, 1 - Math.random());\r\n const intervalSec = -Math.log(u) / this.rate;\r\n const intervalNs = BigInt(Math.max(1, Math.floor(intervalSec * 1_000_000_000)));\r\n this.currentScheduledNs += intervalNs;\r\n } else {\r\n // Uniform fixed arrival interval: Delta_t = 1 / lambda\r\n const intervalNs = BigInt(Math.max(1, Math.floor(1_000_000_000 / this.rate)));\r\n this.currentScheduledNs += intervalNs;\r\n }\r\n\r\n return this.currentScheduledNs;\r\n }\r\n\r\n /**\r\n * High-resolution pause until the target timestamp is reached.\r\n * Uses setTimeout for long waits and micro-spinning for sub-millisecond precision.\r\n */\r\n async waitUntil(targetNs: bigint): Promise<void> {\r\n const now = process.hrtime.bigint();\r\n if (now >= targetNs) {\r\n this.recordDrift(now - targetNs);\r\n return;\r\n }\r\n\r\n const diffNs = targetNs - now;\r\n const diffMs = Number(diffNs) / 1_000_000;\r\n\r\n // Yield event loop for sleeps longer than 2ms\r\n if (diffMs > 2) {\r\n await new Promise((resolve) => setTimeout(resolve, Math.floor(diffMs - 1)));\r\n }\r\n\r\n // Micro-spin for remaining sub-millisecond precision\r\n while (process.hrtime.bigint() < targetNs) {\r\n // Yield slightly if still noticeably ahead\r\n if (Number(targetNs - process.hrtime.bigint()) > 500_000) {\r\n await new Promise((r) => setImmediate(r));\r\n }\r\n }\r\n\r\n const actual = process.hrtime.bigint();\r\n this.recordDrift(actual > targetNs ? actual - targetNs : 0n);\r\n }\r\n\r\n private recordDrift(driftNs: bigint): void {\r\n this.totalDriftNs += driftNs;\r\n this.sampleCount++;\r\n if (driftNs > this.maxDriftNs) {\r\n this.maxDriftNs = driftNs;\r\n }\r\n }\r\n\r\n /**\r\n * Average schedule drift in milliseconds.\r\n */\r\n get averageDriftMs(): number {\r\n if (this.sampleCount === 0) return 0;\r\n return Number(this.totalDriftNs / BigInt(this.sampleCount)) / 1_000_000;\r\n }\r\n\r\n /**\r\n * Maximum schedule drift in milliseconds.\r\n */\r\n get maxDriftMs(): number {\r\n return Number(this.maxDriftNs) / 1_000_000;\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|