Skip to content

Commit 3abec44

Browse files
committed
Core: Add QUnit.reporters.perf (factor PerfReporter from suite.js)
1 parent 65941ae commit 3abec44

File tree

8 files changed

+218
-62
lines changed

8 files changed

+218
-62
lines changed

src/core/utilities.js

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,12 @@
11
import { window } from '../globals';
2-
import Logger from '../logger';
32

43
export const toString = Object.prototype.toString;
54
export const hasOwn = Object.prototype.hasOwnProperty;
65
export const slice = Array.prototype.slice;
76

8-
const nativePerf = getNativePerf();
9-
10-
// TODO: Consider using globalThis instead so that perf marks work
11-
// in Node.js as well. As they can have overhead, we should also
12-
// have a way to disable these, and/or make them an opt-in reporter
13-
// in QUnit 3 and then support globalThis.
14-
// For example: `QUnit.addReporter(QUnit.reporters.perf)`.
15-
function getNativePerf () {
16-
if (window &&
17-
typeof window.performance !== 'undefined' &&
18-
typeof window.performance.mark === 'function' &&
19-
typeof window.performance.measure === 'function'
20-
) {
21-
return window.performance;
22-
} else {
23-
return undefined;
24-
}
25-
}
26-
277
export const performance = {
28-
now: nativePerf
29-
? nativePerf.now.bind(nativePerf)
30-
: Date.now,
31-
measure: nativePerf
32-
? function (comment, startMark, endMark) {
33-
// `performance.measure` may fail if the mark could not be found.
34-
// reasons a specific mark could not be found include: outside code invoking `performance.clearMarks()`
35-
try {
36-
nativePerf.measure(comment, startMark, endMark);
37-
} catch (ex) {
38-
Logger.warn('performance.measure could not be executed because of ', ex.message);
39-
}
40-
}
41-
: function () {},
42-
mark: nativePerf ? nativePerf.mark.bind(nativePerf) : function () {}
8+
// eslint-disable-next-line compat/compat -- Checked
9+
now: window && window.performance && window.performance.now ? window.performance.now.bind(window.performance) : Date.now
4310
};
4411

4512
// Returns a new Array with the elements that are in a but not in b

src/html-reporter/html.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export function escapeText (str) {
3939
return;
4040
}
4141

42+
QUnit.reporters.perf.init(QUnit);
43+
4244
const config = QUnit.config;
4345
const hiddenTests = [];
4446
let collapseNext = false;

src/reporters.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import ConsoleReporter from './reporters/ConsoleReporter.js';
2+
import PerfReporter from './reporters/PerfReporter.js';
23
import TapReporter from './reporters/TapReporter.js';
34

45
export default {
56
console: ConsoleReporter,
7+
perf: PerfReporter,
68
tap: TapReporter
79
};

src/reporters/PerfReporter.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { window } from '../globals';
2+
import Logger from '../logger';
3+
4+
// TODO: Consider using globalThis instead of window, so that the reporter
5+
// works for Node.js as well. As this can add overhead, we should make
6+
// this opt-in before we enable it for CLI.
7+
//
8+
// QUnit 3 will switch from `window` to `globalThis` and then make it
9+
// no longer an implicit feature of the HTML Reporter, but rather let
10+
// it be opt-in via `QUnit.config.reporters = ['perf']` or something
11+
// like that.
12+
const nativePerf = (
13+
window &&
14+
typeof window.performance !== 'undefined' &&
15+
// eslint-disable-next-line compat/compat -- Checked
16+
typeof window.performance.mark === 'function' &&
17+
// eslint-disable-next-line compat/compat -- Checked
18+
typeof window.performance.measure === 'function'
19+
)
20+
? window.performance
21+
: undefined;
22+
23+
const perf = {
24+
measure: nativePerf
25+
? function (comment, startMark, endMark) {
26+
// `performance.measure` may fail if the mark could not be found.
27+
// reasons a specific mark could not be found include: outside code invoking `performance.clearMarks()`
28+
try {
29+
nativePerf.measure(comment, startMark, endMark);
30+
} catch (ex) {
31+
Logger.warn('performance.measure could not be executed because of ', ex.message);
32+
}
33+
}
34+
: function () {},
35+
mark: nativePerf ? nativePerf.mark.bind(nativePerf) : function () {}
36+
};
37+
38+
export default class PerfReporter {
39+
constructor (runner, options = {}) {
40+
this.perf = options.perf || perf;
41+
42+
runner.on('runStart', this.onRunStart.bind(this));
43+
runner.on('runEnd', this.onRunEnd.bind(this));
44+
runner.on('suiteStart', this.onSuiteStart.bind(this));
45+
runner.on('suiteEnd', this.onSuiteEnd.bind(this));
46+
runner.on('testStart', this.onTestStart.bind(this));
47+
runner.on('testEnd', this.onTestEnd.bind(this));
48+
}
49+
50+
static init (runner, options) {
51+
return new PerfReporter(runner, options);
52+
}
53+
54+
onRunStart () {
55+
this.perf.mark('qunit_suite_0_start');
56+
}
57+
58+
onSuiteStart (suiteStart) {
59+
const suiteLevel = suiteStart.fullName.length;
60+
this.perf.mark(`qunit_suite_${suiteLevel}_start`);
61+
}
62+
63+
onSuiteEnd (suiteEnd) {
64+
const suiteLevel = suiteEnd.fullName.length;
65+
const suiteName = suiteEnd.fullName.join(' – ');
66+
67+
this.perf.mark(`qunit_suite_${suiteLevel}_end`);
68+
this.perf.measure(`QUnit Test Suite: ${suiteName}`,
69+
`qunit_suite_${suiteLevel}_start`,
70+
`qunit_suite_${suiteLevel}_end`
71+
);
72+
}
73+
74+
onTestStart () {
75+
this.perf.mark('qunit_test_start');
76+
}
77+
78+
onTestEnd (testEnd) {
79+
this.perf.mark('qunit_test_end');
80+
const testName = testEnd.fullName.join(' – ');
81+
82+
this.perf.measure(`QUnit Test: ${testName}`,
83+
'qunit_test_start',
84+
'qunit_test_end'
85+
);
86+
}
87+
88+
onRunEnd () {
89+
this.perf.mark('qunit_suite_0_end');
90+
this.perf.measure('QUnit Test Run', 'qunit_suite_0_start', 'qunit_suite_0_end');
91+
}
92+
}

src/reports/suite.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ export default class SuiteReport {
2121
start (recordTime) {
2222
if (recordTime) {
2323
this._startTime = performance.now();
24-
25-
const suiteLevel = this.fullName.length;
26-
performance.mark(`qunit_suite_${suiteLevel}_start`);
2724
}
2825

2926
return {
@@ -40,16 +37,6 @@ export default class SuiteReport {
4037
end (recordTime) {
4138
if (recordTime) {
4239
this._endTime = performance.now();
43-
44-
const suiteLevel = this.fullName.length;
45-
const suiteName = this.fullName.join(' – ');
46-
47-
performance.mark(`qunit_suite_${suiteLevel}_end`);
48-
performance.measure(
49-
suiteLevel === 0 ? 'QUnit Test Run' : `QUnit Test Suite: ${suiteName}`,
50-
`qunit_suite_${suiteLevel}_start`,
51-
`qunit_suite_${suiteLevel}_end`
52-
);
5340
}
5441

5542
return {

src/reports/test.js

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export default class TestReport {
2222
start (recordTime) {
2323
if (recordTime) {
2424
this._startTime = performance.now();
25-
performance.mark('qunit_test_start');
2625
}
2726

2827
return {
@@ -35,17 +34,6 @@ export default class TestReport {
3534
end (recordTime) {
3635
if (recordTime) {
3736
this._endTime = performance.now();
38-
if (performance) {
39-
performance.mark('qunit_test_end');
40-
41-
const testName = this.fullName.join(' – ');
42-
43-
performance.measure(
44-
`QUnit Test: ${testName}`,
45-
'qunit_test_start',
46-
'qunit_test_end'
47-
);
48-
}
4937
}
5038

5139
return extend(this.start(), {

test/cli/PerfReporter.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
const { EventEmitter } = require('events');
2+
3+
class MockPerf {
4+
constructor () {
5+
this.marks = new Map();
6+
this.measures = [];
7+
this.clock = 1;
8+
}
9+
10+
mark (name) {
11+
this.clock++;
12+
this.marks.set(name, this.clock);
13+
}
14+
15+
measure (name, startMark, endMark) {
16+
const startTime = this.marks.get(startMark);
17+
const endTime = this.marks.get(endMark);
18+
this.measures.push({ name, startTime, endTime });
19+
this.measures.sort((a, b) => a.startTime - b.startTime);
20+
}
21+
}
22+
23+
QUnit.module('PerfReporter', hooks => {
24+
let emitter;
25+
let perf;
26+
27+
hooks.beforeEach(function () {
28+
emitter = new EventEmitter();
29+
perf = new MockPerf();
30+
QUnit.reporters.perf.init(emitter, {
31+
perf
32+
});
33+
});
34+
35+
QUnit.test('Flat suites', assert => {
36+
emitter.emit('runStart', {});
37+
emitter.emit('suiteStart', { fullName: ['Foo'] });
38+
emitter.emit('testStart', { fullName: ['Foo', 'example'] });
39+
emitter.emit('testEnd', { fullName: ['Foo', 'example'] });
40+
emitter.emit('suiteEnd', { fullName: ['Foo'] });
41+
emitter.emit('suiteStart', { fullName: ['Bar'] });
42+
emitter.emit('testStart', { fullName: ['Bar', 'example'] });
43+
emitter.emit('testEnd', { fullName: ['Bar', 'example'] });
44+
emitter.emit('suiteEnd', { fullName: ['Bar'] });
45+
emitter.emit('runEnd', {});
46+
47+
assert.deepEqual(
48+
perf.measures,
49+
[{
50+
name: 'QUnit Test Run',
51+
startTime: 2,
52+
endTime: 11
53+
},
54+
{
55+
name: 'QUnit Test Suite: Foo',
56+
startTime: 3,
57+
endTime: 6
58+
},
59+
{
60+
name: 'QUnit Test: Foo – example',
61+
startTime: 4,
62+
endTime: 5
63+
},
64+
{
65+
name: 'QUnit Test Suite: Bar',
66+
startTime: 7,
67+
endTime: 10
68+
},
69+
{
70+
name: 'QUnit Test: Bar – example',
71+
startTime: 8,
72+
endTime: 9
73+
}]
74+
);
75+
});
76+
77+
QUnit.test('Nested suites', assert => {
78+
emitter.emit('runStart', {});
79+
emitter.emit('suiteStart', { fullName: ['Foo'] });
80+
emitter.emit('testStart', { fullName: ['Foo', 'one'] });
81+
emitter.emit('testEnd', { fullName: ['Foo', 'one'] });
82+
emitter.emit('suiteStart', { fullName: ['Foo', 'Bar'] });
83+
emitter.emit('testStart', { fullName: ['Foo', 'Bar', 'two'] });
84+
emitter.emit('testEnd', { fullName: ['Foo', 'Bar', 'two'] });
85+
emitter.emit('suiteEnd', { fullName: ['Foo', 'Bar'] });
86+
emitter.emit('suiteEnd', { fullName: ['Fo'] });
87+
emitter.emit('runEnd', {});
88+
89+
assert.deepEqual(
90+
perf.measures,
91+
[{
92+
name: 'QUnit Test Run',
93+
startTime: 2,
94+
endTime: 11
95+
},
96+
{
97+
name: 'QUnit Test Suite: Fo',
98+
startTime: 3,
99+
endTime: 10
100+
},
101+
{
102+
name: 'QUnit Test: Foo – one',
103+
startTime: 4,
104+
endTime: 5
105+
},
106+
{
107+
name: 'QUnit Test Suite: Foo – Bar',
108+
startTime: 6,
109+
endTime: 9
110+
},
111+
{
112+
name: 'QUnit Test: Foo – Bar – two',
113+
startTime: 7,
114+
endTime: 8
115+
}]
116+
);
117+
});
118+
});

test/cli/fixtures/expected/tap-outputs.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,14 @@ ok 5 A-Test > derp
172172
'qunit --reporter does-not-exist':
173173
`# stderr
174174
No reporter found matching "does-not-exist".
175-
Built-in reporters: console, tap
175+
Built-in reporters: console, perf, tap
176176
Extra reporters found among package dependencies: npm-reporter
177177
178178
# exit code: 1`,
179179

180180
'qunit --reporter':
181181
`# stderr
182-
Built-in reporters: console, tap
182+
Built-in reporters: console, perf, tap
183183
Extra reporters found among package dependencies: npm-reporter
184184
185185
# exit code: 1`,

0 commit comments

Comments
 (0)