// Copyright 2025 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import './Table.js'; import '../../../../ui/kit/kit.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 UI from '../../../../ui/legacy/legacy.js'; import * as Lit from '../../../../ui/lit/lit.js'; import {BaseInsightComponent} from './BaseInsightComponent.js'; import {eventRef} from './EventRef.js'; import {md} from './Helpers.js'; import networkDependencyTreeInsightStyles from './networkDependencyTreeInsight.css.js'; import {nodeLink} from './NodeLink.js'; import {renderOthersLabel, Table, type TableDataRow} from './Table.js'; const {UIStrings, i18nString} = Trace.Insights.Models.NetworkDependencyTree; const {html} = Lit; const {widgetConfig} = UI.Widget; export const MAX_CHAINS_TO_SHOW = 5; export class NetworkDependencyTree extends BaseInsightComponent { override internalName = 'long-critical-network-tree'; #relatedRequests: Set|null = null; #countOfChains = 0; protected override hasAskAiSupport(): boolean { return true; } #createOverlayForChain(requests: Set): Trace.Types.Overlays.EntryOutline[] { const overlays: Trace.Types.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`
${eventRef(node.request)} ${i18n.TimeUtilities.formatMicroSecondsTime(Trace.Types.Timing.Micro(node.timeFromInitialRequest))}
`; // clang-format on } mapNetworkDependencyToRow(node: CriticalRequestNode): TableDataRow|null { // Check early if we've exceeded the maximum number of chains to show. // If so, and this is a leaf node, increment count and then skip rendering. // Otherwise, simply skip rendering. if (this.#countOfChains >= MAX_CHAINS_TO_SHOW) { if (node.children.length === 0) { // This still counts the chain even if not rendered, so we can count how many chains are collapsed. this.#countOfChains++; } return null; } // If this is a leaf node and we haven't exceeded the max chains, increment the count. // This ensures we only count chains that will actually be rendered (or at least considered for rendering). if (node.children.length === 0) { this.#countOfChains++; } return { values: [this.#renderNetworkTreeRow(node)], overlays: this.#createOverlayForChain(node.relatedRequests), // Filter out the empty rows otherwise the `Table`component will render a super short row subRows: node.children.map(child => this.mapNetworkDependencyToRow(child)).filter(row => row !== null), }; } #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: [], // Filter out the empty rows otherwise the `Table` component will render a super short row subRows: nodes.map(node => this.mapNetworkDependencyToRow(node)).filter(row => row !== null), }]; if (this.#countOfChains > MAX_CHAINS_TO_SHOW) { rows.push({ values: [renderOthersLabel(this.#countOfChains - MAX_CHAINS_TO_SHOW)], }); } // clang-format off return html` `; // clang-format on } #renderNetworkTreeSection(): Lit.LitTemplate { if (!this.model) { return Lit.nothing; } if (!this.model.rootNodes.length) { // clang-format off return html`
${i18nString(UIStrings.noNetworkDependencyTree)}
`; // clang-format on } // clang-format off return html`
${i18nString(UIStrings.maxCriticalPathLatency)}
${i18n.TimeUtilities.formatMicroSecondsTime((this.model.maxTime))}
${this.#renderNetworkDependencyTree(this.model.rootNodes)}
`; // 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`
${md(i18nString(UIStrings.tooManyPreconnectLinksWarning))}
`; // clang-format on } #renderPreconnectOriginsTable(): Lit.LitTemplate { if (!this.model) { return Lit.nothing; } const preconnectOriginsTableTitle = html`
${i18nString(UIStrings.preconnectOriginsTableTitle)}
${md(i18nString(UIStrings.preconnectOriginsTableDescription))}
`; if (!this.model.preconnectedOrigins.length) { // clang-format off return html`
${preconnectOriginsTableTitle} ${i18nString(UIStrings.noPreconnectOrigins)}
`; // 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, }; } const nodeEl = nodeLink({ backendNodeId: preconnectOrigin.node_id, frame: preconnectOrigin.frame, fallbackHtmlSnippet: ``, }); return { values: [preconnectOrigin.url, nodeEl], subRows, }; }); // clang-format off return html`
${preconnectOriginsTableTitle} ${this.#renderTooManyPreconnectsWarning()}
`; // clang-format on } #renderEstSavingTable(): Lit.LitTemplate { if (!this.model) { return Lit.nothing; } const estSavingTableTitle = html`
${i18nString(UIStrings.estSavingTableTitle)}
${md(i18nString(UIStrings.estSavingTableDescription))}
`; if (!this.model.preconnectCandidates.length) { // clang-format off return html`
${estSavingTableTitle} ${i18nString(UIStrings.noPreconnectCandidates)}
`; // clang-format on } const rows: TableDataRow[] = this.model.preconnectCandidates.map( candidate => ({ values: [candidate.origin, i18n.TimeUtilities.millisToString(candidate.wastedMs)], })); // clang-format off return html`
${estSavingTableTitle}
`; // clang-format on } override renderContent(): Lit.LitTemplate { return html` ${this.#renderNetworkTreeSection()} ${this.#renderPreconnectOriginsTable()} ${this.#renderEstSavingTable()} `; } }