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:
Jordan Santell 2015-06-16 17:25:21 -07:00
parent 2593c16981
commit 980618b980
4 changed files with 312 additions and 14 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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;
}
});