Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Core: Add QUnit.reporters.perf (factor PerfReporter from suite.js)
  • Loading branch information
Krinkle committed May 28, 2023
commit dc3b90e70d22cd8d7a90e6d7b021e783dc48103e
37 changes: 2 additions & 35 deletions src/core/utilities.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,12 @@
import { window } from '../globals';
import Logger from '../logger';

export const toString = Object.prototype.toString;
export const hasOwn = Object.prototype.hasOwnProperty;
export const slice = Array.prototype.slice;

const nativePerf = getNativePerf();

// TODO: Consider using globalThis instead so that perf marks work
// in Node.js as well. As they can have overhead, we should also
// have a way to disable these, and/or make them an opt-in reporter
// in QUnit 3 and then support globalThis.
// For example: `QUnit.addReporter(QUnit.reporters.perf)`.
function getNativePerf () {
if (window &&
typeof window.performance !== 'undefined' &&
typeof window.performance.mark === 'function' &&
typeof window.performance.measure === 'function'
) {
return window.performance;
} else {
return undefined;
}
}

export const performance = {
now: nativePerf
? nativePerf.now.bind(nativePerf)
: Date.now,
measure: nativePerf
? function (comment, startMark, endMark) {
// `performance.measure` may fail if the mark could not be found.
// reasons a specific mark could not be found include: outside code invoking `performance.clearMarks()`
try {
nativePerf.measure(comment, startMark, endMark);
} catch (ex) {
Logger.warn('performance.measure could not be executed because of ', ex.message);
}
}
: function () {},
mark: nativePerf ? nativePerf.mark.bind(nativePerf) : function () {}
// eslint-disable-next-line compat/compat -- Checked
now: window && window.performance && window.performance.now ? window.performance.now.bind(window.performance) : Date.now
};

// Returns a new Array with the elements that are in a but not in b
Expand Down
2 changes: 2 additions & 0 deletions src/html-reporter/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export function escapeText (str) {
return;
}

QUnit.reporters.perf.init(QUnit);

const config = QUnit.config;
const hiddenTests = [];
let collapseNext = false;
Expand Down
2 changes: 2 additions & 0 deletions src/reporters.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import ConsoleReporter from './reporters/ConsoleReporter.js';
import PerfReporter from './reporters/PerfReporter.js';
import TapReporter from './reporters/TapReporter.js';

export default {
console: ConsoleReporter,
perf: PerfReporter,
tap: TapReporter
};
92 changes: 92 additions & 0 deletions src/reporters/PerfReporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { window } from '../globals';
import Logger from '../logger';

// TODO: Consider using globalThis instead of window, so that the reporter
// works for Node.js as well. As this can add overhead, we should make
// this opt-in before we enable it for CLI.
//
// QUnit 3 will switch from `window` to `globalThis` and then make it
// no longer an implicit feature of the HTML Reporter, but rather let
// it be opt-in via `QUnit.config.reporters = ['perf']` or something
// like that.
const nativePerf = (
window &&
typeof window.performance !== 'undefined' &&
// eslint-disable-next-line compat/compat -- Checked
typeof window.performance.mark === 'function' &&
// eslint-disable-next-line compat/compat -- Checked
typeof window.performance.measure === 'function'
)
? window.performance
: undefined;

const perf = {
measure: nativePerf
? function (comment, startMark, endMark) {
// `performance.measure` may fail if the mark could not be found.
// reasons a specific mark could not be found include: outside code invoking `performance.clearMarks()`
try {
nativePerf.measure(comment, startMark, endMark);
} catch (ex) {
Logger.warn('performance.measure could not be executed because of ', ex.message);
}
}
: function () {},
mark: nativePerf ? nativePerf.mark.bind(nativePerf) : function () {}
};

export default class PerfReporter {
constructor (runner, options = {}) {
this.perf = options.perf || perf;

runner.on('runStart', this.onRunStart.bind(this));
runner.on('runEnd', this.onRunEnd.bind(this));
runner.on('suiteStart', this.onSuiteStart.bind(this));
runner.on('suiteEnd', this.onSuiteEnd.bind(this));
runner.on('testStart', this.onTestStart.bind(this));
runner.on('testEnd', this.onTestEnd.bind(this));
}

static init (runner, options) {
return new PerfReporter(runner, options);
}

onRunStart () {
this.perf.mark('qunit_suite_0_start');
}

onSuiteStart (suiteStart) {
const suiteLevel = suiteStart.fullName.length;
this.perf.mark(`qunit_suite_${suiteLevel}_start`);
}

onSuiteEnd (suiteEnd) {
const suiteLevel = suiteEnd.fullName.length;
const suiteName = suiteEnd.fullName.join(' – ');

this.perf.mark(`qunit_suite_${suiteLevel}_end`);
this.perf.measure(`QUnit Test Suite: ${suiteName}`,
`qunit_suite_${suiteLevel}_start`,
`qunit_suite_${suiteLevel}_end`
);
}

onTestStart () {
this.perf.mark('qunit_test_start');
}

onTestEnd (testEnd) {
this.perf.mark('qunit_test_end');
const testName = testEnd.fullName.join(' – ');

this.perf.measure(`QUnit Test: ${testName}`,
'qunit_test_start',
'qunit_test_end'
);
}

onRunEnd () {
this.perf.mark('qunit_suite_0_end');
this.perf.measure('QUnit Test Run', 'qunit_suite_0_start', 'qunit_suite_0_end');
}
}
13 changes: 0 additions & 13 deletions src/reports/suite.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ export default class SuiteReport {
start (recordTime) {
if (recordTime) {
this._startTime = performance.now();

const suiteLevel = this.fullName.length;
performance.mark(`qunit_suite_${suiteLevel}_start`);
}

return {
Expand All @@ -40,16 +37,6 @@ export default class SuiteReport {
end (recordTime) {
if (recordTime) {
this._endTime = performance.now();

const suiteLevel = this.fullName.length;
const suiteName = this.fullName.join(' – ');

performance.mark(`qunit_suite_${suiteLevel}_end`);
performance.measure(
suiteLevel === 0 ? 'QUnit Test Run' : `QUnit Test Suite: ${suiteName}`,
`qunit_suite_${suiteLevel}_start`,
`qunit_suite_${suiteLevel}_end`
);
}

return {
Expand Down
12 changes: 0 additions & 12 deletions src/reports/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export default class TestReport {
start (recordTime) {
if (recordTime) {
this._startTime = performance.now();
performance.mark('qunit_test_start');
}

return {
Expand All @@ -35,17 +34,6 @@ export default class TestReport {
end (recordTime) {
if (recordTime) {
this._endTime = performance.now();
if (performance) {
performance.mark('qunit_test_end');

const testName = this.fullName.join(' – ');

performance.measure(
`QUnit Test: ${testName}`,
'qunit_test_start',
'qunit_test_end'
);
}
}

return extend(this.start(), {
Expand Down
118 changes: 118 additions & 0 deletions test/cli/PerfReporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const { EventEmitter } = require('events');

class MockPerf {
constructor () {
this.marks = new Map();
this.measures = [];
this.clock = 1;
}

mark (name) {
this.clock++;
this.marks.set(name, this.clock);
}

measure (name, startMark, endMark) {
const startTime = this.marks.get(startMark);
const endTime = this.marks.get(endMark);
this.measures.push({ name, startTime, endTime });
this.measures.sort((a, b) => a.startTime - b.startTime);
}
}

QUnit.module('PerfReporter', hooks => {
let emitter;
let perf;

hooks.beforeEach(function () {
emitter = new EventEmitter();
perf = new MockPerf();
QUnit.reporters.perf.init(emitter, {
perf
});
});

QUnit.test('Flat suites', assert => {
emitter.emit('runStart', {});
emitter.emit('suiteStart', { fullName: ['Foo'] });
emitter.emit('testStart', { fullName: ['Foo', 'example'] });
emitter.emit('testEnd', { fullName: ['Foo', 'example'] });
emitter.emit('suiteEnd', { fullName: ['Foo'] });
emitter.emit('suiteStart', { fullName: ['Bar'] });
emitter.emit('testStart', { fullName: ['Bar', 'example'] });
emitter.emit('testEnd', { fullName: ['Bar', 'example'] });
emitter.emit('suiteEnd', { fullName: ['Bar'] });
emitter.emit('runEnd', {});

assert.deepEqual(
perf.measures,
[{
name: 'QUnit Test Run',
startTime: 2,
endTime: 11
},
{
name: 'QUnit Test Suite: Foo',
startTime: 3,
endTime: 6
},
{
name: 'QUnit Test: Foo – example',
startTime: 4,
endTime: 5
},
{
name: 'QUnit Test Suite: Bar',
startTime: 7,
endTime: 10
},
{
name: 'QUnit Test: Bar – example',
startTime: 8,
endTime: 9
}]
);
});

QUnit.test('Nested suites', assert => {
emitter.emit('runStart', {});
emitter.emit('suiteStart', { fullName: ['Foo'] });
emitter.emit('testStart', { fullName: ['Foo', 'one'] });
emitter.emit('testEnd', { fullName: ['Foo', 'one'] });
emitter.emit('suiteStart', { fullName: ['Foo', 'Bar'] });
emitter.emit('testStart', { fullName: ['Foo', 'Bar', 'two'] });
emitter.emit('testEnd', { fullName: ['Foo', 'Bar', 'two'] });
emitter.emit('suiteEnd', { fullName: ['Foo', 'Bar'] });
emitter.emit('suiteEnd', { fullName: ['Fo'] });
emitter.emit('runEnd', {});

assert.deepEqual(
perf.measures,
[{
name: 'QUnit Test Run',
startTime: 2,
endTime: 11
},
{
name: 'QUnit Test Suite: Fo',
startTime: 3,
endTime: 10
},
{
name: 'QUnit Test: Foo – one',
startTime: 4,
endTime: 5
},
{
name: 'QUnit Test Suite: Foo – Bar',
startTime: 6,
endTime: 9
},
{
name: 'QUnit Test: Foo – Bar – two',
startTime: 7,
endTime: 8
}]
);
});
});
4 changes: 2 additions & 2 deletions test/cli/fixtures/expected/tap-outputs.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,14 @@ ok 5 A-Test > derp
'qunit --reporter does-not-exist':
`# stderr
No reporter found matching "does-not-exist".
Built-in reporters: console, tap
Built-in reporters: console, perf, tap
Extra reporters found among package dependencies: npm-reporter

# exit code: 1`,

'qunit --reporter':
`# stderr
Built-in reporters: console, tap
Built-in reporters: console, perf, tap
Extra reporters found among package dependencies: npm-reporter

# exit code: 1`,
Expand Down