sol-trade-sdk 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/README.md +390 -0
- package/dist/chunk-MMQAMIKR.mjs +3735 -0
- package/dist/chunk-NEZDFAYA.mjs +7744 -0
- package/dist/clients-VITWK7B6.mjs +1370 -0
- package/dist/index-1BK_FXsW.d.mts +2327 -0
- package/dist/index-1BK_FXsW.d.ts +2327 -0
- package/dist/index.d.mts +2659 -0
- package/dist/index.d.ts +2659 -0
- package/dist/index.js +13265 -0
- package/dist/index.mjs +562 -0
- package/dist/perf/index.d.mts +2 -0
- package/dist/perf/index.d.ts +2 -0
- package/dist/perf/index.js +3742 -0
- package/dist/perf/index.mjs +214 -0
- package/package.json +101 -0
- package/src/__tests__/complete_sdk.test.ts +354 -0
- package/src/__tests__/hotpath.test.ts +486 -0
- package/src/__tests__/nonce.test.ts +45 -0
- package/src/__tests__/sdk.test.ts +425 -0
- package/src/address-lookup/index.ts +197 -0
- package/src/cache/cache.ts +308 -0
- package/src/calc/index.ts +1058 -0
- package/src/calc/pumpfun.ts +124 -0
- package/src/common/bonding_curve.ts +272 -0
- package/src/common/compute-budget.ts +148 -0
- package/src/common/confirm-any-signature.ts +184 -0
- package/src/common/fast-timing.ts +481 -0
- package/src/common/fast_fn.ts +150 -0
- package/src/common/gas-fee-strategy.ts +253 -0
- package/src/common/map-pool.ts +23 -0
- package/src/common/nonce.ts +40 -0
- package/src/common/sdk-log.ts +460 -0
- package/src/common/seed.ts +381 -0
- package/src/common/spl-token.ts +578 -0
- package/src/common/subscription-handle.ts +644 -0
- package/src/common/trading-utils.ts +239 -0
- package/src/common/wsol-manager.ts +325 -0
- package/src/compute/compute_budget_manager.ts +187 -0
- package/src/compute/index.ts +21 -0
- package/src/constants/index.ts +96 -0
- package/src/execution/execution.ts +532 -0
- package/src/execution/index.ts +42 -0
- package/src/hotpath/executor.ts +464 -0
- package/src/hotpath/index.ts +64 -0
- package/src/hotpath/state.ts +435 -0
- package/src/index.ts +2117 -0
- package/src/instruction/bonk_builder.ts +730 -0
- package/src/instruction/index.ts +24 -0
- package/src/instruction/meteora_damm_v2_builder.ts +509 -0
- package/src/instruction/pumpfun_builder.ts +1183 -0
- package/src/instruction/pumpswap.ts +1123 -0
- package/src/instruction/raydium_amm_v4_builder.ts +692 -0
- package/src/instruction/raydium_cpmm_builder.ts +795 -0
- package/src/middleware/traits.ts +407 -0
- package/src/params/index.ts +483 -0
- package/src/perf/compiler-optimization.ts +529 -0
- package/src/perf/hardware.ts +631 -0
- package/src/perf/index.ts +9 -0
- package/src/perf/kernel-bypass.ts +656 -0
- package/src/perf/protocol.ts +682 -0
- package/src/perf/realtime.ts +592 -0
- package/src/perf/simd.ts +668 -0
- package/src/perf/syscall-bypass.ts +331 -0
- package/src/perf/ultra-low-latency.ts +505 -0
- package/src/perf/zero-copy.ts +589 -0
- package/src/pool/pool.ts +294 -0
- package/src/rpc/client.ts +345 -0
- package/src/sdk-errors.ts +13 -0
- package/src/security/index.ts +26 -0
- package/src/security/secure-key.ts +303 -0
- package/src/security/validators.ts +281 -0
- package/src/seed/pda.ts +262 -0
- package/src/serialization/index.ts +28 -0
- package/src/serialization/serialization.ts +288 -0
- package/src/swqos/clients.ts +1754 -0
- package/src/swqos/index.ts +50 -0
- package/src/swqos/providers.ts +1707 -0
- package/src/trading/core/async-executor.ts +702 -0
- package/src/trading/core/confirmation-monitor.ts +711 -0
- package/src/trading/core/index.ts +82 -0
- package/src/trading/core/retry-handler.ts +683 -0
- package/src/trading/core/transaction-pool.ts +780 -0
- package/src/trading/executor.ts +385 -0
- package/src/trading/factory.ts +282 -0
- package/src/trading/index.ts +30 -0
- package/src/types.ts +8 -0
- package/src/utils/index.ts +155 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll multiple transaction signatures until one reaches confirmed/finalized without error.
|
|
3
|
+
* Rust: `swqos::common::poll_any_transaction_confirmation`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Connection, type Commitment, type Finality } from '@solana/web3.js';
|
|
7
|
+
import { TradeError } from '../sdk-errors';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Map RPC `Commitment` to `getTransaction` `Finality` (web3: only `confirmed` | `finalized`).
|
|
11
|
+
* `processed` / unknown → `confirmed`.
|
|
12
|
+
*/
|
|
13
|
+
export function commitmentToGetTxFinality(c: Commitment | undefined): Finality {
|
|
14
|
+
if (c === 'finalized') return 'finalized';
|
|
15
|
+
return 'confirmed';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ConfirmAnySignatureOptions {
|
|
19
|
+
commitment?: Commitment;
|
|
20
|
+
/**
|
|
21
|
+
* `getTransaction` lookup commitment (Rust: `CommitmentConfig::confirmed()`).
|
|
22
|
+
* Defaults from {@link commitmentToGetTxFinality}(`commitment`).
|
|
23
|
+
*/
|
|
24
|
+
getTransactionCommitment?: Finality;
|
|
25
|
+
/** Default 15000 (Rust uses 15s). */
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
pollIntervalMs?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Rust waits until `poll_count >= 10` before `get_transaction_with_config` on the first landed signature.
|
|
30
|
+
* @default 10
|
|
31
|
+
*/
|
|
32
|
+
pollsBeforeGetTransaction?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Rust: log line patterns for user-facing failure hints. */
|
|
36
|
+
export function extractHintsFromLogs(
|
|
37
|
+
logs: readonly string[] | null | undefined
|
|
38
|
+
): string {
|
|
39
|
+
if (!logs?.length) return '';
|
|
40
|
+
const parts: string[] = [];
|
|
41
|
+
for (const log of logs) {
|
|
42
|
+
let idx = log.indexOf('Error Message: ');
|
|
43
|
+
if (idx !== -1) {
|
|
44
|
+
parts.push(log.slice(idx + 15).replace(/\.$/, '').trim());
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
idx = log.indexOf('Program log: Error: ');
|
|
48
|
+
if (idx !== -1) {
|
|
49
|
+
parts.push(log.slice(idx + 20).replace(/\.$/, '').trim());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return parts.join('; ');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Map `TransactionError` JSON (meta.err) to a numeric code; prefers Custom instruction code like Rust.
|
|
57
|
+
*/
|
|
58
|
+
export function instructionErrorCodeFromMetaErr(err: unknown): {
|
|
59
|
+
code: number;
|
|
60
|
+
instructionIndex?: number;
|
|
61
|
+
} {
|
|
62
|
+
if (err == null) return { code: 0 };
|
|
63
|
+
if (typeof err === 'object' && err !== null) {
|
|
64
|
+
const ie = (err as { InstructionError?: [number, unknown] }).InstructionError;
|
|
65
|
+
if (Array.isArray(ie) && ie.length >= 2) {
|
|
66
|
+
const instructionIndex = ie[0];
|
|
67
|
+
const detail = ie[1];
|
|
68
|
+
if (
|
|
69
|
+
detail &&
|
|
70
|
+
typeof detail === 'object' &&
|
|
71
|
+
detail !== null &&
|
|
72
|
+
'Custom' in detail
|
|
73
|
+
) {
|
|
74
|
+
return {
|
|
75
|
+
code: Number((detail as { Custom: number }).Custom),
|
|
76
|
+
instructionIndex,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return { code: 999, instructionIndex };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { code: 108 };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Wait until any signature in `signatures` is confirmed successfully on-chain.
|
|
87
|
+
* Single-signature path uses `connection.confirmTransaction` (blockhash strategy when applicable).
|
|
88
|
+
*/
|
|
89
|
+
export async function confirmAnyTransactionSignature(
|
|
90
|
+
connection: Connection,
|
|
91
|
+
signatures: string[],
|
|
92
|
+
options?: ConfirmAnySignatureOptions
|
|
93
|
+
): Promise<string> {
|
|
94
|
+
const commitment = options?.commitment ?? 'confirmed';
|
|
95
|
+
const getTxFinality =
|
|
96
|
+
options?.getTransactionCommitment ??
|
|
97
|
+
commitmentToGetTxFinality(commitment);
|
|
98
|
+
const timeoutMs = options?.timeoutMs ?? 15_000;
|
|
99
|
+
const pollIntervalMs = options?.pollIntervalMs ?? 1000;
|
|
100
|
+
const pollsBeforeTx =
|
|
101
|
+
options?.pollsBeforeGetTransaction ?? 10;
|
|
102
|
+
|
|
103
|
+
const unique = [...new Set(signatures.filter(Boolean))];
|
|
104
|
+
if (unique.length === 0) {
|
|
105
|
+
throw new TradeError(106, 'No signatures to confirm');
|
|
106
|
+
}
|
|
107
|
+
if (unique.length === 1) {
|
|
108
|
+
const sig = unique[0]!;
|
|
109
|
+
await connection.confirmTransaction(sig, commitment);
|
|
110
|
+
return sig;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const start = Date.now();
|
|
114
|
+
let pollCount = 0;
|
|
115
|
+
|
|
116
|
+
while (Date.now() - start < timeoutMs) {
|
|
117
|
+
pollCount += 1;
|
|
118
|
+
const { value } = await connection.getSignatureStatuses(unique, {
|
|
119
|
+
searchTransactionHistory: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < unique.length; i++) {
|
|
123
|
+
const st = value[i];
|
|
124
|
+
if (!st) continue;
|
|
125
|
+
const c = st.confirmationStatus;
|
|
126
|
+
if (
|
|
127
|
+
st.err == null &&
|
|
128
|
+
(c === 'confirmed' || c === 'finalized')
|
|
129
|
+
) {
|
|
130
|
+
return unique[i]!;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let landedIndex: number | undefined;
|
|
135
|
+
for (let i = 0; i < unique.length; i++) {
|
|
136
|
+
if (value[i] != null) {
|
|
137
|
+
landedIndex = i;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (landedIndex === undefined) {
|
|
143
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (pollCount < pollsBeforeTx) {
|
|
148
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const landedSig = unique[landedIndex]!;
|
|
153
|
+
let tx: Awaited<ReturnType<Connection['getTransaction']>>;
|
|
154
|
+
try {
|
|
155
|
+
tx = await connection.getTransaction(landedSig, {
|
|
156
|
+
commitment: getTxFinality,
|
|
157
|
+
maxSupportedTransactionVersion: 0,
|
|
158
|
+
});
|
|
159
|
+
} catch {
|
|
160
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (tx == null || tx.meta == null) {
|
|
165
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (tx.meta.err == null) {
|
|
170
|
+
return landedSig;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const hints = extractHintsFromLogs(tx.meta.logMessages ?? undefined);
|
|
174
|
+
const parsed = instructionErrorCodeFromMetaErr(tx.meta.err);
|
|
175
|
+
const base = JSON.stringify(tx.meta.err);
|
|
176
|
+
const msg = hints ? `${base} ${hints}` : base;
|
|
177
|
+
throw new TradeError(parsed.code || 108, `${msg} (${landedSig})`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
throw new TradeError(
|
|
181
|
+
107,
|
|
182
|
+
`Transaction confirmation timed out after ${timeoutMs}ms (${unique.length} signatures polled)`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast timing utilities for Sol Trade SDK
|
|
3
|
+
* Provides high-precision timing functions and latency measurement tools.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PerformanceObserver } from 'perf_hooks';
|
|
7
|
+
import type { PerformanceEntry } from 'perf_hooks';
|
|
8
|
+
|
|
9
|
+
// ===== Time Unit Conversion Functions =====
|
|
10
|
+
|
|
11
|
+
const NS_PER_MS = BigInt(1_000_000);
|
|
12
|
+
const NS_PER_US = BigInt(1_000);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get current timestamp in nanoseconds
|
|
16
|
+
* Uses process.hrtime.bigint() in Node.js, falls back to Date.now() * 1_000_000
|
|
17
|
+
*/
|
|
18
|
+
export function nowNs(): bigint {
|
|
19
|
+
if (typeof process !== 'undefined' && process.hrtime) {
|
|
20
|
+
return process.hrtime.bigint();
|
|
21
|
+
}
|
|
22
|
+
return BigInt(Date.now()) * NS_PER_MS;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get current timestamp in microseconds
|
|
27
|
+
*/
|
|
28
|
+
export function nowUs(): bigint {
|
|
29
|
+
if (typeof process !== 'undefined' && process.hrtime) {
|
|
30
|
+
return process.hrtime.bigint() / NS_PER_US;
|
|
31
|
+
}
|
|
32
|
+
return BigInt(Date.now()) * BigInt(1_000);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get current timestamp in milliseconds
|
|
37
|
+
*/
|
|
38
|
+
export function nowMs(): number {
|
|
39
|
+
return Date.now();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ===== Timer Class =====
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* High-precision timer for measuring elapsed time
|
|
46
|
+
*/
|
|
47
|
+
export class Timer {
|
|
48
|
+
private startTime: bigint;
|
|
49
|
+
private endTime?: bigint;
|
|
50
|
+
private running: boolean = true;
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
this.startTime = nowNs();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Start or restart the timer
|
|
58
|
+
*/
|
|
59
|
+
start(): void {
|
|
60
|
+
this.startTime = nowNs();
|
|
61
|
+
this.running = true;
|
|
62
|
+
this.endTime = undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Stop the timer and return elapsed time in nanoseconds
|
|
67
|
+
*/
|
|
68
|
+
stop(): bigint {
|
|
69
|
+
if (this.running) {
|
|
70
|
+
this.endTime = nowNs();
|
|
71
|
+
this.running = false;
|
|
72
|
+
}
|
|
73
|
+
return this.elapsedNs();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get elapsed time in nanoseconds without stopping
|
|
78
|
+
*/
|
|
79
|
+
elapsedNs(): bigint {
|
|
80
|
+
const end = this.running ? nowNs() : this.endTime!;
|
|
81
|
+
return end - this.startTime;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get elapsed time in microseconds without stopping
|
|
86
|
+
*/
|
|
87
|
+
elapsedUs(): bigint {
|
|
88
|
+
return this.elapsedNs() / NS_PER_US;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get elapsed time in milliseconds without stopping
|
|
93
|
+
*/
|
|
94
|
+
elapsedMs(): number {
|
|
95
|
+
return Number(this.elapsedNs()) / 1_000_000;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if timer is still running
|
|
100
|
+
*/
|
|
101
|
+
isRunning(): boolean {
|
|
102
|
+
return this.running;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Reset the timer
|
|
107
|
+
*/
|
|
108
|
+
reset(): void {
|
|
109
|
+
this.startTime = nowNs();
|
|
110
|
+
this.endTime = undefined;
|
|
111
|
+
this.running = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ===== Timing Context =====
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Timing context for tracking multiple timing points
|
|
119
|
+
*/
|
|
120
|
+
export interface TimingPoint {
|
|
121
|
+
name: string;
|
|
122
|
+
timestamp: bigint;
|
|
123
|
+
elapsedFromStart: bigint;
|
|
124
|
+
elapsedFromPrevious: bigint;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Context for collecting timing measurements
|
|
129
|
+
*/
|
|
130
|
+
export class TimingContext {
|
|
131
|
+
private points: TimingPoint[] = [];
|
|
132
|
+
private startTime: bigint;
|
|
133
|
+
private lastTime: bigint;
|
|
134
|
+
private name: string;
|
|
135
|
+
|
|
136
|
+
constructor(name: string = 'default') {
|
|
137
|
+
this.name = name;
|
|
138
|
+
this.startTime = nowNs();
|
|
139
|
+
this.lastTime = this.startTime;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Mark a timing point
|
|
144
|
+
*/
|
|
145
|
+
mark(pointName: string): void {
|
|
146
|
+
const now = nowNs();
|
|
147
|
+
this.points.push({
|
|
148
|
+
name: pointName,
|
|
149
|
+
timestamp: now,
|
|
150
|
+
elapsedFromStart: now - this.startTime,
|
|
151
|
+
elapsedFromPrevious: now - this.lastTime,
|
|
152
|
+
});
|
|
153
|
+
this.lastTime = now;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get all timing points
|
|
158
|
+
*/
|
|
159
|
+
getPoints(): TimingPoint[] {
|
|
160
|
+
return [...this.points];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get total elapsed time in nanoseconds
|
|
165
|
+
*/
|
|
166
|
+
totalElapsedNs(): bigint {
|
|
167
|
+
return nowNs() - this.startTime;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get total elapsed time in microseconds
|
|
172
|
+
*/
|
|
173
|
+
totalElapsedUs(): bigint {
|
|
174
|
+
return this.totalElapsedNs() / NS_PER_US;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get total elapsed time in milliseconds
|
|
179
|
+
*/
|
|
180
|
+
totalElapsedMs(): number {
|
|
181
|
+
return Number(this.totalElapsedNs()) / 1_000_000;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get timing report as a formatted string
|
|
186
|
+
*/
|
|
187
|
+
getReport(): string {
|
|
188
|
+
const lines: string[] = [`Timing Report: ${this.name}`];
|
|
189
|
+
lines.push('='.repeat(50));
|
|
190
|
+
|
|
191
|
+
for (const point of this.points) {
|
|
192
|
+
lines.push(
|
|
193
|
+
`${point.name.padEnd(30)}: ${this.formatNs(point.elapsedFromStart)} (Δ ${this.formatNs(point.elapsedFromPrevious)})`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
lines.push('-'.repeat(50));
|
|
198
|
+
lines.push(`Total: ${this.formatNs(this.totalElapsedNs())}`);
|
|
199
|
+
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Reset the context
|
|
205
|
+
*/
|
|
206
|
+
reset(): void {
|
|
207
|
+
this.points = [];
|
|
208
|
+
this.startTime = nowNs();
|
|
209
|
+
this.lastTime = this.startTime;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Format nanoseconds to human-readable string
|
|
214
|
+
*/
|
|
215
|
+
private formatNs(ns: bigint): string {
|
|
216
|
+
if (ns >= NS_PER_MS) {
|
|
217
|
+
return `${(Number(ns) / 1_000_000).toFixed(2)}ms`;
|
|
218
|
+
}
|
|
219
|
+
if (ns >= NS_PER_US) {
|
|
220
|
+
return `${(Number(ns) / 1_000).toFixed(2)}µs`;
|
|
221
|
+
}
|
|
222
|
+
return `${ns}ns`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ===== Latency Histogram =====
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Bucket boundaries for latency histogram (in microseconds)
|
|
230
|
+
*/
|
|
231
|
+
const DEFAULT_BUCKETS = [
|
|
232
|
+
10, // 10µs
|
|
233
|
+
50, // 50µs
|
|
234
|
+
100, // 100µs
|
|
235
|
+
250, // 250µs
|
|
236
|
+
500, // 500µs
|
|
237
|
+
1000, // 1ms
|
|
238
|
+
2500, // 2.5ms
|
|
239
|
+
5000, // 5ms
|
|
240
|
+
10000, // 10ms
|
|
241
|
+
25000, // 25ms
|
|
242
|
+
50000, // 50ms
|
|
243
|
+
100000, // 100ms
|
|
244
|
+
250000, // 250ms
|
|
245
|
+
500000, // 500ms
|
|
246
|
+
1000000, // 1s
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Histogram entry
|
|
251
|
+
*/
|
|
252
|
+
export interface HistogramEntry {
|
|
253
|
+
bucket: number;
|
|
254
|
+
count: number;
|
|
255
|
+
cumulativeCount: number;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Latency histogram for tracking operation latencies
|
|
260
|
+
*/
|
|
261
|
+
export class LatencyHistogram {
|
|
262
|
+
private buckets: number[];
|
|
263
|
+
private counts: number[];
|
|
264
|
+
private totalCount: number = 0;
|
|
265
|
+
private sum: bigint = BigInt(0);
|
|
266
|
+
private min: bigint = BigInt(Number.MAX_SAFE_INTEGER);
|
|
267
|
+
private max: bigint = BigInt(0);
|
|
268
|
+
|
|
269
|
+
constructor(buckets: number[] = DEFAULT_BUCKETS) {
|
|
270
|
+
this.buckets = [...buckets].sort((a, b) => a - b);
|
|
271
|
+
this.counts = new Array(this.buckets.length + 1).fill(0);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Record a latency measurement in microseconds
|
|
276
|
+
*/
|
|
277
|
+
record(latencyUs: number | bigint): void {
|
|
278
|
+
const latency = typeof latencyUs === 'bigint' ? latencyUs : BigInt(latencyUs);
|
|
279
|
+
|
|
280
|
+
this.totalCount++;
|
|
281
|
+
this.sum += latency;
|
|
282
|
+
|
|
283
|
+
if (latency < this.min) {
|
|
284
|
+
this.min = latency;
|
|
285
|
+
}
|
|
286
|
+
if (latency > this.max) {
|
|
287
|
+
this.max = latency;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Find bucket
|
|
291
|
+
const bucketIndex = this.buckets.findIndex(b => latency <= BigInt(b));
|
|
292
|
+
const index = bucketIndex === -1 ? this.buckets.length : bucketIndex;
|
|
293
|
+
if (index >= 0 && index < this.counts.length) {
|
|
294
|
+
this.counts[index]!++;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Record from a Timer instance
|
|
300
|
+
*/
|
|
301
|
+
recordTimer(timer: Timer): void {
|
|
302
|
+
this.record(timer.elapsedUs());
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get histogram entries with cumulative counts
|
|
307
|
+
*/
|
|
308
|
+
getHistogram(): HistogramEntry[] {
|
|
309
|
+
let cumulative = 0;
|
|
310
|
+
const entries: HistogramEntry[] = [];
|
|
311
|
+
|
|
312
|
+
for (let i = 0; i < this.buckets.length; i++) {
|
|
313
|
+
const count = this.counts[i] ?? 0;
|
|
314
|
+
cumulative += count;
|
|
315
|
+
entries.push({
|
|
316
|
+
bucket: this.buckets[i] ?? 0,
|
|
317
|
+
count: count,
|
|
318
|
+
cumulativeCount: cumulative,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Add overflow bucket
|
|
323
|
+
const overflowCount = this.counts[this.buckets.length] ?? 0;
|
|
324
|
+
cumulative += overflowCount;
|
|
325
|
+
entries.push({
|
|
326
|
+
bucket: Infinity,
|
|
327
|
+
count: overflowCount,
|
|
328
|
+
cumulativeCount: cumulative,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return entries;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get percentile latency in microseconds
|
|
336
|
+
*/
|
|
337
|
+
getPercentile(percentile: number): number {
|
|
338
|
+
if (this.totalCount === 0) {
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const targetCount = Math.ceil((percentile / 100) * this.totalCount);
|
|
343
|
+
let cumulative = 0;
|
|
344
|
+
|
|
345
|
+
for (let i = 0; i < this.buckets.length; i++) {
|
|
346
|
+
const count = this.counts[i] ?? 0;
|
|
347
|
+
cumulative += count;
|
|
348
|
+
if (cumulative >= targetCount) {
|
|
349
|
+
return this.buckets[i] ?? 0;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return Infinity;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get statistics
|
|
358
|
+
*/
|
|
359
|
+
getStats(): {
|
|
360
|
+
count: number;
|
|
361
|
+
min: number;
|
|
362
|
+
max: number;
|
|
363
|
+
mean: number;
|
|
364
|
+
p50: number;
|
|
365
|
+
p90: number;
|
|
366
|
+
p95: number;
|
|
367
|
+
p99: number;
|
|
368
|
+
} {
|
|
369
|
+
return {
|
|
370
|
+
count: this.totalCount,
|
|
371
|
+
min: this.totalCount > 0 ? Number(this.min) : 0,
|
|
372
|
+
max: this.totalCount > 0 ? Number(this.max) : 0,
|
|
373
|
+
mean: this.totalCount > 0 ? Number(this.sum) / this.totalCount : 0,
|
|
374
|
+
p50: this.getPercentile(50),
|
|
375
|
+
p90: this.getPercentile(90),
|
|
376
|
+
p95: this.getPercentile(95),
|
|
377
|
+
p99: this.getPercentile(99),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Reset the histogram
|
|
383
|
+
*/
|
|
384
|
+
reset(): void {
|
|
385
|
+
this.counts = new Array(this.buckets.length + 1).fill(0);
|
|
386
|
+
this.totalCount = 0;
|
|
387
|
+
this.sum = BigInt(0);
|
|
388
|
+
this.min = BigInt(Number.MAX_SAFE_INTEGER);
|
|
389
|
+
this.max = BigInt(0);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get a formatted report
|
|
394
|
+
*/
|
|
395
|
+
getReport(): string {
|
|
396
|
+
const stats = this.getStats();
|
|
397
|
+
const lines: string[] = [
|
|
398
|
+
'Latency Histogram Report',
|
|
399
|
+
'='.repeat(50),
|
|
400
|
+
`Count: ${stats.count}`,
|
|
401
|
+
`Min: ${this.formatUs(stats.min)}`,
|
|
402
|
+
`Max: ${this.formatUs(stats.max)}`,
|
|
403
|
+
`Mean: ${this.formatUs(stats.mean)}`,
|
|
404
|
+
`P50: ${this.formatUs(stats.p50)}`,
|
|
405
|
+
`P90: ${this.formatUs(stats.p90)}`,
|
|
406
|
+
`P95: ${this.formatUs(stats.p95)}`,
|
|
407
|
+
`P99: ${this.formatUs(stats.p99)}`,
|
|
408
|
+
'-'.repeat(50),
|
|
409
|
+
'Histogram:',
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
const histogram = this.getHistogram();
|
|
413
|
+
for (const entry of histogram) {
|
|
414
|
+
const percentage = ((entry.cumulativeCount / this.totalCount) * 100).toFixed(1);
|
|
415
|
+
const bucketLabel = entry.bucket === Infinity ? '+Inf' : `≤${this.formatUs(entry.bucket)}`;
|
|
416
|
+
lines.push(`${bucketLabel.padEnd(12)}: ${entry.count.toString().padStart(6)} (${percentage}%)`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return lines.join('\n');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private formatUs(us: number): string {
|
|
423
|
+
if (us >= 1000) {
|
|
424
|
+
return `${(us / 1000).toFixed(2)}ms`;
|
|
425
|
+
}
|
|
426
|
+
return `${Math.round(us)}µs`;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ===== Convenience Functions =====
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Time a function execution and return result with timing
|
|
434
|
+
*/
|
|
435
|
+
export async function timeAsync<T>(
|
|
436
|
+
fn: () => Promise<T>,
|
|
437
|
+
_name: string = 'operation'
|
|
438
|
+
): Promise<{ result: T; elapsedUs: bigint; elapsedMs: number }> {
|
|
439
|
+
const timer = new Timer();
|
|
440
|
+
const result = await fn();
|
|
441
|
+
const elapsedUs = timer.elapsedUs();
|
|
442
|
+
return {
|
|
443
|
+
result,
|
|
444
|
+
elapsedUs,
|
|
445
|
+
elapsedMs: Number(elapsedUs) / 1000,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Time a synchronous function execution
|
|
451
|
+
*/
|
|
452
|
+
export function timeSync<T>(
|
|
453
|
+
fn: () => T,
|
|
454
|
+
_name: string = 'operation'
|
|
455
|
+
): { result: T; elapsedUs: bigint; elapsedMs: number } {
|
|
456
|
+
const timer = new Timer();
|
|
457
|
+
const result = fn();
|
|
458
|
+
const elapsedUs = timer.elapsedUs();
|
|
459
|
+
return {
|
|
460
|
+
result,
|
|
461
|
+
elapsedUs,
|
|
462
|
+
elapsedMs: Number(elapsedUs) / 1000,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Create a performance observer for monitoring
|
|
468
|
+
*/
|
|
469
|
+
export function createPerformanceObserver(
|
|
470
|
+
callback: (entry: PerformanceEntry) => void
|
|
471
|
+
): PerformanceObserver | null {
|
|
472
|
+
if (typeof PerformanceObserver !== 'undefined') {
|
|
473
|
+
const observer = new PerformanceObserver((list) => {
|
|
474
|
+
for (const entry of list.getEntries()) {
|
|
475
|
+
callback(entry);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
return observer;
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|