55 * of HotCRM's critical paths: hooks, actions, workflows, and AI predictions.
66 *
77 * Usage:
8- * const bench = new Benchmark('lead_scoring');
9- * const result = await bench.run(() => scoreLead(lead), { iterations: 100 });
10- * logger.info(result.summary());
8+ * const bench = new Benchmark('lead_scoring');
9+ * const result = await bench.run(() => scoreLead(lead), { iterations: 100 });
10+ * logger.info(result.summary());
1111 */
1212
1313import { createLogger } from '@hotcrm/core' ;
@@ -18,146 +18,146 @@ const logger = createLogger('ai:benchmark');
1818// ---------------------------------------------------------------------------
1919
2020export interface BenchmarkOptions {
21- /** Number of iterations (default: 100) */
22- iterations ?: number ;
23- /** Warm-up iterations before measurement (default: 5) */
24- warmup ?: number ;
25- /** Timeout per iteration in ms (default: 30000) */
26- timeout ?: number ;
21+ /** Number of iterations (default: 100) */
22+ iterations ?: number ;
23+ /** Warm-up iterations before measurement (default: 5) */
24+ warmup ?: number ;
25+ /** Timeout per iteration in ms (default: 30000) */
26+ timeout ?: number ;
2727}
2828
2929export interface BenchmarkResult {
30- /** Benchmark name */
31- name : string ;
32- /** Total iterations executed */
33- iterations : number ;
34- /** Total elapsed time in ms */
35- totalMs : number ;
36- /** Average time per iteration in ms */
37- meanMs : number ;
38- /** Median time per iteration in ms */
39- medianMs : number ;
40- /** 95th percentile latency in ms */
41- p95Ms : number ;
42- /** 99th percentile latency in ms */
43- p99Ms : number ;
44- /** Minimum iteration time in ms */
45- minMs : number ;
46- /** Maximum iteration time in ms */
47- maxMs : number ;
48- /** Operations per second */
49- opsPerSecond : number ;
50- /** Standard deviation in ms */
51- stdDevMs : number ;
52- /** Individual iteration timings in ms */
53- timings : number [ ] ;
30+ /** Benchmark name */
31+ name : string ;
32+ /** Total iterations executed */
33+ iterations : number ;
34+ /** Total elapsed time in ms */
35+ totalMs : number ;
36+ /** Average time per iteration in ms */
37+ meanMs : number ;
38+ /** Median time per iteration in ms */
39+ medianMs : number ;
40+ /** 95th percentile latency in ms */
41+ p95Ms : number ;
42+ /** 99th percentile latency in ms */
43+ p99Ms : number ;
44+ /** Minimum iteration time in ms */
45+ minMs : number ;
46+ /** Maximum iteration time in ms */
47+ maxMs : number ;
48+ /** Operations per second */
49+ opsPerSecond : number ;
50+ /** Standard deviation in ms */
51+ stdDevMs : number ;
52+ /** Individual iteration timings in ms */
53+ timings : number [ ] ;
5454}
5555
5656// ---------------------------------------------------------------------------
5757// Helpers
5858// ---------------------------------------------------------------------------
5959
6060function percentile ( sorted : number [ ] , p : number ) : number {
61- if ( sorted . length === 0 ) return 0 ;
62- const idx = Math . ceil ( ( p / 100 ) * sorted . length ) - 1 ;
63- return sorted [ Math . max ( 0 , idx ) ] ;
61+ if ( sorted . length === 0 ) return 0 ;
62+ const idx = Math . ceil ( ( p / 100 ) * sorted . length ) - 1 ;
63+ return sorted [ Math . max ( 0 , idx ) ] ;
6464}
6565
6666function stdDev ( values : number [ ] , mean : number ) : number {
67- if ( values . length <= 1 ) return 0 ;
68- const squaredDiffs = values . map ( v => ( v - mean ) ** 2 ) ;
69- return Math . sqrt ( squaredDiffs . reduce ( ( a , b ) => a + b , 0 ) / ( values . length - 1 ) ) ;
67+ if ( values . length <= 1 ) return 0 ;
68+ const squaredDiffs = values . map ( v => ( v - mean ) ** 2 ) ;
69+ return Math . sqrt ( squaredDiffs . reduce ( ( a , b ) => a + b , 0 ) / ( values . length - 1 ) ) ;
7070}
7171
7272// ---------------------------------------------------------------------------
7373// Benchmark Class
7474// ---------------------------------------------------------------------------
7575
7676export class Benchmark {
77- private readonly name : string ;
78-
79- constructor ( name : string ) {
80- this . name = name ;
81- }
82-
83- /**
84- * Run a synchronous or asynchronous function and collect timing data.
85- */
86- async run (
87- fn : ( ) => unknown | Promise < unknown > ,
88- options ?: BenchmarkOptions ,
89- ) : Promise < BenchmarkResult > {
90- const iterations = options ?. iterations ?? 100 ;
91- const warmup = options ?. warmup ?? 5 ;
92- const timeout = options ?. timeout ?? 30_000 ;
93-
94- // Warm-up phase
95- for ( let i = 0 ; i < warmup ; i ++ ) {
96- await Promise . resolve ( fn ( ) ) ;
97- }
98-
99- // Measurement phase
100- const timings : number [ ] = [ ] ;
101- const start = performance . now ( ) ;
102-
103- for ( let i = 0 ; i < iterations ; i ++ ) {
104- const iterStart = performance . now ( ) ;
105- await Promise . resolve ( fn ( ) ) ;
106- const iterEnd = performance . now ( ) ;
107- timings . push ( iterEnd - iterStart ) ;
108-
109- if ( iterEnd - start > timeout ) {
110- break ;
111- }
112- }
113-
114- const totalMs = performance . now ( ) - start ;
115- const sorted = [ ...timings ] . sort ( ( a , b ) => a - b ) ;
116- const mean = timings . reduce ( ( a , b ) => a + b , 0 ) / timings . length ;
117-
118- return {
119- name : this . name ,
120- iterations : timings . length ,
121- totalMs,
122- meanMs : mean ,
123- medianMs : percentile ( sorted , 50 ) ,
124- p95Ms : percentile ( sorted , 95 ) ,
125- p99Ms : percentile ( sorted , 99 ) ,
126- minMs : sorted [ 0 ] ?? 0 ,
127- maxMs : sorted [ sorted . length - 1 ] ?? 0 ,
128- opsPerSecond : timings . length / ( totalMs / 1000 ) ,
129- stdDevMs : stdDev ( timings , mean ) ,
130- timings,
131- } ;
132- }
77+ private readonly name : string ;
78+
79+ constructor ( name : string ) {
80+ this . name = name ;
81+ }
82+
83+ /**
84+ * Run a synchronous or asynchronous function and collect timing data.
85+ */
86+ async run (
87+ fn : ( ) => unknown | Promise < unknown > ,
88+ options ?: BenchmarkOptions ,
89+ ) : Promise < BenchmarkResult > {
90+ const iterations = options ?. iterations ?? 100 ;
91+ const warmup = options ?. warmup ?? 5 ;
92+ const timeout = options ?. timeout ?? 30_000 ;
93+
94+ // Warm-up phase
95+ for ( let i = 0 ; i < warmup ; i ++ ) {
96+ await Promise . resolve ( fn ( ) ) ;
97+ }
98+
99+ // Measurement phase
100+ const timings : number [ ] = [ ] ;
101+ const start = performance . now ( ) ;
102+
103+ for ( let i = 0 ; i < iterations ; i ++ ) {
104+ const iterStart = performance . now ( ) ;
105+ await Promise . resolve ( fn ( ) ) ;
106+ const iterEnd = performance . now ( ) ;
107+ timings . push ( iterEnd - iterStart ) ;
108+
109+ if ( iterEnd - start > timeout ) {
110+ break ;
111+ }
112+ }
113+
114+ const totalMs = performance . now ( ) - start ;
115+ const sorted = [ ...timings ] . sort ( ( a , b ) => a - b ) ;
116+ const mean = timings . reduce ( ( a , b ) => a + b , 0 ) / timings . length ;
117+
118+ return {
119+ name : this . name ,
120+ iterations : timings . length ,
121+ totalMs,
122+ meanMs : mean ,
123+ medianMs : percentile ( sorted , 50 ) ,
124+ p95Ms : percentile ( sorted , 95 ) ,
125+ p99Ms : percentile ( sorted , 99 ) ,
126+ minMs : sorted [ 0 ] ?? 0 ,
127+ maxMs : sorted [ sorted . length - 1 ] ?? 0 ,
128+ opsPerSecond : timings . length / ( totalMs / 1000 ) ,
129+ stdDevMs : stdDev ( timings , mean ) ,
130+ timings,
131+ } ;
132+ }
133133}
134134
135135// ---------------------------------------------------------------------------
136136// Suite (run multiple benchmarks)
137137// ---------------------------------------------------------------------------
138138
139139export interface BenchmarkSuiteEntry {
140- name : string ;
141- fn : ( ) => unknown | Promise < unknown > ;
142- options ?: BenchmarkOptions ;
140+ name : string ;
141+ fn : ( ) => unknown | Promise < unknown > ;
142+ options ?: BenchmarkOptions ;
143143}
144144
145145export class BenchmarkSuite {
146- private readonly entries : BenchmarkSuiteEntry [ ] = [ ] ;
147-
148- add ( name : string , fn : ( ) => unknown | Promise < unknown > , options ?: BenchmarkOptions ) : this {
149- this . entries . push ( { name, fn, options } ) ;
150- return this ;
151- }
152-
153- async run ( ) : Promise < BenchmarkResult [ ] > {
154- const results : BenchmarkResult [ ] = [ ] ;
155- for ( const entry of this . entries ) {
156- const bench = new Benchmark ( entry . name ) ;
157- results . push ( await bench . run ( entry . fn , entry . options ) ) ;
158- }
159- return results ;
160- }
146+ private readonly entries : BenchmarkSuiteEntry [ ] = [ ] ;
147+
148+ add ( name : string , fn : ( ) => unknown | Promise < unknown > , options ?: BenchmarkOptions ) : this {
149+ this . entries . push ( { name, fn, options } ) ;
150+ return this ;
151+ }
152+
153+ async run ( ) : Promise < BenchmarkResult [ ] > {
154+ const results : BenchmarkResult [ ] = [ ] ;
155+ for ( const entry of this . entries ) {
156+ const bench = new Benchmark ( entry . name ) ;
157+ results . push ( await bench . run ( entry . fn , entry . options ) ) ;
158+ }
159+ return results ;
160+ }
161161}
162162
163163// ---------------------------------------------------------------------------
@@ -168,10 +168,10 @@ export class BenchmarkSuite {
168168 * Format benchmark results as a human-readable table string.
169169 */
170170export function formatBenchmarkResults ( results : BenchmarkResult [ ] ) : string {
171- const header = '| Benchmark | Ops/s | Mean (ms) | Median (ms) | P95 (ms) | P99 (ms) | Std Dev |' ;
172- const sep = '|-----------|-------|-----------|-------------|----------|----------|---------|' ;
173- const rows = results . map ( r =>
174- `| ${ r . name } | ${ r . opsPerSecond . toFixed ( 0 ) } | ${ r . meanMs . toFixed ( 2 ) } | ${ r . medianMs . toFixed ( 2 ) } | ${ r . p95Ms . toFixed ( 2 ) } | ${ r . p99Ms . toFixed ( 2 ) } | ${ r . stdDevMs . toFixed ( 2 ) } |`
175- ) ;
176- return [ header , sep , ...rows ] . join ( '\n' ) ;
171+ const header = '| Benchmark | Ops/s | Mean (ms) | Median (ms) | P95 (ms) | P99 (ms) | Std Dev |' ;
172+ const sep = '|-----------|-------|-----------|-------------|----------|----------|---------|' ;
173+ const rows = results . map ( r =>
174+ `| ${ r . name } | ${ r . opsPerSecond . toFixed ( 0 ) } | ${ r . meanMs . toFixed ( 2 ) } | ${ r . medianMs . toFixed ( 2 ) } | ${ r . p95Ms . toFixed ( 2 ) } | ${ r . p99Ms . toFixed ( 2 ) } | ${ r . stdDevMs . toFixed ( 2 ) } |`
175+ ) ;
176+ return [ header , sep , ...rows ] . join ( '\n' ) ;
177177}
0 commit comments