Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Add QUnit.reporters.perf (factor PerfReporter from suite.js) #1714

Merged
merged 2 commits into from
May 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 1 addition & 2 deletions bin/qunit.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ program
const opts = program.opts();

if (opts.reporter === true) {
const requireQUnit = require('../src/cli/require-qunit');
displayAvailableReporters(requireQUnit().reporters);
displayAvailableReporters();
}

const options = {
Expand Down
9 changes: 6 additions & 3 deletions src/cli/find-reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,20 @@ function findReporter (reporterName, builtin) {
}

// If we didn't find a reporter, display the available reporters and exit
displayAvailableReporters(builtin, reporterName);
displayAvailableReporters(reporterName);
}

function displayAvailableReporters (builtin, inputReporterName) {
function displayAvailableReporters (inputReporterName) {
const message = [];

if (inputReporterName) {
message.push(`No reporter found matching "${inputReporterName}".`);
}

const jsReporters = Object.keys(builtin).sort();
const jsReporters = [
'console',
'tap'
].sort();
message.push(`Built-in reporters: ${jsReporters.join(', ')}`);

const npmReporters = getReportersFromDependencies();
Expand Down
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
}]
);
});
});