mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-26 11:45:37 +00:00
Bug 1174889 - Record optimization tiers over time in FrameNodes, and create a utility function for converting the tier data to plottable points in a stacked mountain graph.
This commit is contained in:
parent
2593c16981
commit
980618b980
@ -259,6 +259,7 @@ function getOrAddInflatedFrame(cache, index, frameTable, stringTable, allocation
|
||||
*/
|
||||
function InflatedFrame(index, frameTable, stringTable, allocationsTable) {
|
||||
const LOCATION_SLOT = frameTable.schema.location;
|
||||
const IMPLEMENTATION_SLOT = frameTable.schema.implementation;
|
||||
const OPTIMIZATIONS_SLOT = frameTable.schema.optimizations;
|
||||
const LINE_SLOT = frameTable.schema.line;
|
||||
const CATEGORY_SLOT = frameTable.schema.category;
|
||||
@ -266,6 +267,7 @@ function InflatedFrame(index, frameTable, stringTable, allocationsTable) {
|
||||
let frame = frameTable.data[index];
|
||||
let category = frame[CATEGORY_SLOT];
|
||||
this.location = stringTable[frame[LOCATION_SLOT]];
|
||||
this.implementation = frame[IMPLEMENTATION_SLOT];
|
||||
this.optimizations = frame[OPTIMIZATIONS_SLOT];
|
||||
this.line = frame[LINE_SLOT];
|
||||
this.column = undefined;
|
||||
|
@ -207,7 +207,7 @@ const JITOptimizations = function (rawSites, stringTable) {
|
||||
};
|
||||
}
|
||||
|
||||
this.optimizationSites = sites.sort((a, b) => b.samples - a.samples);;
|
||||
this.optimizationSites = sites.sort((a, b) => b.samples - a.samples);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -252,5 +252,94 @@ function maybeTypeset(typeset, stringTable) {
|
||||
});
|
||||
}
|
||||
|
||||
// Map of optimization implementation names to an enum.
|
||||
const IMPLEMENTATION_MAP = {
|
||||
"interpreter": 0,
|
||||
"baseline": 1,
|
||||
"ion": 2
|
||||
};
|
||||
const IMPLEMENTATION_NAMES = Object.keys(IMPLEMENTATION_MAP);
|
||||
|
||||
/**
|
||||
* Takes data from a FrameNode and computes rendering positions for
|
||||
* a stacked mountain graph, to visualize JIT optimization tiers over time.
|
||||
*
|
||||
* @param {FrameNode} frameNode
|
||||
* The FrameNode who's optimizations we're iterating.
|
||||
* @param {Array<number>} sampleTimes
|
||||
* An array of every sample time within the range we're counting.
|
||||
* From a ThreadNode's `sampleTimes` property.
|
||||
* @param {number} op.startTime
|
||||
* The start time of the first sample.
|
||||
* @param {number} op.endTime
|
||||
* The end time of the last sample.
|
||||
* @param {number} op.resolution
|
||||
* The maximum amount of possible data points returned.
|
||||
* Also determines the size in milliseconds of each bucket
|
||||
* via `(endTime - startTime) / resolution`
|
||||
* @return {?Array<object>}
|
||||
*/
|
||||
function createTierGraphDataFromFrameNode (frameNode, sampleTimes, { startTime, endTime, resolution }) {
|
||||
if (!frameNode.hasOptimizations()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tierData = frameNode.getOptimizationTierData();
|
||||
let duration = endTime - startTime;
|
||||
let stringTable = frameNode._stringTable;
|
||||
let output = [];
|
||||
let implEnum;
|
||||
|
||||
let tierDataIndex = 0;
|
||||
let nextOptSample = tierData[tierDataIndex];
|
||||
|
||||
// Bucket data
|
||||
let samplesInCurrentBucket = 0;
|
||||
let currentBucketStartTime = sampleTimes[0];
|
||||
let bucket = [];
|
||||
// Size of each bucket in milliseconds
|
||||
let bucketSize = Math.ceil(duration / resolution);
|
||||
|
||||
// Iterate one after the samples, so we can finalize the last bucket
|
||||
for (let i = 0; i <= sampleTimes.length; i++) {
|
||||
let sampleTime = sampleTimes[i];
|
||||
|
||||
// If this sample is in the next bucket, or we're done
|
||||
// checking sampleTimes and on the last iteration, finalize previous bucket
|
||||
if (sampleTime >= (currentBucketStartTime + bucketSize) ||
|
||||
i >= sampleTimes.length) {
|
||||
|
||||
let dataPoint = {};
|
||||
dataPoint.ys = [];
|
||||
dataPoint.x = currentBucketStartTime;
|
||||
|
||||
// Map the opt site counts as a normalized percentage (0-1)
|
||||
// of its count in context of total samples this bucket
|
||||
for (let j = 0; j < IMPLEMENTATION_NAMES.length; j++) {
|
||||
dataPoint.ys[j] = (bucket[j] || 0) / (samplesInCurrentBucket || 1);
|
||||
}
|
||||
output.push(dataPoint);
|
||||
|
||||
// Set the new start time of this bucket and reset its count
|
||||
currentBucketStartTime += bucketSize;
|
||||
samplesInCurrentBucket = 0;
|
||||
bucket = [];
|
||||
}
|
||||
|
||||
// If this sample observed an optimization in this frame, record it
|
||||
if (nextOptSample && nextOptSample.time === sampleTime) {
|
||||
// If no implementation defined, it was the "interpreter".
|
||||
implEnum = IMPLEMENTATION_MAP[stringTable[nextOptSample.implementation] || "interpreter"];
|
||||
bucket[implEnum] = (bucket[implEnum] || 0) + 1;
|
||||
nextOptSample = tierData[++tierDataIndex];
|
||||
}
|
||||
|
||||
samplesInCurrentBucket++;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
exports.createTierGraphDataFromFrameNode = createTierGraphDataFromFrameNode;
|
||||
exports.OptimizationSite = OptimizationSite;
|
||||
exports.JITOptimizations = JITOptimizations;
|
||||
|
@ -35,6 +35,7 @@ function ThreadNode(thread, options = {}) {
|
||||
throw new Error("ThreadNode requires both `startTime` and `endTime`.");
|
||||
}
|
||||
this.samples = 0;
|
||||
this.sampleTimes = [];
|
||||
this.youngestFrameSamples = 0;
|
||||
this.calls = [];
|
||||
this.duration = options.endTime - options.startTime;
|
||||
@ -131,11 +132,6 @@ ThreadNode.prototype = {
|
||||
let endTime = options.endTime;
|
||||
let flattenRecursion = options.flattenRecursion;
|
||||
|
||||
// Take the timestamp of the first sample as prevSampleTime. 0 is
|
||||
// incorrect due to circular buffer wraparound. If wraparound happens,
|
||||
// then the first sample will have an incorrect, large duration.
|
||||
let prevSampleTime = samplesData[0][SAMPLE_TIME_SLOT];
|
||||
|
||||
// Reused options object passed to InflatedFrame.prototype.getFrameKey.
|
||||
let mutableFrameKeyOptions = {
|
||||
contentOnly: options.contentOnly,
|
||||
@ -144,9 +140,7 @@ ThreadNode.prototype = {
|
||||
isMetaCategoryOut: false
|
||||
};
|
||||
|
||||
// Start iteration at the second sample, as we use the first sample to
|
||||
// compute prevSampleTime.
|
||||
for (let i = 1; i < samplesData.length; i++) {
|
||||
for (let i = 0; i < samplesData.length; i++) {
|
||||
let sample = samplesData[i];
|
||||
let sampleTime = sample[SAMPLE_TIME_SLOT];
|
||||
|
||||
@ -156,7 +150,6 @@ ThreadNode.prototype = {
|
||||
// Thus, we compare sampleTime <= start instead of < to filter out
|
||||
// samples that end exactly at the start time.
|
||||
if (!sampleTime || sampleTime <= startTime || sampleTime > endTime) {
|
||||
prevSampleTime = sampleTime;
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -235,7 +228,10 @@ ThreadNode.prototype = {
|
||||
leafTable);
|
||||
if (isLeaf) {
|
||||
frameNode.youngestFrameSamples++;
|
||||
frameNode._addOptimizations(inflatedFrame.optimizations, stringTable);
|
||||
if (inflatedFrame.optimizations) {
|
||||
frameNode._addOptimizations(inflatedFrame.optimizations, inflatedFrame.implementation,
|
||||
sampleTime, stringTable);
|
||||
}
|
||||
}
|
||||
frameNode.samples++;
|
||||
|
||||
@ -245,6 +241,7 @@ ThreadNode.prototype = {
|
||||
}
|
||||
|
||||
this.samples++;
|
||||
this.sampleTimes.push(sampleTime);
|
||||
}
|
||||
},
|
||||
|
||||
@ -372,6 +369,7 @@ function FrameNode(frameKey, { location, line, category, allocations, isContent
|
||||
this.calls = [];
|
||||
this.isContent = !!isContent;
|
||||
this._optimizations = null;
|
||||
this._tierData = null;
|
||||
this._stringTable = null;
|
||||
this.isMetaCategory = !!isMetaCategory;
|
||||
this.category = category;
|
||||
@ -384,19 +382,30 @@ FrameNode.prototype = {
|
||||
* @param object optimizationSite
|
||||
* Any JIT optimization information attached to the current
|
||||
* sample. Lazily inflated via stringTable.
|
||||
* @param number implementation
|
||||
* JIT implementation used for this observed frame (interpreter,
|
||||
* baseline, ion);
|
||||
* @param number time
|
||||
* The time this optimization occurred.
|
||||
* @param object stringTable
|
||||
* The string table used to inflate the optimizationSite.
|
||||
*/
|
||||
_addOptimizations: function (optimizationSite, stringTable) {
|
||||
_addOptimizations: function (site, implementation, time, stringTable) {
|
||||
// Simply accumulate optimization sites for now. Processing is done lazily
|
||||
// by JITOptimizations, if optimization information is actually displayed.
|
||||
if (optimizationSite) {
|
||||
if (site) {
|
||||
let opts = this._optimizations;
|
||||
if (opts === null) {
|
||||
opts = this._optimizations = [];
|
||||
this._stringTable = stringTable;
|
||||
}
|
||||
opts.push(optimizationSite);
|
||||
opts.push(site);
|
||||
|
||||
if (this._tierData === null) {
|
||||
this._tierData = [];
|
||||
}
|
||||
// Record type of implementation used and the sample time
|
||||
this._tierData.push({ implementation, time });
|
||||
}
|
||||
},
|
||||
|
||||
@ -475,6 +484,18 @@ FrameNode.prototype = {
|
||||
}
|
||||
return new JITOptimizations(this._optimizations, this._stringTable);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the optimization tiers used overtime.
|
||||
*
|
||||
* @return {?Array<object>}
|
||||
*/
|
||||
getOptimizationTierData: function () {
|
||||
if (!this._tierData) {
|
||||
return null;
|
||||
}
|
||||
return this._tierData;
|
||||
}
|
||||
};
|
||||
|
||||
exports.ThreadNode = ThreadNode;
|
||||
|
186
browser/devtools/performance/test/unit/test_jit-graph-data.js
Normal file
186
browser/devtools/performance/test/unit/test_jit-graph-data.js
Normal file
@ -0,0 +1,186 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Unit test for `createTierGraphDataFromFrameNode` function.
|
||||
*/
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const SAMPLE_COUNT = 1000;
|
||||
const RESOLUTION = 50;
|
||||
const TIME_PER_SAMPLE = 5;
|
||||
|
||||
// Offset needed since ThreadNode requires the first sample to be strictly
|
||||
// greater than its start time. This lets us still have pretty numbers
|
||||
// in this test to keep it (more) simple, which it sorely needs.
|
||||
const TIME_OFFSET = 5;
|
||||
|
||||
add_task(function test() {
|
||||
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
|
||||
let { createTierGraphDataFromFrameNode } = devtools.require("devtools/performance/jit");
|
||||
|
||||
// Select the second half of the set of samples
|
||||
let startTime = (SAMPLE_COUNT / 2 * TIME_PER_SAMPLE) - TIME_OFFSET;
|
||||
let endTime = (SAMPLE_COUNT * TIME_PER_SAMPLE) - TIME_OFFSET;
|
||||
let invertTree = true;
|
||||
|
||||
let root = new ThreadNode(gThread, { invertTree, startTime, endTime });
|
||||
|
||||
equal(root.samples, SAMPLE_COUNT / 2, "root has correct amount of samples");
|
||||
equal(root.sampleTimes.length, SAMPLE_COUNT / 2, "root has correct amount of sample times");
|
||||
// Add time offset since the first sample begins TIME_OFFSET after startTime
|
||||
equal(root.sampleTimes[0], startTime + TIME_OFFSET, "root recorded first sample time in scope");
|
||||
equal(root.sampleTimes[root.sampleTimes.length - 1], endTime, "root recorded last sample time in scope");
|
||||
|
||||
let frame = getFrameNodePath(root, "X");
|
||||
let data = createTierGraphDataFromFrameNode(frame, root.sampleTimes, { startTime, endTime, resolution: RESOLUTION });
|
||||
|
||||
let TIME_PER_WINDOW = SAMPLE_COUNT / 2 / RESOLUTION * TIME_PER_SAMPLE;
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
equal(data[i].x, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "first window has correct x");
|
||||
equal(data[i].ys[0], 0.2, "first window has 2 frames in interpreter");
|
||||
equal(data[i].ys[1], 0.2, "first window has 2 frames in baseline");
|
||||
equal(data[i].ys[2], 0.2, "first window has 2 frames in ion");
|
||||
}
|
||||
for (let i = 10; i < 20; i++) {
|
||||
equal(data[i].x, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "second window has correct x");
|
||||
equal(data[i].ys[0], 0, "second window observed no optimizations");
|
||||
equal(data[i].ys[1], 0, "second window observed no optimizations");
|
||||
equal(data[i].ys[2], 0, "second window observed no optimizations");
|
||||
}
|
||||
for (let i = 20; i < 30; i++) {
|
||||
equal(data[i].x, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i), "third window has correct x");
|
||||
equal(data[i].ys[0], 0.3, "third window has 3 frames in interpreter");
|
||||
equal(data[i].ys[1], 0, "third window has 0 frames in baseline");
|
||||
equal(data[i].ys[2], 0, "third window has 0 frames in ion");
|
||||
}
|
||||
});
|
||||
|
||||
let gUniqueStacks = new RecordingUtils.UniqueStacks();
|
||||
|
||||
function uniqStr(s) {
|
||||
return gUniqueStacks.getOrAddStringIndex(s);
|
||||
}
|
||||
|
||||
const TIER_PATTERNS = [
|
||||
// 0-99
|
||||
["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
|
||||
// 100-199
|
||||
["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
|
||||
// 200-299
|
||||
["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
|
||||
// 300-399
|
||||
["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
|
||||
// 400-499
|
||||
["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
|
||||
|
||||
// 500-599
|
||||
// Test current frames in all opts, including that
|
||||
// the same frame with no opts does not get counted
|
||||
["X", "X", "A", "A", "X_1", "X_2", "X_1", "X_2", "X_0", "X_0"],
|
||||
|
||||
// 600-699
|
||||
// Nothing for current frame
|
||||
["A", "B", "A", "B", "A", "B", "A", "B", "A", "B"],
|
||||
|
||||
// 700-799
|
||||
// A few frames where the frame is not the leaf node
|
||||
["X_2 -> Y", "X_2 -> Y", "X_2 -> Y", "X_0", "X_0", "X_0", "A", "A", "A", "A"],
|
||||
|
||||
// 800-899
|
||||
["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
|
||||
// 900-999
|
||||
["X", "X", "X", "X", "X", "X", "X", "X", "X", "X"],
|
||||
];
|
||||
|
||||
function createSample (i, frames) {
|
||||
let sample = {};
|
||||
sample.time = i * TIME_PER_SAMPLE;
|
||||
sample.frames = [{ location: "(root)" }];
|
||||
if (i === 0) {
|
||||
return sample;
|
||||
}
|
||||
if (frames) {
|
||||
frames.split(" -> ").forEach(frame => sample.frames.push({ location: frame }));
|
||||
}
|
||||
return sample;
|
||||
}
|
||||
|
||||
let SAMPLES = (function () {
|
||||
let samples = [];
|
||||
|
||||
for (let i = 0; i < SAMPLE_COUNT;) {
|
||||
let pattern = TIER_PATTERNS[Math.floor(i/100)];
|
||||
for (let j = 0; j < pattern.length; j++) {
|
||||
samples.push(createSample(i+j, pattern[j]));
|
||||
}
|
||||
i += 10;
|
||||
}
|
||||
|
||||
return samples;
|
||||
})();
|
||||
|
||||
let gThread = RecordingUtils.deflateThread({ samples: SAMPLES, markers: [] }, gUniqueStacks);
|
||||
|
||||
let gRawSite1 = {
|
||||
line: 12,
|
||||
column: 2,
|
||||
types: [{
|
||||
mirType: uniqStr("Object"),
|
||||
site: uniqStr("B (http://foo/bar:10)"),
|
||||
typeset: [{
|
||||
keyedBy: uniqStr("constructor"),
|
||||
name: uniqStr("Foo"),
|
||||
location: uniqStr("B (http://foo/bar:10)")
|
||||
}, {
|
||||
keyedBy: uniqStr("primitive"),
|
||||
location: uniqStr("self-hosted")
|
||||
}]
|
||||
}],
|
||||
attempts: {
|
||||
schema: {
|
||||
outcome: 0,
|
||||
strategy: 1
|
||||
},
|
||||
data: [
|
||||
[uniqStr("Failure1"), uniqStr("SomeGetter1")],
|
||||
[uniqStr("Failure2"), uniqStr("SomeGetter2")],
|
||||
[uniqStr("Inlined"), uniqStr("SomeGetter3")]
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function serialize (x) {
|
||||
return JSON.parse(JSON.stringify(x));
|
||||
}
|
||||
|
||||
gThread.frameTable.data.forEach((frame) => {
|
||||
const LOCATION_SLOT = gThread.frameTable.schema.location;
|
||||
const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
|
||||
const IMPLEMENTATION_SLOT = gThread.frameTable.schema.implementation;
|
||||
|
||||
let l = gThread.stringTable[frame[LOCATION_SLOT]];
|
||||
switch (l) {
|
||||
// Rename some of the location sites so we can register different
|
||||
// frames with different opt sites
|
||||
case "X_0":
|
||||
frame[LOCATION_SLOT] = uniqStr("X");
|
||||
frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
|
||||
frame[IMPLEMENTATION_SLOT] = null;
|
||||
break;
|
||||
case "X_1":
|
||||
frame[LOCATION_SLOT] = uniqStr("X");
|
||||
frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
|
||||
frame[IMPLEMENTATION_SLOT] = uniqStr("baseline");
|
||||
break;
|
||||
case "X_2":
|
||||
frame[LOCATION_SLOT] = uniqStr("X");
|
||||
frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
|
||||
frame[IMPLEMENTATION_SLOT] = uniqStr("ion");
|
||||
break;
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue
Block a user