chrome-devtools-frontend
Version:
Chrome DevTools UI
311 lines (270 loc) • 9.77 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 './Table.js';
import './NodeLink.js';
import '../../../../ui/components/icon_button/icon_button.js';
import * as i18n from '../../../../core/i18n/i18n.js';
import type {
CriticalRequestNode, NetworkDependencyTreeInsightModel} from
'../../../../models/trace/insights/NetworkDependencyTree.js';
import * as Trace from '../../../../models/trace/trace.js';
import * as Lit from '../../../../ui/lit/lit.js';
import type * as Overlays from '../../overlays/overlays.js';
import {md} from '../../utils/Helpers.js';
import {BaseInsightComponent} from './BaseInsightComponent.js';
import {eventRef} from './EventRef.js';
import networkDependencyTreeInsightStyles from './networkDependencyTreeInsight.css.js';
import type {NodeLinkData} from './NodeLink.js';
import type {TableData, TableDataRow} from './Table.js';
const {UIStrings, i18nString} = Trace.Insights.Models.NetworkDependencyTree;
const {html} = Lit;
export class NetworkDependencyTree extends BaseInsightComponent<NetworkDependencyTreeInsightModel> {
static override readonly litTagName = Lit.StaticHtml.literal`devtools-performance-long-critical-network-tree`;
override internalName = 'long-critical-network-tree';
#relatedRequests: Set<Trace.Types.Events.SyntheticNetworkRequest>|null = null;
override createOverlays(): Overlays.Overlays.TimelineOverlay[] {
if (!this.model) {
return [];
}
const overlays: Overlays.Overlays.TimelineOverlay[] = [];
getAllOverlays(this.model.rootNodes, overlays);
return overlays;
}
#createOverlayForChain(requests: Set<Trace.Types.Events.SyntheticNetworkRequest>): Overlays.Overlays.EntryOutline[] {
const overlays: Overlays.Overlays.EntryOutline[] = [];
requests.forEach(entry => overlays.push({
type: 'ENTRY_OUTLINE',
entry,
outlineReason: 'ERROR',
}));
return overlays;
}
#renderNetworkTreeRow(node: CriticalRequestNode): Lit.LitTemplate {
const requestStyles = Lit.Directives.styleMap({
display: 'flex',
'--override-timeline-link-text-color': node.isLongest ? 'var(--sys-color-error)' : '',
color: node.isLongest ? 'var(--sys-color-error)' : '',
backgroundColor: this.#relatedRequests?.has(node.request) ? 'var(--sys-color-state-hover-on-subtle)' : '',
});
const urlStyles = Lit.Directives.styleMap({
flex: 'auto',
});
// clang-format off
return html`
<div style=${requestStyles}>
<span style=${urlStyles}>${eventRef(node.request)}</span>
<span>
${i18n.TimeUtilities.formatMicroSecondsTime(Trace.Types.Timing.Micro(node.timeFromInitialRequest))}
</span>
</div>
`;
// clang-format on
}
#mapNetworkDependencyToRow(node: CriticalRequestNode): TableDataRow {
return {
values: [this.#renderNetworkTreeRow(node)],
overlays: this.#createOverlayForChain(node.relatedRequests),
subRows: node.children.map(child => this.#mapNetworkDependencyToRow(child)),
};
}
#renderNetworkDependencyTree(nodes: CriticalRequestNode[]): Lit.LitTemplate|null {
if (nodes.length === 0) {
return null;
}
const rows: TableDataRow[] = [{
// Add one empty row so the main document request can also has a left border
values: [],
subRows: nodes.map(node => this.#mapNetworkDependencyToRow(node))
}];
// clang-format off
return html`
<devtools-performance-table
.data=${{
insight: this,
headers: [i18nString(UIStrings.columnRequest), i18nString(UIStrings.columnTime)],
rows,
} as TableData}>
</devtools-performance-table>
`;
// clang-format on
}
#renderNetworkTreeSection(): Lit.LitTemplate {
if (!this.model) {
return Lit.nothing;
}
if (!this.model.rootNodes.length) {
// clang-format off
return html`
<style>${networkDependencyTreeInsightStyles}</style>
<div class="insight-section">${i18nString(UIStrings.noNetworkDependencyTree)}</div>
`;
// clang-format on
}
// clang-format off
return html`
<style>${networkDependencyTreeInsightStyles}</style>
<div class="insight-section">
<div class="max-time">
${i18nString(UIStrings.maxCriticalPathLatency)}
<br>
<span class='longest'> ${i18n.TimeUtilities.formatMicroSecondsTime((this.model.maxTime))}</span>
</div>
</div>
<div class="insight-section">
${this.#renderNetworkDependencyTree(this.model.rootNodes)}
</div>
`;
// clang-format on
}
#renderTooManyPreconnectsWarning(): Lit.LitTemplate {
if (!this.model) {
return Lit.nothing;
}
if (this.model.preconnectedOrigins.length <=
Trace.Insights.Models.NetworkDependencyTree.TOO_MANY_PRECONNECTS_THRESHOLD) {
return Lit.nothing;
}
const warningStyles = Lit.Directives.styleMap({
backgroundColor: 'var(--sys-color-surface-yellow)',
padding: ' var(--sys-size-5) var(--sys-size-8);',
display: 'flex',
});
// clang-format off
return html`
<div style=${warningStyles}>
${md(i18nString(UIStrings.tooManyPreconnectLinksWarning))}
</div>
`;
// clang-format on
}
#renderPreconnectOriginsTable(): Lit.LitTemplate {
if (!this.model) {
return Lit.nothing;
}
const preconnectOriginsTableTitle = html`
<style>${networkDependencyTreeInsightStyles}</style>
<div class='section-title'>${i18nString(UIStrings.preconnectOriginsTableTitle)}</div>
<div class="insight-description">${md(i18nString(UIStrings.preconnectOriginsTableDescription))}</div>
`;
if (!this.model.preconnectedOrigins.length) {
// clang-format off
return html`
<div class="insight-section">
${preconnectOriginsTableTitle}
${i18nString(UIStrings.noPreconnectOrigins)}
</div>
`;
// clang-format on
}
const rows: TableDataRow[] = this.model.preconnectedOrigins.map(preconnectOrigin => {
const subRows = [];
if (preconnectOrigin.unused) {
subRows.push({
values: [md(i18nString(UIStrings.unusedWarning))],
});
}
if (preconnectOrigin.crossorigin) {
subRows.push({
values: [md(i18nString(UIStrings.crossoriginWarning))],
});
}
if (preconnectOrigin.source === 'ResponseHeader') {
return {
values: [preconnectOrigin.url, eventRef(preconnectOrigin.request, {text: preconnectOrigin.headerText})],
subRows,
};
}
// clang-format off
const nodeEl = html`
<devtools-performance-node-link
.data=${{
backendNodeId: preconnectOrigin.node_id,
frame: preconnectOrigin.frame,
fallbackHtmlSnippet: `<link rel="preconnect" href="${preconnectOrigin.url}">`,
} as NodeLinkData}>
</devtools-performance-node-link>`;
// clang-format on
return {
values: [preconnectOrigin.url, nodeEl],
subRows,
};
});
// clang-format off
return html`
<div class="insight-section">
${preconnectOriginsTableTitle}
${this.#renderTooManyPreconnectsWarning()}
<devtools-performance-table
.data=${{
insight: this,
headers: [i18nString(UIStrings.columnOrigin), i18nString(UIStrings.columnSource)],
rows,
} as TableData}>
</devtools-performance-table>
</div>
`;
// clang-format on
}
#renderEstSavingTable(): Lit.LitTemplate {
if (!this.model) {
return Lit.nothing;
}
const estSavingTableTitle = html`
<style>${networkDependencyTreeInsightStyles}</style>
<div class='section-title'>${i18nString(UIStrings.estSavingTableTitle)}</div>
<div class="insight-description">${md(i18nString(UIStrings.estSavingTableDescription))}</div>
`;
if (!this.model.preconnectCandidates.length) {
// clang-format off
return html`
<div class="insight-section">
${estSavingTableTitle}
${i18nString(UIStrings.noPreconnectCandidates)}
</div>
`;
// clang-format on
}
const rows: TableDataRow[] = this.model.preconnectCandidates.map(
candidate => ({
values: [candidate.origin, i18n.TimeUtilities.millisToString(candidate.wastedMs)],
}));
// clang-format off
return html`
<div class="insight-section">
${estSavingTableTitle}
<devtools-performance-table
.data=${{
insight: this,
headers: [i18nString(UIStrings.columnOrigin), i18nString(UIStrings.columnWastedMs)],
rows,
} as TableData}>
</devtools-performance-table>
</div>
`;
// clang-format on
}
override renderContent(): Lit.LitTemplate {
return html`
${this.#renderNetworkTreeSection()}
${this.#renderPreconnectOriginsTable()}
${this.#renderEstSavingTable()}
`;
}
}
function getAllOverlays(nodes: CriticalRequestNode[], overlays: Overlays.Overlays.TimelineOverlay[]): void {
nodes.forEach(node => {
overlays.push({
type: 'ENTRY_OUTLINE',
entry: node.request,
outlineReason: 'ERROR',
});
getAllOverlays(node.children, overlays);
});
}
declare global {
interface HTMLElementTagNameMap {
'devtools-performance-long-critical-network-tree': NetworkDependencyTree;
}
}
customElements.define('devtools-performance-long-critical-network-tree', NetworkDependencyTree);