chrome-devtools-frontend
Version:
Chrome DevTools UI
301 lines (262 loc) • 10.5 kB
text/typescript
// Copyright 2025 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.
import type * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Trace from '../../models/trace/trace.js';
import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import * as TimelineTreeView from './TimelineTreeView.js';
import * as Utils from './utils/utils.js';
const UIStrings = {
/**
*@description Unattributed text for an unattributed entity.
*/
unattributed: '[unattributed]',
/**
*@description Title for the name of either 1st or 3rd Party entities.
*/
firstOrThirdPartyName: '1st / 3rd party',
/**
*@description Title referencing transfer size.
*/
transferSize: 'Transfer size',
/**
*@description Title referencing self time.
*/
selfTime: 'Self time',
};
const str_ = i18n.i18n.registerUIStrings('panels/timeline/ThirdPartyTreeView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class ThirdPartyTreeViewWidget extends TimelineTreeView.TimelineTreeView {
#thirdPartySummaries: {
summaries: Trace.Extras.ThirdParties.ThirdPartySummary,
entityByEvent: Map<Trace.Types.Events.Event, Trace.Extras.ThirdParties.Entity>,
}|null = null;
// By default the TimelineTreeView will auto-select the first row
// when the grid is refreshed but for the ThirdParty view we only
// want to do this when the user hovers.
protected override autoSelectFirstChildOnRefresh = false;
constructor() {
super();
this.element.setAttribute('jslog', `${VisualLogging.pane('third-party-tree').track({hover: true})}`);
this.init();
this.dataGrid.markColumnAsSortedBy('self', DataGrid.DataGrid.Order.Descending);
this.dataGrid.setResizeMethod(DataGrid.DataGrid.ResizeMethod.NEAREST);
/**
* By default data grids always expand when arrowing.
* For 3P table, we don't use this feature.
*/
this.dataGrid.expandNodesWhenArrowing = false;
}
override wasShown(): void {
this.dataGrid.addEventListener(DataGrid.DataGrid.Events.SELECTED_NODE, this.#onDataGridSelectionChange, this);
}
override childWasDetached(_widget: UI.Widget.Widget): void {
this.dataGrid.removeEventListener(DataGrid.DataGrid.Events.SELECTED_NODE, this.#onDataGridSelectionChange);
}
override buildTree(): Trace.Extras.TraceTree.Node {
const parsedTrace = this.parsedTrace();
const entityMapper = this.entityMapper();
if (!parsedTrace || !entityMapper) {
return new Trace.Extras.TraceTree.BottomUpRootNode([], {
textFilter: this.textFilter(),
filters: this.filtersWithoutTextFilter(),
startTime: this.startTime,
endTime: this.endTime,
eventGroupIdCallback: this.groupingFunction(),
});
}
// Update summaries.
const min = Trace.Helpers.Timing.milliToMicro(this.startTime);
const max = Trace.Helpers.Timing.milliToMicro(this.endTime);
const bounds: Trace.Types.Timing.TraceWindowMicro = {max, min, range: Trace.Types.Timing.Micro(max - min)};
this.#thirdPartySummaries =
Trace.Extras.ThirdParties.getSummariesAndEntitiesWithMapping(parsedTrace, bounds, entityMapper.mappings());
const events = this.#thirdPartySummaries?.entityByEvent.keys();
const relatedEvents = Array.from(events ?? []).sort(Trace.Helpers.Trace.eventTimeComparator);
// The filters for this view are slightly different; we want to use the set
// of visible event types, but also include network events, which by
// default are not in the set of visible entries (as they are not shown on
// the main flame chart).
const filter = new Trace.Extras.TraceFilter.VisibleEventsFilter(
Utils.EntryStyles.visibleTypes().concat([Trace.Types.Events.Name.SYNTHETIC_NETWORK_REQUEST]));
const node = new Trace.Extras.TraceTree.BottomUpRootNode(relatedEvents, {
textFilter: this.textFilter(),
filters: [filter],
startTime: this.startTime,
endTime: this.endTime,
eventGroupIdCallback: this.groupingFunction(),
});
return node;
}
/**
* Third party tree view doesn't require the select feature, as this expands the node.
*/
override selectProfileNode(): void {
return;
}
protected groupingFunction(): ((arg0: Trace.Types.Events.Event) => string)|null {
return this.domainByEvent.bind(this);
}
private domainByEvent(event: Trace.Types.Events.Event): string {
const parsedTrace = this.parsedTrace();
if (!parsedTrace) {
return '';
}
const entityMappings = this.entityMapper();
if (!entityMappings) {
return '';
}
const entity = entityMappings.entityForEvent(event);
if (!entity) {
return '';
}
return entity.name;
}
override populateColumns(columns: DataGrid.DataGrid.ColumnDescriptor[]): void {
columns.push(
{
id: 'site',
title: i18nString(UIStrings.firstOrThirdPartyName),
// It's important that this width is the `.widget.vbox.timeline-tree-view` max-width (550)
// minus the two fixed sizes below. (550-100-105) == 345
width: '345px',
// And with this column not-fixed-width and resizingMethod NEAREST, the name-column will appropriately flex.
sortable: true,
},
{
id: 'transfer-size',
title: i18nString(UIStrings.transferSize),
width: '100px', // Mostly so there's room for the header plus sorting triangle
fixedWidth: true,
sortable: true,
},
{
id: 'self',
title: i18nString(UIStrings.selfTime),
width: '105px', // Mostly to fit large self-time plus devtools-button
fixedWidth: true,
sortable: true,
});
}
override populateToolbar(): void {
return;
}
private compareTransferSize(
a: DataGrid.SortableDataGrid.SortableDataGridNode<TimelineTreeView.GridNode>,
b: DataGrid.SortableDataGrid.SortableDataGridNode<TimelineTreeView.GridNode>): number {
const nodeA = a as TimelineTreeView.TreeGridNode;
const nodeB = b as TimelineTreeView.TreeGridNode;
const transferA = this.extractThirdPartySummary(nodeA.profileNode).transferSize ?? 0;
const transferB = this.extractThirdPartySummary(nodeB.profileNode).transferSize ?? 0;
return transferA - transferB;
}
override sortingChanged(): void {
const columnId = this.dataGrid.sortColumnId();
if (!columnId) {
return;
}
let sortFunction:
((a: DataGrid.SortableDataGrid.SortableDataGridNode<TimelineTreeView.GridNode>,
b: DataGrid.SortableDataGrid.SortableDataGridNode<TimelineTreeView.GridNode>) => number)|null;
switch (columnId) {
case 'transfer-size':
sortFunction = this.compareTransferSize.bind(this);
break;
default:
sortFunction = super.getSortingFunction(columnId);
break;
}
if (sortFunction) {
this.dataGrid.sortNodes(sortFunction, !this.dataGrid.isSortOrderAscending());
}
}
/**
* This event fires when the user selects a row in the grid, either by
* clicking or by using the arrow keys. We want to have the same effect as
* when the user hover overs a row.
*/
#onDataGridSelectionChange(
event: Common.EventTarget.EventTargetEvent<DataGrid.DataGrid.DataGridNode<TimelineTreeView.GridNode>>): void {
this.onHover((event.data as TimelineTreeView.GridNode).profileNode);
}
override onHover(node: Trace.Extras.TraceTree.Node|null): void {
const entityMappings = this.entityMapper();
if (!entityMappings || !node?.event) {
return;
}
const nodeEntity = entityMappings.entityForEvent(node.event);
if (!nodeEntity) {
return;
}
const eventsForEntity = entityMappings.eventsForEntity(nodeEntity);
this.dispatchEventToListeners(TimelineTreeView.TimelineTreeView.Events.THIRD_PARTY_ROW_HOVERED, eventsForEntity);
}
displayInfoForGroupNode(node: Trace.Extras.TraceTree.Node): {
name: string,
color: string,
icon: (Element|undefined),
} {
const color = 'gray';
const unattributed = i18nString(UIStrings.unattributed);
const id = typeof node.id === 'symbol' ? undefined : node.id;
const domainName = id ? this.domainByEvent(node.event) : undefined;
return {name: domainName || unattributed, color, icon: undefined};
}
extractThirdPartySummary(node: Trace.Extras.TraceTree.Node): {transferSize: number} {
if (!this.#thirdPartySummaries) {
return {transferSize: 0};
}
const entity = this.#thirdPartySummaries.entityByEvent.get(node.event);
if (!entity) {
return {transferSize: 0};
}
const summary = this.#thirdPartySummaries.summaries.byEntity.get(entity);
if (!summary) {
return {transferSize: 0};
}
return {transferSize: summary.transferSize};
}
nodeIsFirstParty(node: Trace.Extras.TraceTree.Node): boolean {
const mapper = this.entityMapper();
if (!mapper) {
return false;
}
const firstParty = mapper.firstPartyEntity();
return firstParty === mapper.entityForEvent(node.event);
}
nodeIsExtension(node: Trace.Extras.TraceTree.Node): boolean {
const mapper = this.entityMapper();
if (!mapper) {
return false;
}
const entity = mapper.entityForEvent(node.event);
return Boolean(entity) && entity?.category === 'Chrome Extension';
}
}
export class ThirdPartyTreeElement extends UI.Widget.WidgetElement<UI.Widget.Widget> {
#treeView?: ThirdPartyTreeViewWidget;
set treeView(treeView: ThirdPartyTreeViewWidget) {
this.#treeView = treeView;
}
constructor() {
super();
this.style.display = 'contents';
}
override createWidget(): UI.Widget.Widget {
const containerWidget = new UI.Widget.Widget(false, undefined, this);
containerWidget.contentElement.style.display = 'contents';
if (this.#treeView) {
this.#treeView.show(containerWidget.contentElement);
}
return containerWidget;
}
}
customElements.define('devtools-performance-third-party-tree-view', ThirdPartyTreeElement);
declare global {
interface HTMLElementTagNameMap {
'devtools-performance-third-party-tree-view': ThirdPartyTreeElement;
}
}