• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

uber-web / probe.gl / 3732260095

pending completion
3732260095

push

github

GitHub
chore: stricter typescript settings (#213)

262 of 614 branches covered (42.67%)

Branch coverage included in aggregate %.

29 of 29 new or added lines in 6 files covered. (100.0%)

549 of 969 relevant lines covered (56.66%)

283546.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.35
/modules/bench/src/bench.ts
1
/* eslint-disable no-console */
2
import {autobind, LocalStorage, getHiResTimestamp} from '@probe.gl/log';
3
import {formatSI} from './format-utils';
4
import {mean, cv} from './stat-utils';
5
import {logResultsAsMarkdownTable, logResultsAsTree} from './bench-loggers';
6

7
const noop = () => {};
1✔
8

9
export type LogEntryType = 'group' | 'test' | 'complete';
10

11
/** @deprecated - Just use string constants */
12
export const LOG_ENTRY = {
1✔
13
  GROUP: 'group',
14
  TEST: 'test',
15
  COMPLETE: 'complete'
16
};
17

18
export type LogEntry = {
19
  entry: LogEntryType;
20
  id: string;
21
  message: string;
22
  itersPerSecond: string;
23
  unit: string;
24
  error: any;
25
  time: number;
26
};
27

28
export type Logger = (entry: LogEntry) => void;
29

30
/** Properties for benchmark suite */
31
export type BenchProps = {
32
  /** Id of suite. @note Used as key for regression (storing/loading results in browser storage) */
33
  id?: string;
34
  /** Log object */
35
  log?: Logger | null;
36
  /** Minimum number of milliseconds to iterate each bench test */
37
  time?: number;
38
  /** milliseconds of idle time, or "cooldown" between tests */
39
  delay?: number;
40
  /** Increase if OK to let slow benchmarks take long time, potentially produces more stable results */
41
  minIterations?: number;
42
};
43

44
const DEFAULT_BENCH_OPTIONS: Required<BenchProps> = {
1✔
45
  id: '',
46
  log: null,
47
  time: 80,
48
  delay: 5,
49
  minIterations: 3
50
};
51

52
/** One test in the suite */
53
type BenchTest = {
54
  id: string;
55
  priority?: number;
56
  message?: string;
57
  initFunc?: Function;
58
  testFunc?: Function;
59
  group?: boolean;
60
  async?: boolean;
61
  once?: boolean;
62
  opts: BenchProps & {
63
    multipler?: number;
64
    unit?: string;
65
  };
66
};
67

68
/**
69
 * A benchmark suite.
70
 * Test cases can be added and then benchmarks can be run/
71
 */
72
export default class Bench {
73
  id: string;
74
  opts: Required<BenchProps>;
75
  tests: Record<string, BenchTest> = {};
2✔
76
  results: Record<string, unknown> = {};
2✔
77
  table: Record<string, any> = {};
2✔
78

79
  constructor(props: BenchProps = {}) {
×
80
    this.opts = {...DEFAULT_BENCH_OPTIONS, ...props};
2✔
81
    const {id, log, time, delay, minIterations} = this.opts;
2✔
82

83
    let logger = log;
2✔
84
    if (!logger) {
2✔
85
      const markdown = globalThis.probe && globalThis.probe.markdown;
1✔
86
      logger = markdown ? logResultsAsMarkdownTable : logResultsAsTree;
1!
87
    }
88

89
    this.id = id;
2✔
90
    this.opts = {id, log: logger, time, delay, minIterations};
2✔
91
    autobind(this);
2✔
92
    Object.seal(this);
2✔
93
  }
94

95
  /** Not yet implemented */
96
  calibrate(id?: string, func1?: Function, func2?: Function, opts?: {}): this {
97
    return this;
×
98
  }
99

100
  /** Runs the test suite */
101
  async run(): Promise<void> {
102
    const timeStart = getHiResTimestamp();
1✔
103

104
    // eslint-disable-next-line @typescript-eslint/unbound-method
105
    const {tests, onBenchmarkComplete} = this;
1✔
106
    // @ts-expect-error
107
    await runTests({tests, onBenchmarkComplete});
1✔
108

109
    const elapsed = (getHiResTimestamp() - timeStart) / 1000;
1✔
110
    logEntry(this, {entry: 'complete', time: elapsed, message: 'Complete'});
1✔
111
    this.onSuiteComplete();
1✔
112
  }
113

114
  /** Adds a group to the test suite */
115
  group(id: string): this {
116
    if (this.tests[id]) {
4!
117
      throw new Error('tests need unique id strings');
×
118
    }
119
    this.tests[id] = {id, group: true, opts: this.opts};
4✔
120
    return this;
4✔
121
  }
122

123
  add(priority, id, func1?, func2?): this {
124
    this._add(priority, id, func1, func2);
19✔
125
    return this;
19✔
126
  }
127

128
  // Mark test as async (returns promise)
129
  addAsync(priority, id, func1?, func2?): this {
130
    const test = this._add(priority, id, func1, func2);
2✔
131
    test.async = true;
2✔
132
    return this;
2✔
133
  }
134

135
  onBenchmarkComplete(params: {
136
    id: string;
137
    time: number;
138
    iterations: number;
139
    itersPerSecond: number;
140
  }): void {
141
    const {id, time, iterations, itersPerSecond} = params;
21✔
142
    // calculate iterations per second, save as numeric value
143
    const current = Math.round(iterations / time);
21✔
144
    // Format as human readable strings
145
    this.table[id] = {
21✔
146
      percent: '',
147
      iterations: `${itersPerSecond}/s`,
148
      current,
149
      max: ''
150
    };
151
  }
152

153
  onSuiteComplete(): void {
154
    const localStorage = new LocalStorage<Record<string, any>>(this.id, {});
1✔
155
    const saved = localStorage.getConfiguration();
1✔
156
    const current = this.updateTable(this.table, saved);
1✔
157
    localStorage.setConfiguration(current);
1✔
158
    console.table(current);
1✔
159
  }
160

161
  updateTable(current: Record<string, any>, saved: Record<string, any>): Record<string, any> {
162
    for (const id in this.table) {
1✔
163
      if (saved[id] && saved[id].max !== undefined) {
21!
164
        current[id].max = Math.max(current[id].current, saved[id].max);
×
165
        const delta = current[id].current / saved[id].max;
×
166
        current[id].percent = `${Math.round(delta * 100 - 100)}%`;
×
167
      } else {
168
        current[id].max = current[id].current;
21✔
169
      }
170
    }
171
    return current;
1✔
172
  }
173

174
  // Signatures:
175
  //  add(id, {...options}, testFunc)
176
  //  add(id, testFunc)
177
  // Deprecated signatures
178
  //  add(priority, id, testFunc)
179
  //  add(priority, id, initFunc, testFunc)
180
  //  add(id, initFunc, testFunc)
181

182
  _add(priority: number | string, id: string | Function, func1: Function, func2?: Function) {
183
    let options = {};
21✔
184

185
    if (typeof priority === 'number') {
21✔
186
      console.warn('`priority` argument is deprecated, use `options.priority` instead');
4✔
187
    }
188

189
    if (typeof priority === 'string' && typeof id === 'object') {
21✔
190
      options = id;
15✔
191
      id = priority;
15✔
192
      priority = 0;
15✔
193
    } else if (typeof priority === 'string') {
6✔
194
      func2 = func1;
2✔
195
      func1 = id as FunctionConstructor;
2✔
196
      id = priority;
2✔
197
      priority = 0;
2✔
198
    }
199

200
    if (typeof id !== 'string' || typeof func1 !== 'function') {
21!
201
      throw new Error('_add');
×
202
    }
203

204
    // @ts-expect-error
205
    let initFunc = options.initialize;
21✔
206
    let testFunc = func1;
21✔
207
    if (typeof func2 === 'function') {
21✔
208
      console.warn('`initFunc` argument is deprecated, use `options.initialize` instead');
1✔
209
      initFunc = func1;
1✔
210
      testFunc = func2;
1✔
211
    }
212

213
    // Test case options
214
    const opts = {
21✔
215
      ...this.opts,
216
      multiplier: 1, // multiplier per test case
217
      unit: '',
218
      ...options
219
    };
220

221
    if (this.tests[id]) {
21!
222
      throw new Error('tests need unique id strings');
×
223
    }
224

225
    this.tests[id] = {
21✔
226
      id,
227
      priority,
228
      initFunc,
229
      testFunc,
230
      opts
231
    };
232
    return this.tests[id];
21✔
233
  }
234
}
235

236
// Helper methods
237

238
// Helper function to promisify setTimeout
239
function addDelay(timeout: number): Promise<void> {
240
  return new Promise(resolve => {
21✔
241
    setTimeout(() => resolve(), timeout);
21✔
242
  });
243
}
244

245
function runCalibrationTests({tests}: {tests: Record<string, BenchTest>}): void {
246
  // Beat JIT - run each test once
247
  for (const id in tests) {
1✔
248
    const test = tests[id];
25✔
249
    if (!test.group) {
25✔
250
      runBenchTestIterations(test, 1);
21✔
251
    }
252
  }
253
}
254

255
function logEntry(test: Bench, opts: any): void {
256
  const priority = (globalThis.probe && globalThis.probe.priority) | 10;
26✔
257
  if ((opts.priority | 0) <= priority) {
26!
258
    opts = {...test, ...test.opts, ...opts, id: test.id};
26✔
259
    delete opts.opts;
26✔
260
    test.opts.log(opts);
26✔
261
  }
262
}
263

264
// Run a list of bench test case asynchronously (with short timeouts inbetween)
265
async function runTests({tests, onBenchmarkComplete = noop}) {
×
266
  // Run default warm up and calibration tests
267
  // @ts-expect-error
268
  runCalibrationTests({tests, onBenchmarkComplete});
1✔
269

270
  // Run the suite tests
271
  for (const id in tests) {
1✔
272
    const test = tests[id];
25✔
273
    if (test.group) {
25✔
274
      logEntry(test, {entry: 'group', message: test.id});
4✔
275
    } else {
276
      await runTest({test, onBenchmarkComplete});
21✔
277
    }
278
  }
279
}
280

281
async function runTest({test, onBenchmarkComplete, silent = false}) {
21✔
282
  // Inject a small delay between each test. System cools and DOM console updates...
283
  await addDelay(test.opts.delay);
21✔
284

285
  const result = await runBenchTestAsync(test);
21✔
286

287
  const {iterationsPerSecond, time, iterations, error} = result;
21✔
288

289
  const itersPerSecond = formatSI(iterationsPerSecond);
21✔
290

291
  if (!silent) {
21!
292
    logEntry(test, {
21✔
293
      entry: 'test',
294
      itersPerSecond,
295
      time,
296
      error,
297
      message: `${test.id} ${itersPerSecond} ${test.opts.unit}/s ±${(error * 100).toFixed(2)}%`
298
    });
299
  }
300

301
  if (onBenchmarkComplete) {
21!
302
    onBenchmarkComplete({
21✔
303
      id: test.id,
304
      time,
305
      iterations,
306
      iterationsPerSecond,
307
      itersPerSecond
308
    });
309
  }
310
}
311

312
// Test runners
313

314
async function runBenchTestAsync(test) {
315
  const results = [];
21✔
316
  let totalTime = 0;
21✔
317
  let totalIterations = 0;
21✔
318

319
  for (let i = 0; i < test.opts.minIterations; i++) {
21✔
320
    let time;
321
    let iterations;
322
    // Runs "test._throughput" parallel test cases
323
    if (test.async && test.opts._throughput) {
63✔
324
      const {_throughput} = test.opts;
3✔
325
      ({time, iterations} = await runBenchTestParallelIterationsAsync(test, _throughput));
3✔
326
    } else {
327
      ({time, iterations} = await runBenchTestForMinimumTimeAsync(test, test.opts.time));
60✔
328
    }
329

330
    // Test options can have `multiplier` to return a more semantic number
331
    // (e.g. number of bytes, lines, points or pixels decoded per iteration)
332
    iterations *= test.opts.multiplier;
63✔
333

334
    const iterationsPerSecond = iterations / time;
63✔
335
    results.push(iterationsPerSecond);
63✔
336
    totalTime += time;
63✔
337
    totalIterations += iterations;
63✔
338
  }
339

340
  return {
21✔
341
    time: totalTime,
342
    iterations: totalIterations,
343
    iterationsPerSecond: mean(results),
344
    error: cv(results)
345
  };
346
}
347

348
// Run a test func for an increasing amount of iterations until time threshold exceeded
349
async function runBenchTestForMinimumTimeAsync(test, minTime) {
350
  let iterations = 1;
60✔
351
  let elapsedMillis = 0;
60✔
352

353
  // Run increasing amount of interations until we reach time threshold, default at least 100ms
354
  while (elapsedMillis < minTime) {
60✔
355
    let multiplier = 10;
340✔
356
    if (elapsedMillis > 10) {
340✔
357
      multiplier = (test.opts.time / elapsedMillis) * 1.25;
47✔
358
    }
359
    iterations *= multiplier;
340✔
360
    const timeStart = getHiResTimestamp();
340✔
361
    if (test.async) {
340✔
362
      await runBenchTestIterationsAsync(test, iterations);
3✔
363
    } else {
364
      runBenchTestIterations(test, iterations);
337✔
365
    }
366
    elapsedMillis = getHiResTimestamp() - timeStart;
340✔
367
  }
368

369
  const time = elapsedMillis / 1000;
60✔
370

371
  return {
60✔
372
    time,
373
    iterations
374
  };
375
}
376

377
// Run a test func for a specific amount of parallel iterations
378
async function runBenchTestParallelIterationsAsync(test, iterations) {
379
  const testArgs = test.initFunc && test.initFunc();
3!
380

381
  const timeStart = getHiResTimestamp();
3✔
382

383
  const promises = [];
3✔
384

385
  const {context, testFunc} = test;
3✔
386
  for (let i = 0; i < iterations; i++) {
3✔
387
    promises.push(testFunc.call(context, testArgs));
3,000,000✔
388
  }
389

390
  await Promise.all(promises);
3✔
391

392
  const time = (getHiResTimestamp() - timeStart) / 1000;
3✔
393

394
  return {
3✔
395
    time,
396
    iterations
397
  };
398
}
399

400
// Run a test func for a specific amount of iterations
401
async function runBenchTestIterationsAsync(test, iterations) {
402
  const testArgs = test.initFunc && test.initFunc();
3!
403
  const {context, testFunc} = test;
3✔
404
  for (let i = 0; i < iterations; i++) {
3✔
405
    await testFunc.call(context, testArgs);
30✔
406
  }
407
}
408

409
// Sync tests
410

411
// Run a test func for a specific amount of iterations
412
function runBenchTestIterations(test, iterations) {
413
  const testArgs = test.initFunc && test.initFunc();
358✔
414

415
  // When running sync, avoid overhead of parameter passing if not needed
416
  const {context, testFunc} = test;
358✔
417
  if (context && testArgs) {
358!
418
    for (let i = 0; i < iterations; i++) {
×
419
      testFunc.call(context, testArgs);
×
420
    }
421
  } else {
422
    for (let i = 0; i < iterations; i++) {
358✔
423
      testFunc.call(context);
271,717,445✔
424
    }
425
  }
426
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc