Commit 79d7764f authored by Alexei Filippov's avatar Alexei Filippov Committed by Commit Bot

DevTools: Fix missing samples in CPU profile.

When v8 is unable to unwind a JS stack trace it emits a sample
with an empty stack. That causes DT frontend to show gaps in stack frames.

The patch workarounds these by replacing a missing sample with a neighbor one.

It basically reverts https://chromium.googlesource.com/chromium/src/+/357c6a05e490f5e187dbad88aea96722a72c7446

Change-Id: I16f21a06bb33db160213319d38243c77ea497b09
Reviewed-on: https://chromium-review.googlesource.com/967466
Commit-Queue: Alexei Filippov <alph@chromium.org>
Reviewed-by: default avatarAndrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/master@{#544252}
parent b90dd1ad
Tests missing samples are replaced with neighbor stacks.
Profile tree:
(root) id:1 total:16 self:0 depth:-1
(garbage collector) id:2 total:3.2 self:3.2 depth:0
(program) id:3 total:3.2 self:3.2 depth:0
bar id:4 total:6.4 self:3.2 depth:0
foo id:6 total:3.2 self:3.2 depth:1
baz id:5 total:3.2 self:3.2 depth:0
raw samples: 3 4 3 4 3 6 2 2 3 6 3 3 6 5 3 6
samples: 3 4 4 4 4 6 2 2 3 6 3 3 6 5 3 6
timestamps: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
forEachFrame iterator structure:
+ 0 (program) 1
- 0 (program) 1 1 1
+ 0 bar 2
+ 1 foo 6
+ 2 (garbage collector) 7
- 2 (garbage collector) 7 2 2
- 1 foo 6 3 1
- 0 bar 2 7 4
+ 0 (program) 9
- 0 (program) 9 1 1
+ 0 bar 10
+ 1 foo 10
- 1 foo 10 1 1
- 0 bar 10 1 0
+ 0 (program) 11
- 0 (program) 11 2 2
+ 0 bar 13
+ 1 foo 13
- 1 foo 13 1 1
- 0 bar 13 1 0
+ 0 baz 14
- 0 baz 14 1 1
+ 0 (program) 15
- 0 (program) 15 1 1
+ 0 bar 16
+ 1 foo 16
- 1 foo 16 1 1
- 0 bar 16 1 0
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(async function() {
TestRunner.addResult(`Tests missing samples are replaced with neighbor stacks.`);
await TestRunner.loadModule('cpu_profiler_test_runner');
const profile = {
startTime: 1000,
endTime: 4000,
nodes: [
{id: 1, hitCount: 0, callFrame: {functionName: '(root)'}, children: [2,3,4,5]},
{id: 2, hitCount: 1000, callFrame: {functionName: '(garbage collector)'}},
{id: 3, hitCount: 1000, callFrame: {functionName: '(program)'}},
{id: 4, hitCount: 1000, callFrame: {functionName: 'bar'}, children: [6]},
{id: 5, hitCount: 1000, callFrame: {functionName: 'baz'}},
{id: 6, hitCount: 1000, callFrame: {functionName: 'foo'}}
],
samples: [3, 4, 3, 4, 3, 6, 2, 2, 3, 6, 3, 3, 6, 5, 3, 6]
};
profile.timeDeltas = profile.samples.map(_ => 1000);
profile.endTime = profile.startTime + profile.timeDeltas.length * 1000;
const rawSamples = profile.samples.slice();
const model = new SDK.CPUProfileDataModel(profile);
TestRunner.addResult('Profile tree:');
printTree('', model.profileHead);
function printTree(padding, node) {
TestRunner.addResult(
`${padding}${node.functionName} id:${node.id} total:${node.total} self:${node.self} depth:${node.depth}`);
node.children.sort((a, b) => a.id - b.id).forEach(printTree.bind(null, padding + ' '));
}
TestRunner.addResult('raw samples: ' + rawSamples.join(' '));
TestRunner.addResult('samples: ' + model.samples.join(' '));
TestRunner.addResult('timestamps: ' + model.timestamps.join(' '));
TestRunner.addResult('forEachFrame iterator structure:');
model.forEachFrame(
(depth, node, ts) =>
TestRunner.addResult(' '.repeat(depth) + `+ ${depth} ${node.callFrame.functionName} ${ts}`),
(depth, node, ts, total, self) =>
TestRunner.addResult(' '.repeat(depth) + `- ${depth} ${node.callFrame.functionName} ${ts} ${total} ${self}`),
);
TestRunner.completeTest();
})();
......@@ -58,6 +58,7 @@ SDK.CPUProfileDataModel = class extends SDK.ProfileTreeModel {
this._buildIdToNodeMap();
this._sortSamples();
this._normalizeTimestamps();
this._fixMissingSamples();
}
}
......@@ -246,6 +247,53 @@ SDK.CPUProfileDataModel = class extends SDK.ProfileTreeModel {
}
}
_fixMissingSamples() {
// Sometimes sampler is not able to parse the JS stack and returns
// a (program) sample instead. The issue leads to call frames belong
// to the same function invocation being split apart.
// Here's a workaround for that. When there's a single (program) sample
// between two call stacks sharing the same bottom node, it is replaced
// with the preceeding sample.
const samples = this.samples;
const samplesCount = samples.length;
if (!this.programNode || samplesCount < 3)
return;
const idToNode = this._idToNode;
const programNodeId = this.programNode.id;
const gcNodeId = this.gcNode ? this.gcNode.id : -1;
const idleNodeId = this.idleNode ? this.idleNode.id : -1;
let prevNodeId = samples[0];
let nodeId = samples[1];
let count = 0;
for (let sampleIndex = 1; sampleIndex < samplesCount - 1; sampleIndex++) {
const nextNodeId = samples[sampleIndex + 1];
if (nodeId === programNodeId && !isSystemNode(prevNodeId) && !isSystemNode(nextNodeId) &&
bottomNode(idToNode.get(prevNodeId)) === bottomNode(idToNode.get(nextNodeId))) {
++count;
samples[sampleIndex] = prevNodeId;
}
prevNodeId = nodeId;
nodeId = nextNodeId;
}
Common.console.warn(ls`DevTools: CPU profile parser is fixing ${count} missing samples.`);
/**
* @param {!SDK.ProfileNode} node
* @return {!SDK.ProfileNode}
*/
function bottomNode(node) {
while (node.parent && node.parent.parent)
node = node.parent;
return node;
}
/**
* @param {number} nodeId
* @return {boolean}
*/
function isSystemNode(nodeId) {
return nodeId === programNodeId || nodeId === gcNodeId || nodeId === idleNodeId;
}
}
/**
* @param {function(number, !SDK.CPUProfileNode, number)} openFrameCallback
* @param {function(number, !SDK.CPUProfileNode, number, number, number)} closeFrameCallback
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment