| <!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> |