Skip to content

Commit 0aff4c2

Browse files
perf: LatencyHistogram → @stackbilt/wasm-core adapter (closes #96)
Thin adapter over @stackbilt/wasm-core@0.1.0. Rust impl uses sorted Vec with binary search (O(log N) insert, O(1) percentile) and circular eviction (O(1)) instead of Array.shift + sort-on-read. Exact nearest-rank semantics preserved; all 487 tests pass. Vitest config updated to enable --experimental-wasm-modules in the fork pool so WASM loads in Node. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7dda997 commit 0aff4c2

5 files changed

Lines changed: 37 additions & 65 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). Versions use [Se
55

66
## [Unreleased]
77

8+
### Performance
9+
- **`LatencyHistogram` backed by WASM (#96)**`LatencyHistogram` is now a thin adapter over `@stackbilt/wasm-core` (Phase 0). The Rust implementation uses a sorted `Vec` with binary search insertion (O(log N) insert, O(1) percentile read) and a circular eviction index (O(1)) in place of `Array.shift()`. No public API change; existing tests pass unchanged. Requires `--experimental-wasm-modules` in Node.js test environments (added to `vitest.config.ts`).
10+
811
## [1.19.0] — 2026-06-23
912

1013
### Added

package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,8 @@
5757
"publishConfig": {
5858
"access": "public",
5959
"provenance": false
60+
},
61+
"dependencies": {
62+
"@stackbilt/wasm-core": "^0.1.0"
6063
}
6164
}

src/utils/latency-histogram.ts

Lines changed: 16 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,4 @@
1-
/**
2-
* Latency Histogram
3-
*
4-
* Per-provider latency tracking with percentile computation.
5-
* Uses a sorted insertion ring buffer to keep memory bounded
6-
* while supporting efficient percentile queries.
7-
*/
8-
9-
/** Maximum samples per provider before oldest are evicted. */
10-
const DEFAULT_MAX_SAMPLES = 1000;
1+
import { LatencyHistogram as WasmLatencyHistogram } from '@stackbilt/wasm-core';
112

123
export interface LatencySummary {
134
p50: number;
@@ -20,83 +11,43 @@ export interface LatencySummary {
2011
}
2112

2213
export class LatencyHistogram {
23-
private buffers: Map<string, number[]> = new Map();
24-
private maxSamples: number;
14+
private inner: WasmLatencyHistogram;
15+
private providers: Set<string> = new Set();
2516

26-
constructor(maxSamples: number = DEFAULT_MAX_SAMPLES) {
27-
this.maxSamples = maxSamples;
17+
constructor(maxSamples: number = 1000) {
18+
this.inner = new WasmLatencyHistogram(maxSamples);
2819
}
2920

30-
/** Record a latency measurement for a provider (in milliseconds). */
3121
record(provider: string, latencyMs: number): void {
32-
let buffer = this.buffers.get(provider);
33-
if (!buffer) {
34-
buffer = [];
35-
this.buffers.set(provider, buffer);
36-
}
37-
38-
buffer.push(latencyMs);
39-
40-
// Evict oldest when buffer is full.
41-
if (buffer.length > this.maxSamples) {
42-
buffer.shift();
43-
}
22+
this.providers.add(provider);
23+
this.inner.record(provider, latencyMs);
4424
}
4525

46-
/**
47-
* Compute a specific percentile for a provider.
48-
* @param p Percentile as a number between 0 and 100 (e.g. 95 for p95).
49-
* Returns 0 if no data.
50-
*/
5126
percentile(provider: string, p: number): number {
52-
const buffer = this.buffers.get(provider);
53-
if (!buffer || buffer.length === 0) return 0;
54-
55-
const sorted = [...buffer].sort((a, b) => a - b);
56-
const index = Math.ceil((p / 100) * sorted.length) - 1;
57-
return sorted[Math.max(0, index)]!;
27+
return this.inner.percentile(provider, p);
5828
}
5929

60-
/** Get a full summary for a provider. */
6130
summary(provider: string): LatencySummary {
62-
const buffer = this.buffers.get(provider);
63-
if (!buffer || buffer.length === 0) {
64-
return { p50: 0, p95: 0, p99: 0, min: 0, max: 0, mean: 0, count: 0 };
65-
}
66-
67-
const sorted = [...buffer].sort((a, b) => a - b);
68-
const count = sorted.length;
69-
const sum = sorted.reduce((a, b) => a + b, 0);
70-
71-
return {
72-
p50: sorted[Math.max(0, Math.ceil(0.50 * count) - 1)]!,
73-
p95: sorted[Math.max(0, Math.ceil(0.95 * count) - 1)]!,
74-
p99: sorted[Math.max(0, Math.ceil(0.99 * count) - 1)]!,
75-
min: sorted[0]!,
76-
max: sorted[count - 1]!,
77-
mean: sum / count,
78-
count,
79-
};
31+
return this.inner.summary(provider) as LatencySummary;
8032
}
8133

82-
/** Get summaries for all tracked providers. */
8334
allSummaries(): Record<string, LatencySummary> {
8435
const result: Record<string, LatencySummary> = {};
85-
for (const provider of this.buffers.keys()) {
86-
result[provider] = this.summary(provider);
36+
for (const provider of this.providers) {
37+
result[provider] = this.inner.summary(provider) as LatencySummary;
8738
}
8839
return result;
8940
}
9041

91-
/** Reset one or all providers. */
9242
reset(provider?: string): void {
93-
if (provider) {
94-
this.buffers.delete(provider);
43+
if (provider !== undefined) {
44+
this.providers.delete(provider);
45+
this.inner.reset(provider);
9546
} else {
96-
this.buffers.clear();
47+
this.providers.clear();
48+
this.inner.reset();
9749
}
9850
}
9951
}
10052

101-
/** Shared singleton. */
10253
export const defaultLatencyHistogram = new LatencyHistogram();

vitest.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ export default defineConfig({
77
environment: 'node',
88
include: ['src/**/*.test.ts', 'src/**/__tests__/**/*.ts'],
99
exclude: ['node_modules', 'dist'],
10+
pool: 'forks',
11+
poolOptions: {
12+
forks: {
13+
execArgv: ['--experimental-wasm-modules'],
14+
},
15+
},
1016
coverage: {
1117
provider: 'v8',
1218
reporter: ['text', 'html', 'json'],

0 commit comments

Comments
 (0)