1313 * limitations under the License.
1414 */
1515
16+ import {
17+ BenchmarkAbortedError ,
18+ BenchmarkDuplicateNameError ,
19+ BenchmarkEmptyError ,
20+ BenchmarkRunnerError ,
21+ } from './errors.js' ;
1622import { generateReport } from './report.js' ;
1723import type {
1824 IBenchmarkFn ,
@@ -21,20 +27,11 @@ import type {
2127 ITrialMeasurement ,
2228 ITrialResult ,
2329} from './types.js' ;
24- import { NULL_FUNCTION_NAME } from './types.js' ;
30+ import { IRunProgress , NULL_FUNCTION_NAME } from './types.js' ;
31+ import { shuffled } from './utils.js' ;
2532
2633// ── Helpers ─────────────────────────────────────────────────────────────
2734
28- /** Fisher-Yates shuffle (returns a new array). */
29- function shuffled < T > ( array : readonly T [ ] ) : T [ ] {
30- const out = [ ...array ] ;
31- for ( let i = out . length - 1 ; i > 0 ; i -- ) {
32- const j = Math . floor ( Math . random ( ) * ( i + 1 ) ) ;
33- [ out [ i ] , out [ j ] ] = [ out [ j ] , out [ i ] ] ;
34- }
35- return out ;
36- }
37-
3835/** Invoke an optional sync-or-async callback with a `this` context. */
3936async function invoke < TC extends object , TA extends unknown [ ] = never [ ] > (
4037 fn : ( ( this : TC , ...args : TA ) => unknown | PromiseLike < unknown > ) | undefined ,
@@ -86,7 +83,9 @@ async function measureTime<TC extends object>(
8683 )
8784 await r ;
8885 }
89- return performance . now ( ) - start ;
86+ const end = performance . now ( ) ;
87+
88+ return end - start ;
9089}
9190
9291// ── Suite ───────────────────────────────────────────────────────────────
@@ -142,20 +141,28 @@ export class Suite<TC extends object = Record<string, unknown>, TR = unknown> {
142141 /** Register a benchmark function. Returns `this` for chaining. */
143142 add ( fn : IBenchmarkFn < TC , TR > ) : this {
144143 if ( this . _fns . some ( ( f ) => f . name === fn . name ) ) {
145- throw new Error ( `Duplicate benchmark name: "${ fn . name } "` ) ;
144+ throw new BenchmarkDuplicateNameError (
145+ `Duplicate benchmark name: "${ fn . name } "` ,
146+ ) ;
146147 }
147148 this . _fns . push ( fn ) ;
148149 return this ;
149150 }
150151
151152 /** Execute all trials and return a {@link ISuiteReport}. */
152- async run ( ) : Promise < ISuiteReport > {
153+ async run ( opts ?: {
154+ eventTarget ?: EventTarget ;
155+ signal ?: AbortSignal ;
156+ } ) : Promise < ISuiteReport > {
153157 if ( this . _fns . length === 0 ) {
154- throw new Error (
158+ throw new BenchmarkEmptyError (
155159 'Suite has no benchmark functions — call .add() before .run()' ,
156160 ) ;
157161 }
158162
163+ const eventTarget = opts ?. eventTarget ;
164+ const signal = opts ?. signal ;
165+
159166 // Inject the null baseline — an empty function that captures the
160167 // overhead of the measurement loop (call dispatch, thenable check,
161168 // loop counter). It participates in shuffling like every other
@@ -176,44 +183,70 @@ export class Suite<TC extends object = Record<string, unknown>, TR = unknown> {
176183 const measurements : Record < string , ITrialMeasurement > = { } ;
177184
178185 for ( const bench of order ) {
179- if (
180- bench . name !== NULL_FUNCTION_NAME &&
181- ( this . _suiteValidate || bench . setup )
182- ) {
183- const validateCtx = { } as TC ;
184- await invoke (
185- this . _suiteValidate ,
186- validateCtx ,
187- bench . fn as IBenchmarkFn < TC , TR > [ 'fn' ] ,
186+ try {
187+ if ( signal ?. aborted ) {
188+ throw new BenchmarkAbortedError ( 'Aborted' ) ;
189+ }
190+ if ( eventTarget ) {
191+ eventTarget . dispatchEvent (
192+ new CustomEvent < IRunProgress > ( 'progress' , {
193+ detail : {
194+ trial : t + 1 ,
195+ totalTrials : this . _trials ,
196+ currentFunction : bench . name ,
197+ } ,
198+ } ) ,
199+ ) ;
200+ // Allow event to propagate
201+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
202+ }
203+
204+ if (
205+ bench . name !== NULL_FUNCTION_NAME &&
206+ ( this . _suiteValidate || bench . setup )
207+ ) {
208+ const validateCtx = { } as TC ;
209+ await invoke (
210+ this . _suiteValidate ,
211+ validateCtx ,
212+ bench . fn as IBenchmarkFn < TC , TR > [ 'fn' ] ,
213+ ) ;
214+ await invoke (
215+ bench . validate ,
216+ validateCtx ,
217+ bench . fn as IBenchmarkFn < TC , TR > [ 'fn' ] ,
218+ ) ;
219+ }
220+
221+ const ctx = { } as TC ;
222+ await invoke ( this . _suiteSetup , ctx ) ;
223+ await invoke ( bench . setup , ctx ) ;
224+
225+ const totalMs = await measureTime (
226+ bench . fn ,
227+ ctx ,
228+ this . _warmup ,
229+ this . _iterations ,
188230 ) ;
189- await invoke (
190- bench . validate ,
191- validateCtx ,
192- bench . fn as IBenchmarkFn < TC , TR > [ 'fn' ] ,
231+
232+ await invoke ( bench . teardown , ctx ) ;
233+ await invoke ( this . _suiteTeardown , ctx ) ;
234+
235+ executionOrder . push ( bench . name ) ;
236+ measurements [ bench . name ] = {
237+ name : bench . name ,
238+ totalMs,
239+ iterations : this . _iterations ,
240+ perIterationMs : totalMs / this . _iterations ,
241+ } ;
242+ } catch ( e ) {
243+ throw new BenchmarkRunnerError (
244+ `Error in ${ bench . name } ` ,
245+ e ,
246+ bench . name ,
247+ t ,
193248 ) ;
194249 }
195-
196- const ctx = { } as TC ;
197- await invoke ( this . _suiteSetup , ctx ) ;
198- await invoke ( bench . setup , ctx ) ;
199-
200- const totalMs = await measureTime (
201- bench . fn ,
202- ctx ,
203- this . _warmup ,
204- this . _iterations ,
205- ) ;
206-
207- await invoke ( bench . teardown , ctx ) ;
208- await invoke ( this . _suiteTeardown , ctx ) ;
209-
210- executionOrder . push ( bench . name ) ;
211- measurements [ bench . name ] = {
212- name : bench . name ,
213- totalMs,
214- iterations : this . _iterations ,
215- perIterationMs : totalMs / this . _iterations ,
216- } ;
217250 }
218251
219252 trials . push ( { trialIndex : t , executionOrder, measurements } ) ;
@@ -243,10 +276,14 @@ export async function runSuite<
243276 TC extends object = Record < string , unknown > ,
244277 TR = unknown ,
245278> (
246- config : ISuiteConfig < TC , TR > & { functions : IBenchmarkFn < TC , TR > [ ] } ,
279+ config : ISuiteConfig < TC , TR > & {
280+ functions : IBenchmarkFn < TC , TR > [ ] ;
281+ eventTarget ?: EventTarget ;
282+ signal ?: AbortSignal ;
283+ } ,
247284) : Promise < ISuiteReport > {
248- const { functions, ...suiteConfig } = config ;
285+ const { functions, eventTarget , signal , ...suiteConfig } = config ;
249286 const suite = new Suite < TC , TR > ( suiteConfig ) ;
250287 for ( const fn of functions ) suite . add ( fn ) ;
251- return suite . run ( ) ;
288+ return suite . run ( { eventTarget , signal } ) ;
252289}
0 commit comments