blob: ac1ac718a82e886cbfc459dfa84b6a7307a634b3 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright 2016 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.
-->
<link rel="import" href="/tracing/base/unit.html">
<link rel="import" href="/tracing/extras/chrome/blame_context/frame_tree_node.html">
<link rel="import" href="/tracing/extras/chrome/blame_context/render_frame.html">
<link rel="import" href="/tracing/extras/chrome/blame_context/top_level.html">
<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
<link rel="import" href="/tracing/ui/base/table.html">
<link rel="import" href="/tracing/ui/side_panel/side_panel.html">
<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html">
<link rel="import" href="/tracing/value/ui/scalar_span.html">
<dom-module id='tr-ui-e-s-frame-data-side-panel'>
<template>
<style>
:host {
display: flex;
width: 600px;
flex-direction: column;
}
table-container {
display: flex;
overflow: auto;
}
</style>
<div>
Organize by:
<select id="select">
<option value="none">None</option>
<option value="tree">Frame Tree</option>
</select>
</div>
<table-container>
<tr-ui-b-table id="table"></tr-ui-b-table>
</table-container>
</template>
</dom-module>
<script>
'use strict';
tr.exportTo('tr.ui.e.s', function() {
var BlameContextSnapshot = tr.e.chrome.BlameContextSnapshot;
var FrameTreeNodeSnapshot = tr.e.chrome.FrameTreeNodeSnapshot;
var RenderFrameSnapshot = tr.e.chrome.RenderFrameSnapshot;
var TopLevelSnapshot = tr.e.chrome.TopLevelSnapshot;
var BlameContextInstance = tr.e.chrome.BlameContextInstance;
var FrameTreeNodeInstance = tr.e.chrome.FrameTreeNodeInstance;
var RenderFrameInstance = tr.e.chrome.RenderFrameInstance;
var TopLevelInstance = tr.e.chrome.TopLevelInstance;
/**
* @constructor
* If |context| is provided, creates a row for the given context.
* Otherwise, creates an empty Row template which can be used for aggregating
* data from a group of subrows.
*/
function Row(context) {
this.subRows = undefined;
this.contexts = [];
this.type = undefined;
this.renderer = 'N/A';
this.url = undefined;
this.time = 0;
this.eventsOfInterest = new tr.model.EventSet();
if (context === undefined)
return;
this.type = context.objectInstance.blameContextType;
this.contexts.push(context);
if (context instanceof FrameTreeNodeSnapshot) {
if (context.renderFrame) {
this.contexts.push(context.renderFrame);
this.renderer = context.renderFrame.objectInstance.parent.pid;
}
} else if (context instanceof RenderFrameSnapshot) {
if (context.frameTreeNode)
this.contexts.push(context.frameTreeNode);
this.renderer = context.objectInstance.parent.pid;
} else if (context instanceof TopLevelSnapshot) {
this.renderer = context.objectInstance.parent.pid;
} else {
throw new Error('Unknown context type');
}
this.eventsOfInterest.addEventSet(this.contexts);
// TODO(xiaochengh): Handle the case where a subframe has a trivial url
// (e.g., about:blank), but inherits the origin of its parent. This is not
// needed now, but will be required if we want to group rows by origin.
this.url = context.url;
}
var groupFunctions = {
none: rows => rows,
// Group the rows according to the frame tree structure.
// Example: consider frame tree a(b, c(d)), where each frame has 1ms time
// attributed to it. The resulting table should look like:
// Type | Time | URL
// --------------+------+-----
// Frame Tree | 4 | a
// +- Frame | 1 | a
// +- Subframe | 1 | b
// +- Frame Tree | 2 | c
// +- Frame | 1 | c
// +- Subframe | 1 | d
tree: function(rows, rowMap) {
// Finds the parent of a specific row. When there is conflict between the
// browser's dump of the frame tree and the renderers', use the browser's.
var getParentRow = function(row) {
var pivot;
row.contexts.forEach(function(context) {
if (context instanceof tr.e.chrome.FrameTreeNodeSnapshot)
pivot = context;
});
if (pivot && pivot.parentContext)
return rowMap[pivot.parentContext.guid];
return undefined;
};
var rootRows = [];
rows.forEach(function(row) {
var parentRow = getParentRow(row);
if (parentRow === undefined) {
rootRows.push(row);
return;
}
if (parentRow.subRows === undefined)
parentRow.subRows = [];
parentRow.subRows.push(row);
});
var aggregateAllDescendants = function(row) {
if (!row.subRows) {
if (getParentRow(row))
row.type = 'Subframe';
return row;
}
var result = new Row();
result.type = 'Frame Tree';
result.renderer = row.renderer;
result.url = row.url;
result.subRows = [row];
row.subRows.forEach(
subRow => result.subRows.push(aggregateAllDescendants(subRow)));
result.subRows.forEach(function(subRow) {
result.time += subRow.time;
result.eventsOfInterest.addEventSet(subRow.eventsOfInterest);
});
row.subRows = undefined;
return result;
};
return rootRows.map(rootRow => aggregateAllDescendants(rootRow));
}
// TODO(xiaochengh): Add grouping by site and probably more...
};
Polymer({
is: 'tr-ui-e-s-frame-data-side-panel',
behaviors: [tr.ui.behaviors.SidePanel],
ready: function() {
this.model_ = undefined;
this.rangeOfInterest_ = new tr.b.Range();
this.$.table.showHeader = true;
this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
this.$.table.tableColumns = this.createFrameDataTableColumns_();
this.$.table.addEventListener('selection-changed', function(e) {
this.selectEventSet_(this.$.table.selectedTableRow.eventsOfInterest);
}.bind(this));
this.$.select.addEventListener('change', function(e) {
this.updateContents_();
}.bind(this));
},
selectEventSet_: function(eventSet) {
var event = new tr.model.RequestSelectionChangeEvent();
event.selection = eventSet;
this.dispatchEvent(event);
},
createFrameDataTableColumns_: function() {
return [
{
title: 'Renderer',
value: row => row.renderer,
cmp: (a, b) => a.renderer - b.renderer
},
{
title: 'Type',
value: row => row.type
},
// TODO(xiaochengh): Decide what details to show in the table:
// - URL seems necessary, but we may also want origin instead/both.
// - Distinguish between browser time and renderer time?
// - Distinguish between CPU time and wall clock time?
// - Memory? Network? ...
{
title: 'Time',
value: row => tr.v.ui.createScalarSpan(row.time, {
unit: tr.b.Unit.byName.timeStampInMs,
ownerDocument: this.ownerDocument
}),
cmp: (a, b) => a.time - b.time
},
{
title: 'URL',
value: row => row.url,
cmp: (a, b) => (a.url || '').localeCompare(b.url || '')
}
];
},
createFrameDataTableRows_: function() {
if (!this.model_)
return [];
// Gather contexts into skeletons of rows.
var rows = [];
var rowMap = {};
tr.b.iterItems(this.model_.processes, function(pid, process) {
process.objects.iterObjectInstances(function(objectInstance) {
if (!(objectInstance instanceof BlameContextInstance))
return;
objectInstance.snapshots.forEach(function(snapshot) {
if (rowMap[snapshot.guid])
return;
var row = new Row(snapshot);
row.contexts.forEach(context => rowMap[context.guid] = row);
rows.push(row);
}, this);
}, this);
}, this);
// Find slices attributed to each row.
// TODO(xiaochengh): We should implement a getter
// BlameContextSnapshot.attributedEvents, instead of process the model in
// a UI component.
tr.b.iterItems(this.model_.processes, function(pid, process) {
tr.b.iterItems(process.threads, function(tid, thread) {
thread.sliceGroup.iterSlicesInTimeRange(function(topLevelSlice) {
topLevelSlice.contexts.forEach(function(context) {
if (!context.snapshot.guid || !rowMap[context.snapshot.guid])
return;
var row = rowMap[context.snapshot.guid];
row.eventsOfInterest.push(topLevelSlice);
row.time += topLevelSlice.selfTime || 0;
});
}, this.currentRangeOfInterest.min, this.currentRangeOfInterest.max);
}, this);
}, this);
// Apply grouping to rows.
var select = this.$.select;
var groupOption = select.options[select.selectedIndex].value;
var groupFunction = groupFunctions[groupOption];
return groupFunction(rows, rowMap);
},
updateContents_: function() {
this.$.table.tableRows = this.createFrameDataTableRows_();
this.$.table.rebuild();
},
supportsModel: function(m) {
if (!m) {
return {
supported: false,
reason: 'No model available.'
};
}
var ans = {supported: false};
tr.b.iterItems(m.processes, function(pid, process) {
process.objects.iterObjectInstances(function(instance) {
if (instance instanceof BlameContextInstance)
ans.supported = true;
});
}, this);
if (!ans.supported)
ans.reason = 'No frame data available';
return ans;
},
get currentRangeOfInterest() {
if (this.rangeOfInterest_.isEmpty)
return this.model_.bounds;
else
return this.rangeOfInterest_;
},
get rangeOfInterest() {
return this.rangeOfInterest_;
},
set rangeOfInterest(rangeOfInterest) {
this.rangeOfInterest_ = rangeOfInterest;
this.updateContents_();
},
get selection() {
// Not applicable.
},
set selection(_) {
// Not applicable.
},
get textLabel() {
return 'Frame Data';
},
get model() {
return this.model_;
},
set model(model) {
this.model_ = model;
this.updateContents_();
}
});
tr.ui.side_panel.SidePanelRegistry.register(function() {
return document.createElement('tr-ui-e-s-frame-data-side-panel');
});
});
</script>