// Copyright 2024 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import '../../../ui/kit/kit.js'; import * as i18n from '../../../core/i18n/i18n.js'; import * as Platform from '../../../core/platform/platform.js'; import * as SDK from '../../../core/sdk/sdk.js'; import * as Trace from '../../../models/trace/trace.js'; import * as PerfUI from '../../../ui/legacy/components/perf_ui/perf_ui.js'; import * as UI from '../../../ui/legacy/legacy.js'; import * as Lit from '../../../ui/lit/lit.js'; import * as TimelineUtils from '../utils/utils.js'; import networkRequestTooltipStyles from './networkRequestTooltip.css.js'; import {colorForNetworkRequest, networkResourceCategory} from './Utils.js'; const {html, nothing, Directives: {classMap, ifDefined}} = Lit; const MAX_URL_LENGTH = 60; const UIStrings = { /** * @description Text that refers to the priority of network request */ priority: 'Priority', /** * @description Text for the duration of a network request */ duration: 'Duration', /** * @description Text that refers to the queueing and connecting time of a network request */ queuingAndConnecting: 'Queuing and connecting', /** * @description Text that refers to the request sent and waiting time of a network request */ requestSentAndWaiting: 'Request sent and waiting', /** * @description Text that refers to the content downloading time of a network request */ contentDownloading: 'Content downloading', /** * @description Text that refers to the waiting on main thread time of a network request */ waitingOnMainThread: 'Waiting on main thread', /** * @description Text that refers to a network request is render-blocking */ renderBlocking: 'Render-blocking', /** * @description Text to refer to the list of redirects. */ redirects: 'Redirects', /** * @description Cell title in Network Data Grid Node of the Network panel * @example {Fast 4G} PH1 */ wasThrottled: 'Request was throttled ({PH1})', } as const; const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/NetworkRequestTooltip.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); interface ViewInput { networkRequest: Trace.Types.Events.SyntheticNetworkRequest; entityMapper?: Trace.EntityMapper.EntityMapper; throttlingTitle?: string; } type View = (input: ViewInput, output: undefined, target: HTMLElement) => void; export const DEFAULT_VIEW: View = (input, output, target) => { const { networkRequest, entityMapper, throttlingTitle, } = input; const chipStyle = { backgroundColor: `${colorForNetworkRequest(networkRequest)}`, }; const url = new URL(networkRequest.args.data.url); const entity = entityMapper ? entityMapper.entityForEvent(networkRequest) : null; const originWithEntity = TimelineUtils.Helpers.formatOriginWithEntity(url, entity, true); const redirectsHtml = NetworkRequestTooltip.renderRedirects(networkRequest); // clang-format off Lit.render(html`
${Platform.StringUtilities.trimMiddle(url.href.replace(url.origin, ''), MAX_URL_LENGTH)}
${originWithEntity}
${networkResourceCategory(networkRequest)}
${i18nString(UIStrings.priority)}: ${NetworkRequestTooltip.renderPriorityValue(networkRequest)}
${throttlingTitle ? html`
${i18nString(UIStrings.wasThrottled, {PH1: throttlingTitle})}
` : nothing} ${Trace.Helpers.Network.isSyntheticNetworkRequestEventRenderBlocking(networkRequest) ? html`
${i18nString(UIStrings.renderBlocking)}
` : Lit.nothing }
${NetworkRequestTooltip.renderTimings(networkRequest)} ${redirectsHtml ? html `
${redirectsHtml} ` : Lit.nothing}
`, target); // clang-format on }; export class NetworkRequestTooltip extends UI.Widget.Widget { static createWidgetElement( request: Trace.Types.Events.SyntheticNetworkRequest, entityMapper?: Trace.EntityMapper.EntityMapper): UI.Widget.WidgetElement { const widgetElement = document.createElement('devtools-widget') as UI.Widget.WidgetElement; widgetElement.widgetConfig = UI.Widget.widgetConfig(NetworkRequestTooltip, { networkRequest: request, entityMapper, }); return widgetElement; } #view: View; #networkRequest?: Trace.Types.Events.SyntheticNetworkRequest; #entityMapper?: Trace.EntityMapper.EntityMapper; constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) { super(element, {useShadowDom: true}); this.#view = view; } set networkRequest(networkRequest: Trace.Types.Events.SyntheticNetworkRequest) { this.#networkRequest = networkRequest; this.requestUpdate(); } set entityMapper(entityMapper: Trace.EntityMapper.EntityMapper|undefined) { this.#entityMapper = entityMapper; this.requestUpdate(); } static renderPriorityValue(networkRequest: Trace.Types.Events.SyntheticNetworkRequest): Lit.TemplateResult { if (networkRequest.args.data.priority === networkRequest.args.data.initialPriority) { return html`${PerfUI.NetworkPriorities.uiLabelForNetworkPriority(networkRequest.args.data.priority)}`; } return html`${PerfUI.NetworkPriorities.uiLabelForNetworkPriority(networkRequest.args.data.initialPriority)} ${PerfUI.NetworkPriorities.uiLabelForNetworkPriority(networkRequest.args.data.priority)}`; } static renderTimings(networkRequest: Trace.Types.Events.SyntheticNetworkRequest): Lit.TemplateResult|null { const syntheticData = networkRequest.args.data.syntheticData; const queueing = (syntheticData.sendStartTime - networkRequest.ts) as Trace.Types.Timing.Micro; const requestPlusWaiting = (syntheticData.downloadStart - syntheticData.sendStartTime) as Trace.Types.Timing.Micro; const download = (syntheticData.finishTime - syntheticData.downloadStart) as Trace.Types.Timing.Micro; const waitingOnMainThread = (networkRequest.ts + networkRequest.dur - syntheticData.finishTime) as Trace.Types.Timing.Micro; const color = colorForNetworkRequest(networkRequest); const styleForWaiting = { backgroundColor: `color-mix(in srgb, ${color}, hsla(0, 100%, 100%, 0.8))`, }; const styleForDownloading = { backgroundColor: color, }; const sdkNetworkRequest = SDK.TraceObject.RevealableNetworkRequest.create(networkRequest); const wasThrottled = sdkNetworkRequest && SDK.NetworkManager.MultitargetNetworkManager.instance().appliedRequestConditions( sdkNetworkRequest.networkRequest); const throttledTitle = wasThrottled ? i18nString(UIStrings.wasThrottled, { PH1: typeof wasThrottled.conditions.title === 'string' ? wasThrottled.conditions.title : wasThrottled.conditions.title() }) : undefined; // The outside spans are transparent with a border on the outside edge. // The inside spans are 1px tall rectangles, vertically centered, with background color. // | // |---- // whisker-left-> | ^ horizontal const leftWhisker = html` `; const rightWhisker = html` `; const classes = classMap({ ['timings-row timings-row--duration']: true, throttled: Boolean(wasThrottled?.urlPattern), }); return html`
${ wasThrottled?.urlPattern ? html`` : html``} ${i18nString(UIStrings.duration)} ${i18n.TimeUtilities.formatMicroSecondsTime(networkRequest.dur)}
${leftWhisker} ${i18nString(UIStrings.queuingAndConnecting)} ${i18n.TimeUtilities.formatMicroSecondsTime(queueing)}
${i18nString(UIStrings.requestSentAndWaiting)} ${i18n.TimeUtilities.formatMicroSecondsTime(requestPlusWaiting)}
${i18nString(UIStrings.contentDownloading)} ${i18n.TimeUtilities.formatMicroSecondsTime(download)}
${rightWhisker} ${i18nString(UIStrings.waitingOnMainThread)} ${i18n.TimeUtilities.formatMicroSecondsTime(waitingOnMainThread)}
`; } static renderRedirects(networkRequest: Trace.Types.Events.SyntheticNetworkRequest): Lit.TemplateResult|null { const redirectRows = []; if (networkRequest.args.data.redirects.length > 0) { redirectRows.push(html`
${i18nString(UIStrings.redirects)}
`); for (const redirect of networkRequest.args.data.redirects) { redirectRows.push(html`
${redirect.url}
`); } return html`${redirectRows}`; } return null; } override performUpdate(): void { if (!this.#networkRequest) { return; } // TODO(crbug.com/466124088): Seems broken. const sdkNetworkRequest = SDK.TraceObject.RevealableNetworkRequest.create(this.#networkRequest); const networkConditions = sdkNetworkRequest && SDK.NetworkManager.MultitargetNetworkManager.instance().appliedRequestConditions( sdkNetworkRequest.networkRequest); let throttlingTitle: string|undefined = undefined; if (networkConditions) { throttlingTitle = typeof networkConditions.conditions.title === 'string' ? networkConditions.conditions.title : networkConditions.conditions.title(); } const input: ViewInput = { networkRequest: this.#networkRequest, entityMapper: this.#entityMapper, throttlingTitle, }; this.#view(input, undefined, this.contentElement); } }