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 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
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ [![CI](https://github.com/shaikafridd/loadscope/actions/workflows/ci.yml/badge.svg)](https://github.com/shaikafridd/loadscope/actions)
8
+ [![npm version](https://img.shields.io/badge/npm-v0.1.0-blue.svg)](https://www.npmjs.com/package/loadscope)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.4-blue.svg)](https://www.typescriptlang.org/)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](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":[]}