chrome-devtools-frontend
Version:
Chrome DevTools UI
235 lines (217 loc) • 9.71 kB
text/typescript
// Copyright 2023 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.
/* eslint-disable rulesdir/no-imperative-dom-api */
import * as i18n from '../../../core/i18n/i18n.js';
import * as Platform from '../../../core/platform/platform.js';
import type * as Protocol from '../../../generated/protocol.js';
import * as Trace from '../../../models/trace/trace.js';
import * as UI from '../../../ui/legacy/legacy.js';
// *********************************************************************
// At the moment this file consists of helpers to aid in the rendering
// of events details in the bottom drawer. In the future, we should use
// Lit for this section, and update this helpers accordingly.
// *********************************************************************
const UIStrings = {
/**
*@description Text in the Performance panel for a forced style and layout calculation of elements
* in a page. See https://developer.mozilla.org/en-US/docs/Glossary/Reflow
*/
forcedReflow: 'Forced reflow',
/**
*@description Text in Timeline UIUtils of the Performance panel
*@example {Forced reflow} PH1
*/
sIsALikelyPerformanceBottleneck: '{PH1} is a likely performance bottleneck.',
/**
*@description Text in the Performance panel for a function called during a time the browser was
* idle (inactive), which to longer to execute than a predefined deadline.
*@example {10ms} PH1
*/
idleCallbackExecutionExtended: 'Idle callback execution extended beyond deadline by {PH1}',
/**
*@description Text in the Performance panel which describes how long a task took.
*@example {task} PH1
*@example {10ms} PH2
*/
sTookS: '{PH1} took {PH2}.',
/**
*@description Text in the Performance panel for a task that took long. See
* https://developer.mozilla.org/en-US/docs/Glossary/Long_task
*/
longTask: 'Long task',
/**
*@description Text used to highlight a long interaction and link to web.dev/inp
*/
longInteractionINP: 'Long interaction',
/**
*@description Text in Timeline UIUtils of the Performance panel when the
* user clicks on a long interaction.
*@example {Long interaction} PH1
*/
sIsLikelyPoorPageResponsiveness: '{PH1} is indicating poor page responsiveness.',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
websocketProtocol: 'WebSocket protocol',
/**
* @description Details text indicating how many bytes were received in a WebSocket message
* @example {1024} PH1
*/
webSocketBytes: '{PH1} byte(s)',
/**
* @description Details text indicating how many bytes were sent in a WebSocket message
*/
webSocketDataLength: 'Data length',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/DetailsView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export function buildWarningElementsForEvent(
event: Trace.Types.Events.Event, parsedTrace: Trace.Handlers.Types.ParsedTrace): HTMLSpanElement[] {
const warnings = parsedTrace.Warnings.perEvent.get(event);
const warningElements: HTMLSpanElement[] = [];
if (!warnings) {
return warningElements;
}
for (const warning of warnings) {
const duration = Trace.Helpers.Timing.microToMilli(Trace.Types.Timing.Micro(event.dur || 0));
const span = document.createElement('span');
switch (warning) {
case 'FORCED_REFLOW': {
const forcedReflowLink = UI.XLink.XLink.create(
'https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing#avoid-forced-synchronous-layouts',
i18nString(UIStrings.forcedReflow), undefined, undefined, 'forced-reflow');
span.appendChild(i18n.i18n.getFormatLocalizedString(
str_, UIStrings.sIsALikelyPerformanceBottleneck, {PH1: forcedReflowLink}));
break;
}
case 'IDLE_CALLBACK_OVER_TIME': {
if (!Trace.Types.Events.isFireIdleCallback(event)) {
break;
}
const exceededMs =
i18n.TimeUtilities.millisToString((duration || 0) - event.args.data['allottedMilliseconds'], true);
span.textContent = i18nString(UIStrings.idleCallbackExecutionExtended, {PH1: exceededMs});
break;
}
case 'LONG_TASK': {
const longTaskLink = UI.XLink.XLink.create(
'https://web.dev/optimize-long-tasks/', i18nString(UIStrings.longTask), undefined, undefined, 'long-tasks');
span.appendChild(i18n.i18n.getFormatLocalizedString(
str_, UIStrings.sTookS,
{PH1: longTaskLink, PH2: i18n.TimeUtilities.millisToString((duration || 0), true)}));
break;
}
case 'LONG_INTERACTION': {
const longInteractionINPLink = UI.XLink.XLink.create(
'https://web.dev/inp', i18nString(UIStrings.longInteractionINP), undefined, undefined, 'long-interaction');
span.appendChild(i18n.i18n.getFormatLocalizedString(
str_, UIStrings.sIsLikelyPoorPageResponsiveness, {PH1: longInteractionINPLink}));
break;
}
default: {
Platform.assertNever(warning, `Unhandled warning type ${warning}`);
}
}
warningElements.push(span);
}
return warningElements;
}
export interface DetailRow {
key: string;
value: string;
}
export function buildRowsForWebSocketEvent(
event: Trace.Types.Events.WebSocketCreate|Trace.Types.Events.WebSocketInfo|Trace.Types.Events.WebSocketTransfer,
parsedTrace: Trace.Handlers.Types.ParsedTrace): readonly DetailRow[] {
const rows: DetailRow[] = [];
const initiator = parsedTrace.Initiators.eventToInitiator.get(event);
if (initiator && Trace.Types.Events.isWebSocketCreate(initiator)) {
// The initiator will be a WebSocketCreate, but this check helps TypeScript to understand.
rows.push({key: i18n.i18n.lockedString('URL'), value: initiator.args.data.url});
if (initiator.args.data.websocketProtocol) {
rows.push({key: i18nString(UIStrings.websocketProtocol), value: initiator.args.data.websocketProtocol});
}
} else if (Trace.Types.Events.isWebSocketCreate(event)) {
rows.push({key: i18n.i18n.lockedString('URL'), value: event.args.data.url});
if (event.args.data.websocketProtocol) {
rows.push({key: i18nString(UIStrings.websocketProtocol), value: event.args.data.websocketProtocol});
}
}
if (Trace.Types.Events.isWebSocketTransfer(event)) {
if (event.args.data.dataLength) {
rows.push({
key: i18nString(UIStrings.webSocketDataLength),
value: `${i18nString(UIStrings.webSocketBytes, {PH1: event.args.data.dataLength})}`,
});
}
}
return rows;
}
/**
* This method does not output any content but instead takes a list of
* invalidations and groups them, doing some processing of the data to collect
* invalidations grouped by the reason/cause.
* It also returns all BackendNodeIds that are related to these invalidations
* so that they can be fetched via CDP.
* It is exported only for testing purposes.
**/
export function generateInvalidationsList(
invalidations: Trace.Types.Events.InvalidationTrackingEvent[],
): {
groupedByReason: Record<string, Trace.Types.Events.InvalidationTrackingEvent[]>,
backendNodeIds: Set<Protocol.DOM.BackendNodeId>,
} {
const groupedByReason: Record<string, Trace.Types.Events.InvalidationTrackingEvent[]> = {};
const backendNodeIds = new Set<Protocol.DOM.BackendNodeId>();
for (const invalidation of invalidations) {
backendNodeIds.add(invalidation.args.data.nodeId);
let reason = invalidation.args.data.reason || 'unknown';
// ScheduleStyle events do not always have a reason, but if they tell us
// via their data what changed, we can update the reason that we show to
// the user.
if (reason === 'unknown' && Trace.Types.Events.isScheduleStyleInvalidationTracking(invalidation) &&
invalidation.args.data.invalidatedSelectorId) {
switch (invalidation.args.data.invalidatedSelectorId) {
case 'attribute':
reason = 'Attribute';
if (invalidation.args.data.changedAttribute) {
reason += ` (${invalidation.args.data.changedAttribute})`;
}
break;
case 'class':
reason = 'Class';
if (invalidation.args.data.changedClass) {
reason += ` (${invalidation.args.data.changedClass})`;
}
break;
case 'id':
reason = 'Id';
if (invalidation.args.data.changedId) {
reason += ` (${invalidation.args.data.changedId})`;
}
break;
}
}
if (reason === 'PseudoClass' && Trace.Types.Events.isStyleRecalcInvalidationTracking(invalidation) &&
invalidation.args.data.extraData) {
// This will append the `:focus` onto the reason.
reason += invalidation.args.data.extraData;
}
if (reason === 'Attribute' && Trace.Types.Events.isStyleRecalcInvalidationTracking(invalidation) &&
invalidation.args.data.extraData) {
// Append the attribute that changed.
reason += ` (${invalidation.args.data.extraData})`;
}
if (reason === 'StyleInvalidator') {
// These events give us some extra metadata but are not in isolation that
// useful and end up duplicating information from other tracking events,
// so we do not include these in the UI.
continue;
}
const existing = groupedByReason[reason] || [];
existing.push(invalidation);
groupedByReason[reason] = existing;
}
return {groupedByReason, backendNodeIds};
}