Skip to content

Commit d7dfebc

Browse files
authored
fix(performanceTimer): work in frames (#4834)
Turns out our performanceTimer errors when running in iframes. I also added some detail to gather so we could see what rule is being gathered.
1 parent 3daa819 commit d7dfebc

File tree

3 files changed

+240
-34
lines changed

3 files changed

+240
-34
lines changed

lib/core/base/rule.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,9 @@ Rule.prototype._trackPerformance = function _trackPerformance() {
380380
* @param {Array} nodes Result of rule.gather
381381
*/
382382
Rule.prototype._logGatherPerformance = function _logGatherPerformance(nodes) {
383-
log('gather (', nodes.length, '):', performanceTimer.timeElapsed() + 'ms');
383+
log(
384+
`gather for ${this.id} (${nodes.length} nodes): ${performanceTimer.timeElapsed()}ms`
385+
);
384386
performanceTimer.mark(this._markChecksStart);
385387
};
386388

lib/core/utils/performance-timer.js

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ const performanceTimer = (() => {
1717
return window.performance.now();
1818
}
1919
}
20-
let originalTime = null;
21-
let lastRecordedTime = now();
20+
let axeStartTime = now();
21+
let axeStarted = false;
2222

2323
/**
2424
* @typedef {utils.performanceTimer} Public API Methods
@@ -28,98 +28,154 @@ const performanceTimer = (() => {
2828
* @member {Function} start Kicks off performance timing for overall axe audit
2929
*/
3030
start() {
31+
this.reset();
32+
axeStarted = true;
3133
this.mark('mark_axe_start');
3234
},
35+
3336
/**
3437
* @member {Function} end Concludes performance timing, compares start/end marks
3538
*/
3639
end() {
3740
this.mark('mark_axe_end');
38-
this.measure('axe', 'mark_axe_start', 'mark_axe_end');
39-
41+
this.measure('axe', 'mark_axe_start', 'mark_axe_end', true);
4042
this.logMeasures('axe');
43+
this.clearMark('mark_axe_start', 'mark_axe_end');
44+
axeStarted = false;
4145
},
46+
4247
/**
4348
* @member {Function} auditStart Starts an audit for a page or frame
4449
*/
4550
auditStart() {
51+
if (!axeStarted) {
52+
this.reset(); // We're in a frame
53+
}
4654
this.mark('mark_audit_start');
4755
},
56+
4857
/**
4958
* @member {Function} auditEnd Ends an audit for a page or frame, logs measurement of start/end marks
5059
*/
5160
auditEnd() {
5261
this.mark('mark_audit_end');
53-
this.measure('audit_start_to_end', 'mark_audit_start', 'mark_audit_end');
62+
this.measure(
63+
'audit_start_to_end',
64+
'mark_audit_start',
65+
'mark_audit_end',
66+
true
67+
);
5468
// log audit/rule measures
5569
this.logMeasures();
70+
this.clearMark('mark_audit_start', 'mark_audit_end');
5671
},
72+
5773
/**
5874
* @member {Function} mark Shims creating a new named time stamp, called a mark
5975
* @param {String} markName String name to record how long it took to get there.
6076
* A mark that already exists will be overwritten.
6177
*
6278
*/
6379
mark(markName) {
64-
if (window.performance && window.performance.mark !== undefined) {
80+
if (window.performance?.mark) {
6581
window.performance.mark(markName);
6682
}
6783
},
84+
6885
/**
6986
* @member {Function} measure Shims creating a measure to compare two marks, which can be logged
7087
* @param {String} measureName String name to log what is being compared.
7188
* Measures that already exist will be overwritten with a new time stamp.
7289
* @param {String} startMark String name of mark to start measuring
7390
* @param {String} endMark String name of mark to end measuring
7491
*/
75-
measure(measureName, startMark, endMark) {
76-
if (window.performance && window.performance.measure !== undefined) {
77-
window.performance.measure(measureName, startMark, endMark);
92+
measure(measureName, startMark, endMark, keepMarks = false) {
93+
if (!window.performance?.measure) {
94+
return;
95+
}
96+
window.performance.measure(measureName, startMark, endMark);
97+
if (!keepMarks) {
98+
this.clearMark(startMark, endMark);
7899
}
79100
},
101+
80102
/**
81-
* @member {Function} logMeasures Iterates through measures and logs any that exist
103+
* @member {Function} logMeasures Logs previously captured measures in chronological order.
104+
* Starts from the most recent start()/auditStart() call.
105+
* @param {String|undefined} measureName If provided, will only log up to the first matching measure.
82106
*/
83107
logMeasures(measureName) {
84-
function logMeasure(req) {
85-
log('Measure ' + req.name + ' took ' + req.duration + 'ms');
86-
}
108+
const last = arr => (Array.isArray(arr) ? arr[arr.length - 1] : arr);
109+
const logMeasure = req => {
110+
this._log('Measure ' + req.name + ' took ' + req.duration + 'ms');
111+
};
87112
if (
88-
window.performance &&
89-
window.performance.getEntriesByType !== undefined
113+
!window.performance?.getEntriesByType ||
114+
!window.performance?.getEntriesByName
90115
) {
91-
// only output measures that were started after axe started, otherwise
92-
// we get measures made by the page before axe ran (which is confusing)
93-
const axeStart =
94-
window.performance.getEntriesByName('mark_axe_start')[0];
95-
const measures = window.performance
96-
.getEntriesByType('measure')
97-
.filter(measure => measure.startTime >= axeStart.startTime);
98-
for (let i = 0; i < measures.length; ++i) {
99-
const req = measures[i];
100-
if (req.name === measureName) {
101-
logMeasure(req);
102-
return;
103-
}
116+
return;
117+
}
118+
// only output measures that were started after axe started, otherwise
119+
// we get measures made by the page before axe ran (which is confusing)
120+
const axeStart =
121+
last(window.performance.getEntriesByName('mark_axe_start')) ||
122+
last(window.performance.getEntriesByName('mark_audit_start'));
123+
if (!axeStart) {
124+
this._log('Axe must be started before using performanceTimer');
125+
return;
126+
}
127+
128+
const measures = window.performance
129+
.getEntriesByType('measure')
130+
.filter(measure => measure.startTime >= axeStart.startTime);
131+
for (let i = 0; i < measures.length; ++i) {
132+
const req = measures[i];
133+
if (req.name === measureName) {
134+
logMeasure(req);
135+
return;
136+
} else if (!measureName) {
104137
logMeasure(req);
105138
}
106139
}
107140
},
141+
108142
/**
109143
* @member {Function} timeElapsed Records time since last audit
110144
* @return {DOMTimeStamp}
111145
*/
112146
timeElapsed() {
113-
return now() - lastRecordedTime;
147+
const currentTime = now();
148+
return currentTime - axeStartTime;
149+
},
150+
151+
/**
152+
* @member {Function} clearMark Clears a mark
153+
* @param {String} markName String name of mark to clear
154+
*/
155+
clearMark(...markNames) {
156+
if (!window.performance?.clearMarks) {
157+
return;
158+
}
159+
160+
for (const markName of markNames) {
161+
window.performance.clearMarks(markName);
162+
}
114163
},
164+
115165
/**
116166
* @member {Function} reset Resets the time for a new audit
117167
*/
118168
reset() {
119-
if (!originalTime) {
120-
originalTime = now();
121-
}
122-
lastRecordedTime = now();
169+
axeStartTime = now();
170+
},
171+
172+
/**
173+
* Logs the message, available to override in testing
174+
* @private
175+
* @param {String} message
176+
*/
177+
_log(message) {
178+
log(message);
123179
}
124180
};
125181
})();
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
describe('performance timer', () => {
2+
const { performanceTimer } = axe.utils;
3+
const originalLog = performanceTimer._log;
4+
let messages = [];
5+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
6+
7+
const getNumber = msg => {
8+
const match = msg.match(/([0-9.]+)ms/);
9+
return match ? parseFloat(match[1]) : 0;
10+
};
11+
12+
beforeEach(() => {
13+
performanceTimer._log = msg => {
14+
messages.push(msg);
15+
};
16+
});
17+
18+
afterEach(async () => {
19+
try {
20+
performanceTimer.end();
21+
} catch {
22+
// ignore - performanceTimer.end() was called in a test instead
23+
}
24+
try {
25+
performanceTimer.auditEnd();
26+
} catch {
27+
// ignore - performanceTimer.auditEnd() was called in a test instead
28+
}
29+
performanceTimer._log = originalLog;
30+
messages = [];
31+
// Force a next tick in the test to avoid flakiness
32+
await sleep(0);
33+
});
34+
35+
it('warns using performanceTimer before axe started', () => {
36+
performanceTimer.mark('foo_start');
37+
performanceTimer.mark('foo_end');
38+
performanceTimer.measure('foo', 'foo_start', 'foo_end');
39+
performanceTimer.logMeasures('foo');
40+
assert.equal(messages.length, 1);
41+
assert.match(
42+
messages[0],
43+
/Axe must be started before using performanceTimer/
44+
);
45+
});
46+
47+
it('measures time elapsed between marks', async () => {
48+
performanceTimer.start();
49+
performanceTimer.mark('foo_start');
50+
await sleep(100);
51+
performanceTimer.mark('foo_end');
52+
performanceTimer.measure('foo', 'foo_start', 'foo_end');
53+
performanceTimer.logMeasures('foo');
54+
55+
assert.equal(messages.length, 1);
56+
assert.closeTo(getNumber(messages[0]), 100, 10);
57+
});
58+
59+
it('measures time elapsed if auditStart() was called', () => {
60+
performanceTimer.auditStart();
61+
performanceTimer.mark('foo_start');
62+
performanceTimer.mark('foo_end');
63+
performanceTimer.measure('foo', 'foo_start', 'foo_end');
64+
performanceTimer.logMeasures('foo');
65+
66+
assert.equal(messages.length, 1);
67+
assert.match(messages[0], /Measure foo took [0-9.]+ms/);
68+
});
69+
70+
it('measures the time axe takes from .start() to .end()', () => {
71+
performanceTimer.start();
72+
performanceTimer.end();
73+
assert.equal(messages.length, 1);
74+
assert.match(messages[0], /Measure axe took [0-9.]+ms/);
75+
});
76+
77+
it('measures the time axe takes from .auditStart() to .auditEnd()', () => {
78+
performanceTimer.auditStart();
79+
performanceTimer.auditEnd();
80+
assert.equal(messages.length, 1);
81+
assert.match(messages[0], /Measure audit_start_to_end took [0-9.]+ms/);
82+
});
83+
84+
describe('logMeasures', () => {
85+
beforeEach(() => {
86+
performanceTimer.start();
87+
});
88+
89+
it('logs a specific measure when provided', () => {
90+
performanceTimer.mark('foo_start');
91+
performanceTimer.mark('foo_end');
92+
performanceTimer.measure('foo', 'foo_start', 'foo_end');
93+
94+
performanceTimer.mark('bar_start');
95+
performanceTimer.mark('bar_end');
96+
performanceTimer.measure('bar', 'bar_start', 'bar_end');
97+
98+
// Mot measured, should not show up
99+
performanceTimer.mark('baz_start');
100+
performanceTimer.mark('baz_end');
101+
102+
performanceTimer.logMeasures('foo');
103+
assert.equal(messages.length, 1);
104+
assert.match(messages[0], /Measure foo took [0-9.]+ms/);
105+
});
106+
107+
it('logs all measures if no measure name is provided', () => {
108+
performanceTimer.mark('foo_start');
109+
performanceTimer.mark('foo_end');
110+
performanceTimer.measure('foo', 'foo_start', 'foo_end');
111+
112+
performanceTimer.mark('bar_start');
113+
performanceTimer.mark('bar_end');
114+
performanceTimer.measure('bar', 'bar_start', 'bar_end');
115+
116+
// Mot measured, should not show up
117+
performanceTimer.mark('baz_start');
118+
performanceTimer.mark('baz_end');
119+
120+
performanceTimer.logMeasures();
121+
assert.equal(messages.length, 2);
122+
assert.match(messages[0], /Measure foo took [0-9.]+ms/);
123+
assert.match(messages[1], /Measure bar took [0-9.]+ms/);
124+
});
125+
});
126+
127+
describe('timeElapsed', () => {
128+
it('returns the time elapsed since axe started', async () => {
129+
performanceTimer.start();
130+
await sleep(100);
131+
assert.closeTo(performanceTimer.timeElapsed(), 100, 10);
132+
});
133+
134+
it('returns the time elapsed since auditStart() was called', async () => {
135+
performanceTimer.auditStart();
136+
await sleep(100);
137+
assert.closeTo(performanceTimer.timeElapsed(), 100, 10);
138+
});
139+
140+
it('does not use auditStart if axe started', async () => {
141+
performanceTimer.start();
142+
await sleep(100);
143+
144+
performanceTimer.auditStart();
145+
assert.closeTo(performanceTimer.timeElapsed(), 100, 10);
146+
});
147+
});
148+
});

0 commit comments

Comments
 (0)