UNPKG

chrome-devtools-frontend

Version:
311 lines (270 loc) • 9.77 kB
// 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);