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.
Files changed (87) hide show
  1. package/README.md +390 -0
  2. package/dist/chunk-MMQAMIKR.mjs +3735 -0
  3. package/dist/chunk-NEZDFAYA.mjs +7744 -0
  4. package/dist/clients-VITWK7B6.mjs +1370 -0
  5. package/dist/index-1BK_FXsW.d.mts +2327 -0
  6. package/dist/index-1BK_FXsW.d.ts +2327 -0
  7. package/dist/index.d.mts +2659 -0
  8. package/dist/index.d.ts +2659 -0
  9. package/dist/index.js +13265 -0
  10. package/dist/index.mjs +562 -0
  11. package/dist/perf/index.d.mts +2 -0
  12. package/dist/perf/index.d.ts +2 -0
  13. package/dist/perf/index.js +3742 -0
  14. package/dist/perf/index.mjs +214 -0
  15. package/package.json +101 -0
  16. package/src/__tests__/complete_sdk.test.ts +354 -0
  17. package/src/__tests__/hotpath.test.ts +486 -0
  18. package/src/__tests__/nonce.test.ts +45 -0
  19. package/src/__tests__/sdk.test.ts +425 -0
  20. package/src/address-lookup/index.ts +197 -0
  21. package/src/cache/cache.ts +308 -0
  22. package/src/calc/index.ts +1058 -0
  23. package/src/calc/pumpfun.ts +124 -0
  24. package/src/common/bonding_curve.ts +272 -0
  25. package/src/common/compute-budget.ts +148 -0
  26. package/src/common/confirm-any-signature.ts +184 -0
  27. package/src/common/fast-timing.ts +481 -0
  28. package/src/common/fast_fn.ts +150 -0
  29. package/src/common/gas-fee-strategy.ts +253 -0
  30. package/src/common/map-pool.ts +23 -0
  31. package/src/common/nonce.ts +40 -0
  32. package/src/common/sdk-log.ts +460 -0
  33. package/src/common/seed.ts +381 -0
  34. package/src/common/spl-token.ts +578 -0
  35. package/src/common/subscription-handle.ts +644 -0
  36. package/src/common/trading-utils.ts +239 -0
  37. package/src/common/wsol-manager.ts +325 -0
  38. package/src/compute/compute_budget_manager.ts +187 -0
  39. package/src/compute/index.ts +21 -0
  40. package/src/constants/index.ts +96 -0
  41. package/src/execution/execution.ts +532 -0
  42. package/src/execution/index.ts +42 -0
  43. package/src/hotpath/executor.ts +464 -0
  44. package/src/hotpath/index.ts +64 -0
  45. package/src/hotpath/state.ts +435 -0
  46. package/src/index.ts +2117 -0
  47. package/src/instruction/bonk_builder.ts +730 -0
  48. package/src/instruction/index.ts +24 -0
  49. package/src/instruction/meteora_damm_v2_builder.ts +509 -0
  50. package/src/instruction/pumpfun_builder.ts +1183 -0
  51. package/src/instruction/pumpswap.ts +1123 -0
  52. package/src/instruction/raydium_amm_v4_builder.ts +692 -0
  53. package/src/instruction/raydium_cpmm_builder.ts +795 -0
  54. package/src/middleware/traits.ts +407 -0
  55. package/src/params/index.ts +483 -0
  56. package/src/perf/compiler-optimization.ts +529 -0
  57. package/src/perf/hardware.ts +631 -0
  58. package/src/perf/index.ts +9 -0
  59. package/src/perf/kernel-bypass.ts +656 -0
  60. package/src/perf/protocol.ts +682 -0
  61. package/src/perf/realtime.ts +592 -0
  62. package/src/perf/simd.ts +668 -0
  63. package/src/perf/syscall-bypass.ts +331 -0
  64. package/src/perf/ultra-low-latency.ts +505 -0
  65. package/src/perf/zero-copy.ts +589 -0
  66. package/src/pool/pool.ts +294 -0
  67. package/src/rpc/client.ts +345 -0
  68. package/src/sdk-errors.ts +13 -0
  69. package/src/security/index.ts +26 -0
  70. package/src/security/secure-key.ts +303 -0
  71. package/src/security/validators.ts +281 -0
  72. package/src/seed/pda.ts +262 -0
  73. package/src/serialization/index.ts +28 -0
  74. package/src/serialization/serialization.ts +288 -0
  75. package/src/swqos/clients.ts +1754 -0
  76. package/src/swqos/index.ts +50 -0
  77. package/src/swqos/providers.ts +1707 -0
  78. package/src/trading/core/async-executor.ts +702 -0
  79. package/src/trading/core/confirmation-monitor.ts +711 -0
  80. package/src/trading/core/index.ts +82 -0
  81. package/src/trading/core/retry-handler.ts +683 -0
  82. package/src/trading/core/transaction-pool.ts +780 -0
  83. package/src/trading/executor.ts +385 -0
  84. package/src/trading/factory.ts +282 -0
  85. package/src/trading/index.ts +30 -0
  86. package/src/types.ts +8 -0
  87. 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
+ }