chrome-devtools-frontend
Version:
Chrome DevTools UI
303 lines (271 loc) • 11.5 kB
text/typescript
// Copyright 2024 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 * as Protocol from '../../../generated/protocol.js';
import type * as Trace from '../../../models/trace/trace.js';
import * as ThemeSupport from '../../../ui/legacy/theme_support/theme_support.js';
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
import type {CompareRating} from './MetricCompareStrings.js';
const UIStrings = {
/**
*@description ms is the short form of milli-seconds and the placeholder is a decimal number.
* The shortest form or abbreviation of milliseconds should be used, as there is
* limited room in this UI.
*@example {2.14} PH1
*/
fms: '{PH1}[ms]()',
/**
*@description s is short for seconds and the placeholder is a decimal number
* The shortest form or abbreviation of seconds should be used, as there is
* limited room in this UI.
*@example {2.14} PH1
*/
fs: '{PH1}[s]()',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/Utils.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export enum NetworkCategory {
DOC = 'Doc',
CSS = 'CSS',
JS = 'JS',
FONT = 'Font',
IMG = 'Img',
MEDIA = 'Media',
WASM = 'Wasm',
OTHER = 'Other',
}
export function networkResourceCategory(request: Trace.Types.Events.SyntheticNetworkRequest): NetworkCategory {
const {mimeType} = request.args.data;
switch (request.args.data.resourceType) {
case Protocol.Network.ResourceType.Document:
return NetworkCategory.DOC;
case Protocol.Network.ResourceType.Stylesheet:
return NetworkCategory.CSS;
case Protocol.Network.ResourceType.Image:
return NetworkCategory.IMG;
case Protocol.Network.ResourceType.Media:
return NetworkCategory.MEDIA;
case Protocol.Network.ResourceType.Font:
return NetworkCategory.FONT;
case Protocol.Network.ResourceType.Script:
case Protocol.Network.ResourceType.WebSocket:
return NetworkCategory.JS;
default:
// FWIW, all the other (current) resourceTypes are:
// TextTrack, XHR, Fetch, Prefetch, EventSource, Manifest, SignedExchange, Ping, CSPViolationReport, Preflight, Other
// Traces before Feb 2024 don't have `resourceType`.
// We'll keep mimeType logic for a couple years to avoid grey network requests for last year's traces.
return mimeType === undefined ? NetworkCategory.OTHER :
mimeType.endsWith('/css') ? NetworkCategory.CSS :
mimeType.endsWith('javascript') ? NetworkCategory.JS :
mimeType.startsWith('image/') ? NetworkCategory.IMG :
mimeType.startsWith('audio/') || mimeType.startsWith('video/') ? NetworkCategory.MEDIA :
mimeType.startsWith('font/') || mimeType.includes('font-') ? NetworkCategory.FONT :
mimeType === 'application/wasm' ? NetworkCategory.WASM :
mimeType.startsWith('text/') ? NetworkCategory.DOC :
// Ultimate fallback:
NetworkCategory.OTHER;
}
}
export function colorForNetworkCategory(category: NetworkCategory): string {
// TODO: These should align with `baseResourceTypeColors` from `NetworkWaterfallColumn`.
let cssVarName = '--app-color-system';
switch (category) {
case NetworkCategory.DOC:
cssVarName = '--app-color-doc';
break;
case NetworkCategory.JS:
cssVarName = '--app-color-scripting';
break;
case NetworkCategory.CSS:
cssVarName = '--app-color-css';
break;
case NetworkCategory.IMG:
cssVarName = '--app-color-image';
break;
case NetworkCategory.MEDIA:
cssVarName = '--app-color-media';
break;
case NetworkCategory.FONT:
cssVarName = '--app-color-font';
break;
case NetworkCategory.WASM:
cssVarName = '--app-color-wasm';
break;
case NetworkCategory.OTHER:
default:
cssVarName = '--app-color-system';
break;
}
return ThemeSupport.ThemeSupport.instance().getComputedValue(cssVarName);
}
export function colorForNetworkRequest(request: Trace.Types.Events.SyntheticNetworkRequest): string {
const category = networkResourceCategory(request);
return colorForNetworkCategory(category);
}
export type MetricRating = 'good'|'needs-improvement'|'poor';
export type MetricThresholds = [number, number];
// TODO: Consolidate our metric rating logic with the trace engine.
export const LCP_THRESHOLDS = [2500, 4000] as MetricThresholds;
export const CLS_THRESHOLDS = [0.1, 0.25] as MetricThresholds;
export const INP_THRESHOLDS = [200, 500] as MetricThresholds;
export function rateMetric(value: number, thresholds: MetricThresholds): MetricRating {
if (value <= thresholds[0]) {
return 'good';
}
if (value <= thresholds[1]) {
return 'needs-improvement';
}
return 'poor';
}
/**
* Ensure to also include `metricValueStyles.css` when generating metric value elements.
*/
export function renderMetricValue(
jslogContext: string, value: number|undefined, thresholds: MetricThresholds, format: (value: number) => string,
options?: {dim?: boolean}): HTMLElement {
const metricValueEl = document.createElement('span');
metricValueEl.classList.add('metric-value');
if (value === undefined) {
metricValueEl.classList.add('waiting');
metricValueEl.textContent = '-';
return metricValueEl;
}
metricValueEl.textContent = format(value);
const rating = rateMetric(value, thresholds);
metricValueEl.classList.add(rating);
// Ensure we log impressions of each section. We purposefully add this here
// because if we don't have field data (dealt with in the undefined branch
// above), we do not want to log an impression on it.
metricValueEl.setAttribute('jslog', `${VisualLogging.section(jslogContext)}`);
if (options?.dim) {
metricValueEl.classList.add('dim');
}
return metricValueEl;
}
export interface NumberWithUnitString {
element: HTMLElement;
text: string;
}
/**
* These methods format numbers with units in a way that allows the unit portion to be styled specifically.
* They return a text string (the usual string resulting from formatting a number), and an HTMLSpanElement.
* The element contains the formatted number, with a nested span element for the unit portion: `.unit`.
*
* This formatting is locale-aware. This is accomplished by utilizing the fact that UIStrings passthru
* markdown link syntax: `[text that will be translated](not translated)`. The result
* is a translated string like this: `[t̂éx̂t́ t̂h́ât́ ŵíl̂ĺ b̂é t̂ŕâńŝĺât́êd́](not translated)`. This is used within
* insight components to localize markdown content. But here, we utilize it to parse a localized string.
*
* If the parsing fails, we fallback to i18n.TimeUtilities, and there will be no `.unit` element.
*
* As of this writing, our only locale where the unit comes before the number is `sw`, ex: `Sek {PH1}`.
*
new Intl.NumberFormat('sw', {
style: 'unit',
unit: 'millisecond',
unitDisplay: 'narrow'
}).format(10); // 'ms 10'
*
*/
export namespace NumberWithUnit {
export function parse(text: string): {firstPart: string, unitPart: string, lastPart: string}|null {
const startBracket = text.indexOf('[');
const endBracket = startBracket !== -1 && text.indexOf(']', startBracket);
const startParen = endBracket && text.indexOf('(', endBracket);
const endParen = startParen && text.indexOf(')', startParen);
if (!endParen || endParen === -1) {
return null;
}
const firstPart = text.substring(0, startBracket);
const unitPart = text.substring(startBracket + 1, endBracket);
const lastPart = text.substring(endParen + 1); // skips `]()`
return {firstPart, unitPart, lastPart};
}
export function formatMicroSecondsAsSeconds(time: Platform.Timing.MicroSeconds): NumberWithUnitString {
const element = document.createElement('span');
element.classList.add('number-with-unit');
const milliseconds = Platform.Timing.microSecondsToMilliSeconds(time);
const seconds = Platform.Timing.milliSecondsToSeconds(milliseconds);
const text = i18nString(UIStrings.fs, {PH1: (seconds).toFixed(2)});
const result = parse(text);
if (!result) {
// Some sort of problem with parsing, so fallback to not marking up the unit.
element.textContent = i18n.TimeUtilities.formatMicroSecondsAsSeconds(time);
return {text, element};
}
const {firstPart, unitPart, lastPart} = result;
if (firstPart) {
element.append(firstPart);
}
element.createChild('span', 'unit').textContent = unitPart;
if (lastPart) {
element.append(lastPart);
}
return {text: element.textContent ?? '', element};
}
export function formatMicroSecondsAsMillisFixed(time: Platform.Timing.MicroSeconds, fractionDigits = 0):
NumberWithUnitString {
const element = document.createElement('span');
element.classList.add('number-with-unit');
const milliseconds = Platform.Timing.microSecondsToMilliSeconds(time);
const text = i18nString(UIStrings.fms, {PH1: (milliseconds).toFixed(fractionDigits)});
const result = parse(text);
if (!result) {
// Some sort of problem with parsing, so fallback to not marking up the unit.
element.textContent = i18n.TimeUtilities.formatMicroSecondsAsMillisFixed(time);
return {text, element};
}
const {firstPart, unitPart, lastPart} = result;
if (firstPart) {
element.append(firstPart);
}
element.createChild('span', 'unit').textContent = unitPart;
if (lastPart) {
element.append(lastPart);
}
return {text: element.textContent ?? '', element};
}
}
/**
* Returns if the local value is better/worse/similar compared to field.
*/
export function determineCompareRating(
metric: 'LCP'|'CLS'|'INP', localValue: Trace.Types.Timing.Milli|number,
fieldValue: Trace.Types.Timing.Milli|number): CompareRating|undefined {
let thresholds: MetricThresholds;
let compareThreshold: number;
switch (metric) {
case 'LCP':
thresholds = LCP_THRESHOLDS;
compareThreshold = 1000;
break;
case 'CLS':
thresholds = CLS_THRESHOLDS;
compareThreshold = 0.1;
break;
case 'INP':
thresholds = INP_THRESHOLDS;
compareThreshold = 200;
break;
default:
Platform.assertNever(metric, `Unknown metric: ${metric}`);
}
const localRating = rateMetric(localValue, thresholds);
const fieldRating = rateMetric(fieldValue, thresholds);
// It's not worth highlighting a significant difference when both #s
// are rated "good"
if (localRating === 'good' && fieldRating === 'good') {
return 'similar';
}
if (localValue - fieldValue > compareThreshold) {
return 'worse';
}
if (fieldValue - localValue > compareThreshold) {
return 'better';
}
return 'similar';
}