mapbox-gl
Version:
A WebGL interactive maps library
169 lines (147 loc) • 6.22 kB
JavaScript
// @flow
import window from '../util/window.js';
import type {RequestParameters} from '../util/ajax.js';
const performance = window.performance;
export type PerformanceMetrics = {
loadTime: number,
fullLoadTime: number,
fps: number,
percentDroppedFrames: number,
parseTile: number,
parseTile1: number,
parseTile2: number,
workerTask: number,
workerInitialization: number,
workerEvaluateScript: number,
workerIdle: number,
workerIdlePercent: number
};
export const PerformanceMarkers = {
create: 'create',
load: 'load',
fullLoad: 'fullLoad'
};
let lastFrameTime = null;
let frameTimes = [];
const frameSequences = [frameTimes];
let i = 0;
// The max milliseconds we should spend to render a single frame.
// This value may need to be tweaked. I chose 14 by increasing frame
// times with busy work and measuring the number of dropped frames.
// On a page with only a map, more frames started being dropped after
// going above 14ms. We might want to lower this to leave more room
// for other work.
const CPU_FRAME_BUDGET = 14;
const framerateTarget = 60;
const frameTimeTarget = 1000 / framerateTarget;
export const PerformanceUtils = {
mark(marker: $Keys<typeof PerformanceMarkers>) {
performance.mark(marker);
},
measure(name: string, begin?: string, end?: string) {
performance.measure(name, begin, end);
},
beginMeasure(name: string) {
const mark = name + i++;
performance.mark(mark);
return {
mark,
name
};
},
endMeasure(m: { name: string, mark: string }) {
performance.measure(m.name, m.mark);
},
frame(timestamp: number, isRenderFrame: boolean) {
const currTimestamp = timestamp;
if (lastFrameTime != null) {
const frameTime = currTimestamp - lastFrameTime;
frameTimes.push(frameTime);
}
if (isRenderFrame) {
lastFrameTime = currTimestamp;
} else {
lastFrameTime = null;
frameTimes = [];
frameSequences.push(frameTimes);
}
},
clearMetrics() {
lastFrameTime = null;
frameTimes = [];
performance.clearMeasures('loadTime');
performance.clearMeasures('fullLoadTime');
for (const marker in PerformanceMarkers) {
performance.clearMarks(PerformanceMarkers[marker]);
}
},
getPerformanceMetrics(): PerformanceMetrics {
const metrics = {};
performance.measure('loadTime', PerformanceMarkers.create, PerformanceMarkers.load);
performance.measure('fullLoadTime', PerformanceMarkers.create, PerformanceMarkers.fullLoad);
const measures = performance.getEntriesByType('measure');
for (const measure of measures) {
metrics[measure.name] = (metrics[measure.name] || 0) + measure.duration;
}
// We don't have a perfect way of measuring the actual number of dropped frames.
// The best way of determining when frames happen is the timestamp passed to
// requestAnimationFrame. In Chrome and Firefox the timestamps are generally
// multiples of 1000/60ms (+-2ms).
//
// The differences between the timestamps vary a lot more in Safari.
// It's not uncommon to see a 24ms difference followedd by a 8ms difference.
// I'm not sure, but I think these might not be dropped frames (due to multiple
// buffering?).
//
// For Safari, I think comparing the number of expected frames with the number of actual
// frames is a more accurate way of measuring dropped frames than comparing
// individual frame time differences to a target time. In Firefox and Chrome
// both approaches produce the same result most of the time.
let droppedFrames = 0;
let totalFrameTimeSum = 0;
let totalFrames = 0;
metrics.jank = 0;
for (const frameTimes of frameSequences) {
if (!frameTimes.length) continue;
const frameTimeSum = frameTimes.reduce((prev, curr) => prev + curr, 0);
const expectedFrames = Math.max(1, Math.round(frameTimeSum / frameTimeTarget));
droppedFrames += expectedFrames - frameTimes.length;
totalFrameTimeSum += frameTimeSum;
totalFrames += frameTimes.length;
// Jank is a change in the frame rate.
// Count the number of times a frame has a worse rate than the previous frame.
// A consistent rate does not increase jank even if it is continuosly dropping frames.
// A one-off frame does not increase jank even if it is really long.
//
// This is not that accurate in Safari because the differences between animation frame
// times is not as close to a multiple of 1000/60ms.
const roundedTimes = frameTimes.map(frameTime => Math.max(1, Math.round(frameTime / frameTimeTarget)));
for (let n = 0; n < roundedTimes.length - 1; n++) {
if (roundedTimes[n + 1] > roundedTimes[n]) {
metrics.jank++;
}
}
}
const avgFrameTime = totalFrameTimeSum / totalFrames / 1000;
metrics.fps = 1 / avgFrameTime;
metrics.droppedFrames = droppedFrames;
metrics.percentDroppedFrames = (droppedFrames / (totalFrames + droppedFrames)) * 100;
metrics.cpuFrameBudgetExceeded = 0;
const renderFrames = performance.getEntriesByName('render');
for (const renderFrame of renderFrames) {
metrics.cpuFrameBudgetExceeded += Math.max(0, renderFrame.duration - CPU_FRAME_BUDGET);
}
return metrics;
},
getWorkerPerformanceMetrics() {
return JSON.parse(JSON.stringify({
timeOrigin: performance.timeOrigin,
measures: performance.getEntriesByType("measure")
}));
}
};
export function getPerformanceMeasurement(request: ?RequestParameters) {
const url = request ? request.url.toString() : undefined;
return performance.getEntriesByName(url);
}
export default performance;