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- * console.log (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
13+ import { createLogger } from '@hotcrm/core' ;
14+ const logger = createLogger ( 'ai:benchmark' ) ;
15+
1316// ---------------------------------------------------------------------------
1417// Types
1518// ---------------------------------------------------------------------------
1619
1720export interface BenchmarkOptions {
18- /** Number of iterations (default: 100) */
19- iterations ?: number ;
20- /** Warm-up iterations before measurement (default: 5) */
21- warmup ?: number ;
22- /** Timeout per iteration in ms (default: 30000) */
23- 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 ;
2427}
2528
2629export interface BenchmarkResult {
27- /** Benchmark name */
28- name : string ;
29- /** Total iterations executed */
30- iterations : number ;
31- /** Total elapsed time in ms */
32- totalMs : number ;
33- /** Average time per iteration in ms */
34- meanMs : number ;
35- /** Median time per iteration in ms */
36- medianMs : number ;
37- /** 95th percentile latency in ms */
38- p95Ms : number ;
39- /** 99th percentile latency in ms */
40- p99Ms : number ;
41- /** Minimum iteration time in ms */
42- minMs : number ;
43- /** Maximum iteration time in ms */
44- maxMs : number ;
45- /** Operations per second */
46- opsPerSecond : number ;
47- /** Standard deviation in ms */
48- stdDevMs : number ;
49- /** Individual iteration timings in ms */
50- 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 [ ] ;
5154}
5255
5356// ---------------------------------------------------------------------------
5457// Helpers
5558// ---------------------------------------------------------------------------
5659
5760function percentile ( sorted : number [ ] , p : number ) : number {
58- if ( sorted . length === 0 ) return 0 ;
59- const idx = Math . ceil ( ( p / 100 ) * sorted . length ) - 1 ;
60- 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 ) ] ;
6164}
6265
6366function stdDev ( values : number [ ] , mean : number ) : number {
64- if ( values . length <= 1 ) return 0 ;
65- const squaredDiffs = values . map ( v => ( v - mean ) ** 2 ) ;
66- 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 ) ) ;
6770}
6871
6972// ---------------------------------------------------------------------------
7073// Benchmark Class
7174// ---------------------------------------------------------------------------
7275
7376export class Benchmark {
74- private readonly name : string ;
75-
76- constructor ( name : string ) {
77- this . name = name ;
78- }
79-
80- /**
81- * Run a synchronous or asynchronous function and collect timing data.
82- */
83- async run (
84- fn : ( ) => unknown | Promise < unknown > ,
85- options ?: BenchmarkOptions ,
86- ) : Promise < BenchmarkResult > {
87- const iterations = options ?. iterations ?? 100 ;
88- const warmup = options ?. warmup ?? 5 ;
89- const timeout = options ?. timeout ?? 30_000 ;
90-
91- // Warm-up phase
92- for ( let i = 0 ; i < warmup ; i ++ ) {
93- await Promise . resolve ( fn ( ) ) ;
94- }
95-
96- // Measurement phase
97- const timings : number [ ] = [ ] ;
98- const start = performance . now ( ) ;
99-
100- for ( let i = 0 ; i < iterations ; i ++ ) {
101- const iterStart = performance . now ( ) ;
102- await Promise . resolve ( fn ( ) ) ;
103- const iterEnd = performance . now ( ) ;
104- timings . push ( iterEnd - iterStart ) ;
105-
106- if ( iterEnd - start > timeout ) {
107- break ;
108- }
109- }
110-
111- const totalMs = performance . now ( ) - start ;
112- const sorted = [ ...timings ] . sort ( ( a , b ) => a - b ) ;
113- const mean = timings . reduce ( ( a , b ) => a + b , 0 ) / timings . length ;
114-
115- return {
116- name : this . name ,
117- iterations : timings . length ,
118- totalMs,
119- meanMs : mean ,
120- medianMs : percentile ( sorted , 50 ) ,
121- p95Ms : percentile ( sorted , 95 ) ,
122- p99Ms : percentile ( sorted , 99 ) ,
123- minMs : sorted [ 0 ] ?? 0 ,
124- maxMs : sorted [ sorted . length - 1 ] ?? 0 ,
125- opsPerSecond : timings . length / ( totalMs / 1000 ) ,
126- stdDevMs : stdDev ( timings , mean ) ,
127- timings,
128- } ;
129- }
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+ }
130133}
131134
132135// ---------------------------------------------------------------------------
133136// Suite (run multiple benchmarks)
134137// ---------------------------------------------------------------------------
135138
136139export interface BenchmarkSuiteEntry {
137- name : string ;
138- fn : ( ) => unknown | Promise < unknown > ;
139- options ?: BenchmarkOptions ;
140+ name : string ;
141+ fn : ( ) => unknown | Promise < unknown > ;
142+ options ?: BenchmarkOptions ;
140143}
141144
142145export class BenchmarkSuite {
143- private readonly entries : BenchmarkSuiteEntry [ ] = [ ] ;
144-
145- add ( name : string , fn : ( ) => unknown | Promise < unknown > , options ?: BenchmarkOptions ) : this {
146- this . entries . push ( { name, fn, options } ) ;
147- return this ;
148- }
149-
150- async run ( ) : Promise < BenchmarkResult [ ] > {
151- const results : BenchmarkResult [ ] = [ ] ;
152- for ( const entry of this . entries ) {
153- const bench = new Benchmark ( entry . name ) ;
154- results . push ( await bench . run ( entry . fn , entry . options ) ) ;
155- }
156- return results ;
157- }
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+ }
158161}
159162
160163// ---------------------------------------------------------------------------
@@ -165,10 +168,10 @@ export class BenchmarkSuite {
165168 * Format benchmark results as a human-readable table string.
166169 */
167170export function formatBenchmarkResults ( results : BenchmarkResult [ ] ) : string {
168- const header = '| Benchmark | Ops/s | Mean (ms) | Median (ms) | P95 (ms) | P99 (ms) | Std Dev |' ;
169- const sep = '|-----------|-------|-----------|-------------|----------|----------|---------|' ;
170- const rows = results . map ( r =>
171- `| ${ 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 ) } |`
172- ) ;
173- 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' ) ;
174177}
0 commit comments