chrome-devtools-frontend 1.0.1603822 → 1.0.1605219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORS +1 -0
- package/front_end/Tests.js +23 -7
- package/front_end/core/host/AidaClient.ts +153 -525
- package/front_end/core/host/AidaClientTypes.ts +470 -0
- package/front_end/core/host/AidaGcaTranslation.ts +231 -122
- package/front_end/core/host/GcaClient.ts +112 -0
- package/front_end/core/host/GcaTypes.ts +107 -155
- package/front_end/core/host/UserMetrics.ts +0 -1
- package/front_end/core/host/host.ts +2 -0
- package/front_end/core/protocol_client/DevToolsCDPConnection.ts +1 -1
- package/front_end/core/root/ExperimentNames.ts +0 -1
- package/front_end/core/root/Runtime.ts +5 -0
- package/front_end/core/sdk/AutofillModel.ts +3 -2
- package/front_end/core/sdk/CSSModel.ts +3 -4
- package/front_end/core/sdk/CSSProperty.ts +1 -1
- package/front_end/core/sdk/ConsoleModel.ts +15 -13
- package/front_end/core/sdk/CookieModel.ts +5 -4
- package/front_end/core/sdk/DOMDebuggerModel.ts +20 -14
- package/front_end/core/sdk/DOMModel.ts +24 -22
- package/front_end/core/sdk/DebuggerModel.ts +21 -36
- package/front_end/core/sdk/EmulationModel.ts +21 -23
- package/front_end/core/sdk/EventBreakpointsModel.ts +16 -6
- package/front_end/core/sdk/HeapProfilerModel.ts +4 -7
- package/front_end/core/sdk/IsolateManager.ts +12 -7
- package/front_end/core/sdk/NetworkManager.ts +35 -28
- package/front_end/core/sdk/OverlayModel.ts +10 -10
- package/front_end/core/sdk/PreloadingModel.ts +3 -4
- package/front_end/core/sdk/ServiceWorkerManager.ts +2 -3
- package/front_end/core/sdk/SourceMapScopesInfo.ts +93 -63
- package/front_end/entrypoints/formatter_worker/FormatterWorker.ts +1 -3
- package/front_end/entrypoints/heap_snapshot_worker/AllocationProfile.ts +22 -48
- package/front_end/entrypoints/heap_snapshot_worker/HeapSnapshot.ts +1 -1
- package/front_end/entrypoints/main/MainImpl.ts +0 -4
- package/front_end/generated/InspectorBackendCommands.ts +4 -0
- package/front_end/generated/SupportedCSSProperties.js +68 -2
- package/front_end/generated/protocol-mapping.d.ts +7 -0
- package/front_end/generated/protocol-proxy-api.d.ts +14 -0
- package/front_end/generated/protocol.ts +22 -0
- package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +31 -17
- package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +1 -1
- package/front_end/models/bindings/TempFile.ts +1 -4
- package/front_end/models/emulation/DeviceModeModel.ts +21 -26
- package/front_end/models/extensions/ExtensionAPI.ts +1 -1
- package/front_end/models/heap_snapshot_model/HeapSnapshotModel.ts +34 -47
- package/front_end/models/javascript_metadata/NativeFunctions.js +6 -6
- package/front_end/models/trace/insights/LCPDiscovery.ts +14 -6
- package/front_end/panels/ai_assistance/AiAssistancePanel.ts +85 -31
- package/front_end/panels/ai_assistance/components/ChatMessage.ts +50 -40
- package/front_end/panels/ai_assistance/components/ChatView.ts +3 -2
- package/front_end/panels/ai_assistance/components/WalkthroughView.ts +35 -7
- package/front_end/panels/ai_assistance/components/chatMessage.css +5 -1
- package/front_end/panels/application/ApplicationPanelSidebar.ts +6 -0
- package/front_end/panels/application/IndexedDBViews.ts +0 -9
- package/front_end/panels/application/WebMCPTreeElement.ts +35 -0
- package/front_end/panels/application/WebMCPView.ts +118 -0
- package/front_end/panels/application/application.ts +4 -0
- package/front_end/panels/application/webMCPView.css +65 -0
- package/front_end/panels/common/aiCodeCompletionSummaryToolbar.css +1 -1
- package/front_end/panels/console/consoleView.css +4 -0
- package/front_end/panels/elements/ElementsTreeElement.ts +91 -13
- package/front_end/panels/elements/StylesAiCodeCompletionProvider.ts +23 -2
- package/front_end/panels/elements/StylesSidebarPane.ts +85 -20
- package/front_end/panels/elements/elementsPanel.css +14 -0
- package/front_end/panels/elements/elementsTreeOutline.css +18 -0
- package/front_end/panels/network/NetworkLogViewColumns.ts +3 -3
- package/front_end/panels/profiler/HeapSnapshotDataGrids.ts +15 -19
- package/front_end/panels/profiler/HeapSnapshotGridNodes.ts +4 -2
- package/front_end/panels/profiler/HeapSnapshotProxy.ts +12 -16
- package/front_end/panels/profiler/HeapSnapshotView.ts +3 -10
- package/front_end/panels/profiler/ProfilesPanel.ts +5 -7
- package/front_end/panels/recorder/components/recordingView.css +0 -6
- package/front_end/panels/sensors/SensorsView.ts +28 -61
- package/front_end/panels/timeline/EventsTimelineTreeView.ts +2 -2
- package/front_end/panels/timeline/ThirdPartyTreeView.ts +35 -36
- package/front_end/panels/timeline/TimelineDetailsView.ts +47 -41
- package/front_end/panels/timeline/TimelinePanel.ts +1 -3
- package/front_end/panels/timeline/TimelineTreeView.ts +13 -13
- package/front_end/panels/timeline/components/LiveMetricsView.ts +852 -824
- package/front_end/panels/timeline/components/liveMetricsView.css +4 -0
- package/front_end/panels/timeline/thirdPartyTreeView.css +2 -2
- package/front_end/third_party/chromium/README.chromium +1 -1
- package/front_end/ui/components/buttons/button.css +2 -2
- package/front_end/ui/components/text_editor/AiCodeCompletionProvider.ts +0 -11
- package/front_end/ui/legacy/InspectorDrawerView.ts +206 -0
- package/front_end/ui/legacy/InspectorView.ts +41 -91
- package/front_end/ui/legacy/ViewManager.ts +14 -5
- package/front_end/ui/legacy/inspectorDrawerTabbedPane.css +5 -0
- package/front_end/ui/visual_logging/KnownContextValues.ts +2 -1
- package/package.json +1 -1
|
@@ -21,8 +21,6 @@ import * as EmulationModel from '../../../models/emulation/emulation.js';
|
|
|
21
21
|
import * as LiveMetrics from '../../../models/live-metrics/live-metrics.js';
|
|
22
22
|
import * as Trace from '../../../models/trace/trace.js';
|
|
23
23
|
import * as Buttons from '../../../ui/components/buttons/buttons.js';
|
|
24
|
-
import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
|
|
25
|
-
import * as LegacyWrapper from '../../../ui/components/legacy_wrapper/legacy_wrapper.js';
|
|
26
24
|
import type * as Menus from '../../../ui/components/menus/menus.js';
|
|
27
25
|
import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js';
|
|
28
26
|
import type * as Settings from '../../../ui/components/settings/settings.js';
|
|
@@ -40,7 +38,7 @@ import metricValueStyles from './metricValueStyles.css.js';
|
|
|
40
38
|
import {CLS_THRESHOLDS, INP_THRESHOLDS, renderMetricValue} from './Utils.js';
|
|
41
39
|
|
|
42
40
|
const {html, nothing} = Lit;
|
|
43
|
-
const {widget} = UI.Widget;
|
|
41
|
+
const {widget, widgetRef} = UI.Widget;
|
|
44
42
|
|
|
45
43
|
type DeviceOption = CrUXManager.DeviceScope|'AUTO';
|
|
46
44
|
|
|
@@ -305,933 +303,979 @@ const UIStrings = {
|
|
|
305
303
|
const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/LiveMetricsView.ts', UIStrings);
|
|
306
304
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
307
305
|
|
|
308
|
-
export
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
306
|
+
export interface ViewInput {
|
|
307
|
+
isNode: boolean;
|
|
308
|
+
lcpValue?: LiveMetrics.LcpValue;
|
|
309
|
+
clsValue?: LiveMetrics.ClsValue;
|
|
310
|
+
inpValue?: LiveMetrics.InpValue;
|
|
311
|
+
interactions: LiveMetrics.InteractionMap;
|
|
312
|
+
layoutShifts: LiveMetrics.LayoutShift[];
|
|
313
|
+
toggleRecordAction: UI.ActionRegistration.Action;
|
|
314
|
+
recordReloadAction: UI.ActionRegistration.Action;
|
|
315
|
+
cruxManager: CrUXManager.CrUXManager;
|
|
316
|
+
handlePageScopeSelected: (event: Menus.SelectMenu.SelectMenuItemSelectedEvent) => void;
|
|
317
|
+
handleDeviceOptionSelected: (event: Menus.SelectMenu.SelectMenuItemSelectedEvent) => void;
|
|
318
|
+
revealLayoutShiftCluster: (clusterIds: Set<LiveMetrics.LayoutShift['uniqueLayoutShiftId']>) => void;
|
|
319
|
+
revealInteraction: (interaction: LiveMetrics.Interaction) => void;
|
|
320
|
+
logExtraInteractionDetails: (interaction: LiveMetrics.Interaction) => void;
|
|
321
|
+
highlightedInteractionId?: string;
|
|
322
|
+
highlightedLayoutShiftClusterIds?: Set<string>;
|
|
323
|
+
}
|
|
320
324
|
|
|
321
|
-
|
|
322
|
-
|
|
325
|
+
export interface ViewOutput {
|
|
326
|
+
shouldKeepInteractionsScrolledToBottom?: () => boolean;
|
|
327
|
+
keepInteractionsScrolledToBottom?: () => void;
|
|
328
|
+
shouldKeepLayoutShiftsScrolledToBottom?: () => boolean;
|
|
329
|
+
keepLayoutShiftsScrolledToBottom?: () => void;
|
|
330
|
+
}
|
|
323
331
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
332
|
+
export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement|DocumentFragment) => void;
|
|
333
|
+
|
|
334
|
+
function getLcpFieldPhases(cruxManager: CrUXManager.CrUXManager): LiveMetrics.LcpValue['phases']|null {
|
|
335
|
+
const ttfb =
|
|
336
|
+
cruxManager.getSelectedFieldMetricData('largest_contentful_paint_image_time_to_first_byte')?.percentiles?.p75;
|
|
337
|
+
const loadDelay =
|
|
338
|
+
cruxManager.getSelectedFieldMetricData('largest_contentful_paint_image_resource_load_delay')?.percentiles?.p75;
|
|
339
|
+
const loadDuration =
|
|
340
|
+
cruxManager.getSelectedFieldMetricData('largest_contentful_paint_image_resource_load_duration')?.percentiles?.p75;
|
|
341
|
+
const renderDelay =
|
|
342
|
+
cruxManager.getSelectedFieldMetricData('largest_contentful_paint_image_element_render_delay')?.percentiles?.p75;
|
|
343
|
+
|
|
344
|
+
if (typeof ttfb !== 'number' || typeof loadDelay !== 'number' || typeof loadDuration !== 'number' ||
|
|
345
|
+
typeof renderDelay !== 'number') {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
330
348
|
|
|
331
|
-
|
|
332
|
-
|
|
349
|
+
return {
|
|
350
|
+
timeToFirstByte: Trace.Types.Timing.Milli(ttfb),
|
|
351
|
+
resourceLoadDelay: Trace.Types.Timing.Milli(loadDelay),
|
|
352
|
+
resourceLoadTime: Trace.Types.Timing.Milli(loadDuration),
|
|
353
|
+
elementRenderDelay: Trace.Types.Timing.Milli(renderDelay),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
333
356
|
|
|
334
|
-
|
|
335
|
-
|
|
357
|
+
function getNetworkRecTitle(cruxManager: CrUXManager.CrUXManager): string|null {
|
|
358
|
+
const response = cruxManager.getSelectedFieldMetricData('round_trip_time');
|
|
359
|
+
if (!response?.percentiles) {
|
|
360
|
+
return null;
|
|
336
361
|
}
|
|
337
362
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const hasNewLS = this.#layoutShifts.length < event.data.layoutShifts.length;
|
|
344
|
-
this.#layoutShifts = [...event.data.layoutShifts];
|
|
363
|
+
const rtt = Number(response.percentiles.p75);
|
|
364
|
+
if (!Number.isFinite(rtt)) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
345
367
|
|
|
346
|
-
|
|
347
|
-
|
|
368
|
+
if (rtt < RTT_MINIMUM) {
|
|
369
|
+
return i18nString(UIStrings.tryDisablingThrottling);
|
|
370
|
+
}
|
|
348
371
|
|
|
349
|
-
|
|
372
|
+
const conditions = SDK.NetworkManager.getRecommendedNetworkPreset(rtt);
|
|
373
|
+
if (!conditions) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
350
376
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
377
|
+
const title = typeof conditions.title === 'function' ? conditions.title() : conditions.title;
|
|
378
|
+
return i18nString(UIStrings.tryUsingThrottling, {PH1: title});
|
|
379
|
+
}
|
|
354
380
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
381
|
+
function getDeviceRec(cruxManager: CrUXManager.CrUXManager): string|null {
|
|
382
|
+
// `form_factors` metric is only populated if CrUX data is fetched for all devices.
|
|
383
|
+
const fractions =
|
|
384
|
+
cruxManager.getFieldResponse(cruxManager.fieldPageScope, 'ALL')?.record.metrics.form_factors?.fractions;
|
|
385
|
+
if (!fractions) {
|
|
386
|
+
return null;
|
|
358
387
|
}
|
|
359
388
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
389
|
+
return i18nString(UIStrings.percentDevices, {
|
|
390
|
+
PH1: Math.round(fractions.phone * 100),
|
|
391
|
+
PH2: Math.round(fractions.desktop * 100),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
364
394
|
|
|
365
|
-
|
|
395
|
+
function getPageScopeLabel(cruxManager: CrUXManager.CrUXManager, pageScope: CrUXManager.PageScope): string {
|
|
396
|
+
const key = cruxManager.pageResult?.[`${pageScope}-ALL` as const]?.record.key[pageScope];
|
|
397
|
+
if (key) {
|
|
398
|
+
return pageScope === 'url' ? i18nString(UIStrings.urlOptionWithKey, {PH1: key}) :
|
|
399
|
+
i18nString(UIStrings.originOptionWithKey, {PH1: key});
|
|
400
|
+
}
|
|
366
401
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// the element hasn't scrolled all the way to the bottom yet.
|
|
371
|
-
if (!isAtBottom && !this.#listIsScrolling) {
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
402
|
+
const baseLabel = pageScope === 'url' ? i18nString(UIStrings.urlOption) : i18nString(UIStrings.originOption);
|
|
403
|
+
return i18nString(UIStrings.needsDataOption, {PH1: baseLabel});
|
|
404
|
+
}
|
|
374
405
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
406
|
+
function getDeviceScopeDisplayName(deviceScope: CrUXManager.DeviceScope): string {
|
|
407
|
+
switch (deviceScope) {
|
|
408
|
+
case 'ALL':
|
|
409
|
+
return i18nString(UIStrings.allDevices);
|
|
410
|
+
case 'DESKTOP':
|
|
411
|
+
return i18nString(UIStrings.desktop);
|
|
412
|
+
case 'PHONE':
|
|
413
|
+
return i18nString(UIStrings.mobile);
|
|
414
|
+
case 'TABLET':
|
|
415
|
+
return i18nString(UIStrings.tablet);
|
|
384
416
|
}
|
|
417
|
+
}
|
|
385
418
|
|
|
386
|
-
|
|
387
|
-
|
|
419
|
+
function getLabelForDeviceOption(cruxManager: CrUXManager.CrUXManager, deviceOption: DeviceOption): string {
|
|
420
|
+
let baseLabel;
|
|
421
|
+
if (deviceOption === 'AUTO') {
|
|
422
|
+
const deviceScope = cruxManager.resolveDeviceOptionToScope(deviceOption);
|
|
423
|
+
const deviceScopeLabel = getDeviceScopeDisplayName(deviceScope);
|
|
424
|
+
baseLabel = i18nString(UIStrings.auto, {PH1: deviceScopeLabel});
|
|
425
|
+
} else {
|
|
426
|
+
baseLabel = getDeviceScopeDisplayName(deviceOption);
|
|
388
427
|
}
|
|
389
428
|
|
|
390
|
-
|
|
391
|
-
|
|
429
|
+
if (!cruxManager.pageResult) {
|
|
430
|
+
return i18nString(UIStrings.loadingOption, {PH1: baseLabel});
|
|
392
431
|
}
|
|
393
432
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
|
|
433
|
+
const result = cruxManager.getSelectedFieldResponse();
|
|
434
|
+
if (!result) {
|
|
435
|
+
return i18nString(UIStrings.needsDataOption, {PH1: baseLabel});
|
|
399
436
|
}
|
|
400
437
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
liveMetrics.addEventListener(LiveMetrics.Events.STATUS, this.#onMetricStatus, this);
|
|
438
|
+
return baseLabel;
|
|
439
|
+
}
|
|
404
440
|
|
|
405
|
-
|
|
406
|
-
|
|
441
|
+
function getCollectionPeriodRange(cruxManager: CrUXManager.CrUXManager): string|null {
|
|
442
|
+
const selectedResponse = cruxManager.getSelectedFieldResponse();
|
|
443
|
+
if (!selectedResponse) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
407
446
|
|
|
408
|
-
|
|
409
|
-
|
|
447
|
+
const {firstDate, lastDate} = selectedResponse.record.collectionPeriod;
|
|
448
|
+
|
|
449
|
+
const formattedFirstDate = new Date(
|
|
450
|
+
firstDate.year,
|
|
451
|
+
// CrUX month is 1-indexed but `Date` month is 0-indexed
|
|
452
|
+
firstDate.month - 1,
|
|
453
|
+
firstDate.day,
|
|
454
|
+
);
|
|
455
|
+
const formattedLastDate = new Date(
|
|
456
|
+
lastDate.year,
|
|
457
|
+
// CrUX month is 1-indexed but `Date` month is 0-indexed
|
|
458
|
+
lastDate.month - 1,
|
|
459
|
+
lastDate.day,
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const options: Intl.DateTimeFormatOptions = {
|
|
463
|
+
year: 'numeric',
|
|
464
|
+
month: 'short',
|
|
465
|
+
day: 'numeric',
|
|
466
|
+
};
|
|
410
467
|
|
|
411
|
-
|
|
412
|
-
|
|
468
|
+
return i18nString(UIStrings.dateRange, {
|
|
469
|
+
PH1: formattedFirstDate.toLocaleDateString(undefined, options),
|
|
470
|
+
PH2: formattedLastDate.toLocaleDateString(undefined, options),
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function createMetricCardRef(cardData: Omit<MetricCardData, 'tooltipContainer'>):
|
|
475
|
+
ReturnType<typeof Lit.Directives.ref> {
|
|
476
|
+
return Lit.Directives.ref(el => {
|
|
477
|
+
if (el instanceof HTMLElement) {
|
|
478
|
+
(el as HTMLElement & {data: MetricCardData}).data = {
|
|
479
|
+
...cardData,
|
|
480
|
+
tooltipContainer: (el.closest('.metric-cards') as HTMLElement) || undefined,
|
|
481
|
+
};
|
|
413
482
|
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
414
485
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
486
|
+
function renderLcpCard(input: ViewInput): Lit.LitTemplate {
|
|
487
|
+
const fieldData = input.cruxManager.getSelectedFieldMetricData('largest_contentful_paint');
|
|
488
|
+
const nodeLink =
|
|
489
|
+
input.lcpValue?.nodeRef && PanelsCommon.DOMLinkifier.Linkifier.instance().linkify(input.lcpValue?.nodeRef);
|
|
490
|
+
const phases = input.lcpValue?.phases;
|
|
491
|
+
|
|
492
|
+
const fieldPhases = getLcpFieldPhases(input.cruxManager);
|
|
493
|
+
|
|
494
|
+
// clang-format off
|
|
495
|
+
return html`
|
|
496
|
+
<devtools-metric-card ${createMetricCardRef({
|
|
497
|
+
metric: 'LCP',
|
|
498
|
+
localValue: input.lcpValue?.value,
|
|
499
|
+
fieldValue: fieldData?.percentiles?.p75,
|
|
500
|
+
histogram: fieldData?.histogram,
|
|
501
|
+
warnings: input.lcpValue?.warnings,
|
|
502
|
+
phases: phases && [
|
|
503
|
+
[i18nString(UIStrings.timeToFirstByte), phases.timeToFirstByte, fieldPhases?.timeToFirstByte],
|
|
504
|
+
[i18nString(UIStrings.resourceLoadDelay), phases.resourceLoadDelay, fieldPhases?.resourceLoadDelay],
|
|
505
|
+
[i18nString(UIStrings.resourceLoadDuration), phases.resourceLoadTime, fieldPhases?.resourceLoadTime],
|
|
506
|
+
[i18nString(UIStrings.elementRenderDelay), phases.elementRenderDelay, fieldPhases?.elementRenderDelay],
|
|
507
|
+
],
|
|
508
|
+
})}>
|
|
509
|
+
${nodeLink ? html`
|
|
510
|
+
<div class="related-info" slot="extra-info">
|
|
511
|
+
<span class="related-info-label">${i18nString(UIStrings.lcpElement)}</span>
|
|
512
|
+
<span class="related-info-link">
|
|
513
|
+
${widget(PanelsCommon.DOMLinkifier.DOMNodeLink, {node: input.lcpValue?.nodeRef})}
|
|
514
|
+
</span>
|
|
515
|
+
</div>
|
|
516
|
+
`
|
|
517
|
+
: nothing}
|
|
518
|
+
</devtools-metric-card>
|
|
519
|
+
`;
|
|
520
|
+
// clang-format on
|
|
521
|
+
}
|
|
422
522
|
|
|
423
|
-
|
|
424
|
-
|
|
523
|
+
function renderClsCard(input: ViewInput): Lit.LitTemplate {
|
|
524
|
+
const fieldData = input.cruxManager.getSelectedFieldMetricData('cumulative_layout_shift');
|
|
525
|
+
|
|
526
|
+
const clusterIds = new Set(input.clsValue?.clusterShiftIds || []);
|
|
527
|
+
const clusterIsVisible =
|
|
528
|
+
clusterIds.size > 0 && input.layoutShifts.some(layoutShift => clusterIds.has(layoutShift.uniqueLayoutShiftId));
|
|
529
|
+
|
|
530
|
+
// clang-format off
|
|
531
|
+
return html`
|
|
532
|
+
<devtools-metric-card ${createMetricCardRef({
|
|
533
|
+
metric: 'CLS',
|
|
534
|
+
localValue: input.clsValue?.value,
|
|
535
|
+
fieldValue: fieldData?.percentiles?.p75,
|
|
536
|
+
histogram: fieldData?.histogram,
|
|
537
|
+
warnings: input.clsValue?.warnings,
|
|
538
|
+
})}>
|
|
539
|
+
${clusterIsVisible ? html`
|
|
540
|
+
<div class="related-info" slot="extra-info">
|
|
541
|
+
<span class="related-info-label">${i18nString(UIStrings.worstCluster)}</span>
|
|
542
|
+
<button
|
|
543
|
+
class="link-to-log"
|
|
544
|
+
title=${i18nString(UIStrings.showClsCluster)}
|
|
545
|
+
@click=${() => input.revealLayoutShiftCluster(clusterIds)}
|
|
546
|
+
jslog=${VisualLogging.action('timeline.landing.show-cls-cluster').track({click: true})}
|
|
547
|
+
>${i18nString(UIStrings.numShifts, {shiftCount: clusterIds.size})}</button>
|
|
548
|
+
</div>
|
|
549
|
+
` : nothing}
|
|
550
|
+
</devtools-metric-card>
|
|
551
|
+
`;
|
|
552
|
+
// clang-format on
|
|
553
|
+
}
|
|
425
554
|
|
|
426
|
-
|
|
427
|
-
|
|
555
|
+
function renderInpCard(input: ViewInput): Lit.LitTemplate {
|
|
556
|
+
const fieldData = input.cruxManager.getSelectedFieldMetricData('interaction_to_next_paint');
|
|
557
|
+
const phases = input.inpValue?.phases;
|
|
558
|
+
const interaction = input.inpValue && input.interactions.get(input.inpValue.interactionId);
|
|
559
|
+
|
|
560
|
+
// clang-format off
|
|
561
|
+
return html`
|
|
562
|
+
<devtools-metric-card ${createMetricCardRef({
|
|
563
|
+
metric: 'INP',
|
|
564
|
+
localValue: input.inpValue?.value,
|
|
565
|
+
fieldValue: fieldData?.percentiles?.p75,
|
|
566
|
+
histogram: fieldData?.histogram,
|
|
567
|
+
warnings: input.inpValue?.warnings,
|
|
568
|
+
phases: phases && [
|
|
569
|
+
[i18nString(UIStrings.inputDelay), phases.inputDelay],
|
|
570
|
+
[i18nString(UIStrings.processingDuration), phases.processingDuration],
|
|
571
|
+
[i18nString(UIStrings.presentationDelay), phases.presentationDelay],
|
|
572
|
+
],
|
|
573
|
+
})}>
|
|
574
|
+
${interaction ? html`
|
|
575
|
+
<div class="related-info" slot="extra-info">
|
|
576
|
+
<span class="related-info-label">${i18nString(UIStrings.inpInteractionLink)}</span>
|
|
577
|
+
<button
|
|
578
|
+
class="link-to-log"
|
|
579
|
+
title=${i18nString(UIStrings.showInpInteraction)}
|
|
580
|
+
@click=${() => input.revealInteraction(interaction)}
|
|
581
|
+
jslog=${VisualLogging.action('timeline.landing.show-inp-interaction').track({click: true})}
|
|
582
|
+
>${interaction.interactionType}</button>
|
|
583
|
+
</div>
|
|
584
|
+
` : nothing}
|
|
585
|
+
</devtools-metric-card>
|
|
586
|
+
`;
|
|
587
|
+
// clang-format on
|
|
588
|
+
}
|
|
428
589
|
|
|
429
|
-
|
|
430
|
-
|
|
590
|
+
function renderRecordAction(action: UI.ActionRegistration.Action): Lit.LitTemplate {
|
|
591
|
+
function onClick(): void {
|
|
592
|
+
void action.execute();
|
|
431
593
|
}
|
|
432
594
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
595
|
+
// clang-format off
|
|
596
|
+
return html`
|
|
597
|
+
<div class="record-action">
|
|
598
|
+
<devtools-button @click=${onClick} .data=${{
|
|
599
|
+
variant: Buttons.Button.Variant.TEXT,
|
|
600
|
+
size: Buttons.Button.Size.REGULAR,
|
|
601
|
+
iconName: action.icon(),
|
|
602
|
+
title: action.title(),
|
|
603
|
+
jslogContext: action.id(),
|
|
604
|
+
} as Buttons.Button.ButtonData}>
|
|
605
|
+
${action.title()}
|
|
606
|
+
</devtools-button>
|
|
607
|
+
<span class="shortcut-label">${UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutTitleForAction(action.id())}</span>
|
|
608
|
+
</div>
|
|
609
|
+
`;
|
|
610
|
+
// clang-format on
|
|
611
|
+
}
|
|
450
612
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
613
|
+
function renderRecordingSettings(input: ViewInput): Lit.LitTemplate {
|
|
614
|
+
const fieldEnabled = input.cruxManager.getConfigSetting().get().enabled;
|
|
615
|
+
|
|
616
|
+
const deviceRec = getDeviceRec(input.cruxManager) || i18nString(UIStrings.notEnoughData);
|
|
617
|
+
const networkRec = getNetworkRecTitle(input.cruxManager) || i18nString(UIStrings.notEnoughData);
|
|
618
|
+
|
|
619
|
+
const recs = PanelsCommon.ThrottlingUtils.getThrottlingRecommendations();
|
|
620
|
+
|
|
621
|
+
// clang-format off
|
|
622
|
+
return html`
|
|
623
|
+
<h3 class="card-title">${i18nString(UIStrings.environmentSettings)}</h3>
|
|
624
|
+
<div class="device-toolbar-description">${md(i18nString(UIStrings.useDeviceToolbar))}</div>
|
|
625
|
+
${fieldEnabled ? html`
|
|
626
|
+
<ul class="environment-recs-list">
|
|
627
|
+
<li>${uiI18n.getFormatLocalizedStringTemplate(str_, UIStrings.device, {PH1: html`<span class="environment-rec">${deviceRec}</span>`})}</li>
|
|
628
|
+
<li>${uiI18n.getFormatLocalizedStringTemplate(str_, UIStrings.network, {PH1: html`<span class="environment-rec">${networkRec}</span>`})}</li>
|
|
629
|
+
</ul>
|
|
630
|
+
` : nothing}
|
|
631
|
+
<div class="environment-option">
|
|
632
|
+
${widget(CPUThrottlingSelector, {recommendedOption: recs.cpuOption})}
|
|
633
|
+
</div>
|
|
634
|
+
<div class="environment-option">
|
|
635
|
+
<devtools-network-throttling-selector .recommendedConditions=${recs.networkConditions}></devtools-network-throttling-selector>
|
|
636
|
+
</div>
|
|
637
|
+
<div class="environment-option">
|
|
638
|
+
<setting-checkbox
|
|
639
|
+
class="network-cache-setting"
|
|
640
|
+
.data=${{
|
|
641
|
+
setting: Common.Settings.Settings.instance().moduleSetting('cache-disabled'),
|
|
642
|
+
textOverride: i18nString(UIStrings.disableNetworkCache),
|
|
643
|
+
} as Settings.SettingCheckbox.SettingCheckboxData}
|
|
644
|
+
></setting-checkbox>
|
|
645
|
+
</div>
|
|
646
|
+
`;
|
|
647
|
+
// clang-format on
|
|
648
|
+
}
|
|
458
649
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
this.#lcpValue?.nodeRef && PanelsCommon.DOMLinkifier.Linkifier.instance().linkify(this.#lcpValue?.nodeRef);
|
|
463
|
-
const phases = this.#lcpValue?.phases;
|
|
464
|
-
|
|
465
|
-
const fieldPhases = this.#getLcpFieldPhases();
|
|
466
|
-
|
|
467
|
-
// clang-format off
|
|
468
|
-
return html`
|
|
469
|
-
<devtools-metric-card .data=${{
|
|
470
|
-
metric: 'LCP',
|
|
471
|
-
localValue: this.#lcpValue?.value,
|
|
472
|
-
fieldValue: fieldData?.percentiles?.p75,
|
|
473
|
-
histogram: fieldData?.histogram,
|
|
474
|
-
tooltipContainer: this.#tooltipContainerEl,
|
|
475
|
-
warnings: this.#lcpValue?.warnings,
|
|
476
|
-
phases: phases && [
|
|
477
|
-
[i18nString(UIStrings.timeToFirstByte), phases.timeToFirstByte, fieldPhases?.timeToFirstByte],
|
|
478
|
-
[i18nString(UIStrings.resourceLoadDelay), phases.resourceLoadDelay, fieldPhases?.resourceLoadDelay],
|
|
479
|
-
[i18nString(UIStrings.resourceLoadDuration), phases.resourceLoadTime, fieldPhases?.resourceLoadTime],
|
|
480
|
-
[i18nString(UIStrings.elementRenderDelay), phases.elementRenderDelay, fieldPhases?.elementRenderDelay],
|
|
481
|
-
],
|
|
482
|
-
} as MetricCardData}>
|
|
483
|
-
${nodeLink ? html`
|
|
484
|
-
<div class="related-info" slot="extra-info">
|
|
485
|
-
<span class="related-info-label">${i18nString(UIStrings.lcpElement)}</span>
|
|
486
|
-
<span class="related-info-link">
|
|
487
|
-
${widget(PanelsCommon.DOMLinkifier.DOMNodeLink, {node: this.#lcpValue?.nodeRef})}
|
|
488
|
-
</span>
|
|
489
|
-
</div>
|
|
490
|
-
`
|
|
491
|
-
: nothing}
|
|
492
|
-
</devtools-metric-card>
|
|
493
|
-
`;
|
|
494
|
-
// clang-format on
|
|
650
|
+
function renderPageScopeSetting(input: ViewInput): Lit.LitTemplate {
|
|
651
|
+
if (!input.cruxManager.getConfigSetting().get().enabled) {
|
|
652
|
+
return Lit.nothing;
|
|
495
653
|
}
|
|
496
654
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
</devtools-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
655
|
+
const urlLabel = getPageScopeLabel(input.cruxManager, 'url');
|
|
656
|
+
const originLabel = getPageScopeLabel(input.cruxManager, 'origin');
|
|
657
|
+
|
|
658
|
+
const buttonTitle = input.cruxManager.fieldPageScope === 'url' ? urlLabel : originLabel;
|
|
659
|
+
const accessibleTitle = i18nString(UIStrings.showFieldDataForPage, {PH1: buttonTitle});
|
|
660
|
+
|
|
661
|
+
// If there is no data at all we should force users to switch pages or reconfigure CrUX.
|
|
662
|
+
const shouldDisable = !input.cruxManager.pageResult?.['url-ALL'] && !input.cruxManager.pageResult?.['origin-ALL'];
|
|
663
|
+
|
|
664
|
+
/* eslint-disable @devtools/no-deprecated-component-usages */
|
|
665
|
+
return html`
|
|
666
|
+
<devtools-select-menu
|
|
667
|
+
id="page-scope-select"
|
|
668
|
+
class="field-data-option"
|
|
669
|
+
@selectmenuselected=${input.handlePageScopeSelected}
|
|
670
|
+
.showDivider=${true}
|
|
671
|
+
.showArrow=${true}
|
|
672
|
+
.sideButton=${false}
|
|
673
|
+
.showSelectedItem=${true}
|
|
674
|
+
.buttonTitle=${buttonTitle}
|
|
675
|
+
.disabled=${shouldDisable}
|
|
676
|
+
title=${accessibleTitle}
|
|
677
|
+
>
|
|
678
|
+
<devtools-menu-item
|
|
679
|
+
.value=${'url'}
|
|
680
|
+
.selected=${input.cruxManager.fieldPageScope === 'url'}
|
|
681
|
+
>
|
|
682
|
+
${urlLabel}
|
|
683
|
+
</devtools-menu-item>
|
|
684
|
+
<devtools-menu-item
|
|
685
|
+
.value=${'origin'}
|
|
686
|
+
.selected=${input.cruxManager.fieldPageScope === 'origin'}
|
|
687
|
+
>
|
|
688
|
+
${originLabel}
|
|
689
|
+
</devtools-menu-item>
|
|
690
|
+
</devtools-select-menu>
|
|
691
|
+
`;
|
|
692
|
+
/* eslint-enable @devtools/no-deprecated-component-usages */
|
|
693
|
+
}
|
|
529
694
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const interaction = this.#inpValue && this.#interactions.get(this.#inpValue.interactionId);
|
|
534
|
-
|
|
535
|
-
// clang-format off
|
|
536
|
-
return html`
|
|
537
|
-
<devtools-metric-card .data=${{
|
|
538
|
-
metric: 'INP',
|
|
539
|
-
localValue: this.#inpValue?.value,
|
|
540
|
-
fieldValue: fieldData?.percentiles?.p75,
|
|
541
|
-
histogram: fieldData?.histogram,
|
|
542
|
-
tooltipContainer: this.#tooltipContainerEl,
|
|
543
|
-
warnings: this.#inpValue?.warnings,
|
|
544
|
-
phases: phases && [
|
|
545
|
-
[i18nString(UIStrings.inputDelay), phases.inputDelay],
|
|
546
|
-
[i18nString(UIStrings.processingDuration), phases.processingDuration],
|
|
547
|
-
[i18nString(UIStrings.presentationDelay), phases.presentationDelay],
|
|
548
|
-
],
|
|
549
|
-
} as MetricCardData}>
|
|
550
|
-
${interaction ? html`
|
|
551
|
-
<div class="related-info" slot="extra-info">
|
|
552
|
-
<span class="related-info-label">${i18nString(UIStrings.inpInteractionLink)}</span>
|
|
553
|
-
<button
|
|
554
|
-
class="link-to-log"
|
|
555
|
-
title=${i18nString(UIStrings.showInpInteraction)}
|
|
556
|
-
@click=${() => this.#revealInteraction(interaction)}
|
|
557
|
-
jslog=${VisualLogging.action('timeline.landing.show-inp-interaction').track({click: true})}
|
|
558
|
-
>${interaction.interactionType}</button>
|
|
559
|
-
</div>
|
|
560
|
-
` : nothing}
|
|
561
|
-
</devtools-metric-card>
|
|
562
|
-
`;
|
|
563
|
-
// clang-format on
|
|
695
|
+
function renderDeviceScopeSetting(input: ViewInput): Lit.LitTemplate {
|
|
696
|
+
if (!input.cruxManager.getConfigSetting().get().enabled) {
|
|
697
|
+
return Lit.nothing;
|
|
564
698
|
}
|
|
565
699
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
700
|
+
// If there is no data at all we should force users to try adjusting the page scope
|
|
701
|
+
// before coming back to this option.
|
|
702
|
+
const shouldDisable = !input.cruxManager.getFieldResponse(input.cruxManager.fieldPageScope, 'ALL');
|
|
703
|
+
|
|
704
|
+
const currentDeviceLabel = getLabelForDeviceOption(input.cruxManager, input.cruxManager.fieldDeviceOption);
|
|
705
|
+
|
|
706
|
+
// clang-format off
|
|
707
|
+
/* eslint-disable @devtools/no-deprecated-component-usages */
|
|
708
|
+
return html`
|
|
709
|
+
<devtools-select-menu
|
|
710
|
+
id="device-scope-select"
|
|
711
|
+
class="field-data-option"
|
|
712
|
+
@selectmenuselected=${input.handleDeviceOptionSelected}
|
|
713
|
+
.showDivider=${true}
|
|
714
|
+
.showArrow=${true}
|
|
715
|
+
.sideButton=${false}
|
|
716
|
+
.showSelectedItem=${true}
|
|
717
|
+
.buttonTitle=${i18nString(UIStrings.device, {PH1: currentDeviceLabel})}
|
|
718
|
+
.disabled=${shouldDisable}
|
|
719
|
+
title=${i18nString(UIStrings.showFieldDataForDevice, {PH1: currentDeviceLabel})}
|
|
720
|
+
>
|
|
721
|
+
${DEVICE_OPTION_LIST.map(deviceOption => {
|
|
722
|
+
return html`
|
|
723
|
+
<devtools-menu-item
|
|
724
|
+
.value=${deviceOption}
|
|
725
|
+
.selected=${input.cruxManager.fieldDeviceOption === deviceOption}
|
|
726
|
+
>
|
|
727
|
+
${getLabelForDeviceOption(input.cruxManager, deviceOption)}
|
|
728
|
+
</devtools-menu-item>
|
|
729
|
+
`;
|
|
730
|
+
})}
|
|
731
|
+
</devtools-select-menu>
|
|
732
|
+
`;
|
|
733
|
+
/* eslint-enable @devtools/no-deprecated-component-usages */
|
|
734
|
+
// clang-format on
|
|
735
|
+
}
|
|
570
736
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
title: action.title(),
|
|
579
|
-
jslogContext: action.id(),
|
|
580
|
-
} as Buttons.Button.ButtonData}>
|
|
581
|
-
${action.title()}
|
|
582
|
-
</devtools-button>
|
|
583
|
-
<span class="shortcut-label">${UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutTitleForAction(action.id())}</span>
|
|
584
|
-
</div>
|
|
585
|
-
`;
|
|
586
|
-
// clang-format on
|
|
737
|
+
function renderFieldDataHistoryLink(cruxManager: CrUXManager.CrUXManager): Lit.LitTemplate {
|
|
738
|
+
if (!cruxManager.getConfigSetting().get().enabled) {
|
|
739
|
+
return Lit.nothing;
|
|
740
|
+
}
|
|
741
|
+
const normalizedUrl = cruxManager.pageResult?.normalizedUrl;
|
|
742
|
+
if (!normalizedUrl) {
|
|
743
|
+
return Lit.nothing;
|
|
587
744
|
}
|
|
745
|
+
const tmp = new URL('https://cruxvis.withgoogle.com/');
|
|
746
|
+
tmp.searchParams.set('view', 'cwvsummary');
|
|
747
|
+
tmp.searchParams.set('url', normalizedUrl);
|
|
748
|
+
// identifier must be 'origin' or 'url'.
|
|
749
|
+
const identifier = cruxManager.fieldPageScope;
|
|
750
|
+
tmp.searchParams.set('identifier', identifier);
|
|
751
|
+
// device must be one 'PHONE', 'DESKTOP', 'TABLET', or 'ALL'.
|
|
752
|
+
const device = cruxManager.getSelectedDeviceScope();
|
|
753
|
+
tmp.searchParams.set('device', device);
|
|
754
|
+
const cruxVis = `${tmp.origin}/#/${tmp.search}`;
|
|
755
|
+
return html`
|
|
756
|
+
(<devtools-link href=${cruxVis}
|
|
757
|
+
class="local-field-link"
|
|
758
|
+
title=${i18nString(UIStrings.fieldDataHistoryTooltip)}
|
|
759
|
+
>${i18nString(UIStrings.fieldDataHistoryLink)}</devtools-link>)
|
|
760
|
+
`;
|
|
761
|
+
}
|
|
588
762
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (!response?.percentiles) {
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
763
|
+
function renderCollectionPeriod(cruxManager: CrUXManager.CrUXManager): Lit.LitTemplate {
|
|
764
|
+
const range = getCollectionPeriodRange(cruxManager);
|
|
594
765
|
|
|
595
|
-
|
|
596
|
-
if (!Number.isFinite(rtt)) {
|
|
597
|
-
return null;
|
|
598
|
-
}
|
|
766
|
+
const dateText = range || i18nString(UIStrings.notEnoughData);
|
|
599
767
|
|
|
600
|
-
|
|
601
|
-
return i18nString(UIStrings.tryDisablingThrottling);
|
|
602
|
-
}
|
|
768
|
+
const fieldDataHistoryLink = range ? renderFieldDataHistoryLink(cruxManager) : Lit.nothing;
|
|
603
769
|
|
|
604
|
-
|
|
605
|
-
if (!conditions) {
|
|
606
|
-
return null;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const title = typeof conditions.title === 'function' ? conditions.title() : conditions.title;
|
|
610
|
-
return i18nString(UIStrings.tryUsingThrottling, {PH1: title});
|
|
611
|
-
}
|
|
770
|
+
const warnings = cruxManager.pageResult?.warnings || [];
|
|
612
771
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
772
|
+
return html`
|
|
773
|
+
<div class="field-data-message">
|
|
774
|
+
<div>${uiI18n.getFormatLocalizedStringTemplate(str_, UIStrings.collectionPeriod, {
|
|
775
|
+
PH1: html`<span class="collection-period-range">${dateText}</span>`,
|
|
776
|
+
})} ${fieldDataHistoryLink}</div>
|
|
777
|
+
${warnings.map(warning => html`
|
|
778
|
+
<div class="field-data-warning">${warning}</div>
|
|
779
|
+
`)}
|
|
780
|
+
</div>
|
|
781
|
+
`;
|
|
782
|
+
}
|
|
620
783
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
});
|
|
784
|
+
function renderFieldDataMessage(cruxManager: CrUXManager.CrUXManager): Lit.LitTemplate {
|
|
785
|
+
if (cruxManager.getConfigSetting().get().enabled) {
|
|
786
|
+
return renderCollectionPeriod(cruxManager);
|
|
625
787
|
}
|
|
626
788
|
|
|
627
|
-
|
|
628
|
-
|
|
789
|
+
// clang-format off
|
|
790
|
+
return html`
|
|
791
|
+
<div class="field-data-message">
|
|
792
|
+
${uiI18n.getFormatLocalizedStringTemplate(
|
|
793
|
+
str_,
|
|
794
|
+
UIStrings.seeHowYourLocalMetricsCompare,
|
|
795
|
+
{ PH1: html`<devtools-link href="https://developer.chrome.com/docs/crux">${i18n.i18n.lockedString('Chrome UX Report')}</devtools-link>` },
|
|
796
|
+
)}
|
|
797
|
+
</div>
|
|
798
|
+
`;
|
|
799
|
+
// clang-format on
|
|
800
|
+
}
|
|
629
801
|
|
|
630
|
-
|
|
631
|
-
deviceRecEl.classList.add('environment-rec');
|
|
632
|
-
deviceRecEl.textContent = this.#getDeviceRec() || i18nString(UIStrings.notEnoughData);
|
|
802
|
+
const listIsScrolling = new WeakMap<HTMLElement, boolean>();
|
|
633
803
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
804
|
+
function shouldKeepScrolledToBottom(listEl: HTMLElement): boolean {
|
|
805
|
+
if (!listEl.checkVisibility()) {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
637
808
|
|
|
638
|
-
|
|
809
|
+
const isAtBottom = Math.abs(listEl.scrollHeight - listEl.clientHeight - listEl.scrollTop) <= 1;
|
|
639
810
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
<li>${uiI18n.getFormatLocalizedString(str_, UIStrings.device, {PH1: deviceRecEl})}</li>
|
|
647
|
-
<li>${uiI18n.getFormatLocalizedString(str_, UIStrings.network, {PH1: networkRecEl})}</li>
|
|
648
|
-
</ul>
|
|
649
|
-
` : nothing}
|
|
650
|
-
<div class="environment-option">
|
|
651
|
-
${widget(CPUThrottlingSelector, {recommendedOption: recs.cpuOption})}
|
|
652
|
-
</div>
|
|
653
|
-
<div class="environment-option">
|
|
654
|
-
<devtools-network-throttling-selector .recommendedConditions=${recs.networkConditions}></devtools-network-throttling-selector>
|
|
655
|
-
</div>
|
|
656
|
-
<div class="environment-option">
|
|
657
|
-
<setting-checkbox
|
|
658
|
-
class="network-cache-setting"
|
|
659
|
-
.data=${{
|
|
660
|
-
setting: Common.Settings.Settings.instance().moduleSetting('cache-disabled'),
|
|
661
|
-
textOverride: i18nString(UIStrings.disableNetworkCache),
|
|
662
|
-
} as Settings.SettingCheckbox.SettingCheckboxData}
|
|
663
|
-
></setting-checkbox>
|
|
664
|
-
</div>
|
|
665
|
-
`;
|
|
666
|
-
// clang-format on
|
|
667
|
-
}
|
|
811
|
+
// We shouldn't scroll to the bottom if the list wasn't already at the bottom.
|
|
812
|
+
// However, if a new item appears while the animation for a previous item is still going,
|
|
813
|
+
// then we should "finish" the scroll by sending another scroll command even if the scroll position
|
|
814
|
+
// the element hasn't scrolled all the way to the bottom yet.
|
|
815
|
+
return isAtBottom || Boolean(listIsScrolling.get(listEl));
|
|
816
|
+
}
|
|
668
817
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
}
|
|
818
|
+
function keepScrolledToBottom(listEl: HTMLElement): void {
|
|
819
|
+
requestAnimationFrame(() => {
|
|
820
|
+
listIsScrolling.set(listEl, true);
|
|
821
|
+
listEl.addEventListener('scrollend', () => {
|
|
822
|
+
listIsScrolling.set(listEl, false);
|
|
823
|
+
}, {once: true});
|
|
824
|
+
listEl.scrollTo({top: listEl.scrollHeight, behavior: 'smooth'});
|
|
825
|
+
});
|
|
826
|
+
}
|
|
675
827
|
|
|
676
|
-
|
|
677
|
-
|
|
828
|
+
function renderInteractionsLog(input: ViewInput, output: ViewOutput): Lit.LitTemplate {
|
|
829
|
+
if (!input.interactions.size) {
|
|
830
|
+
return Lit.nothing;
|
|
678
831
|
}
|
|
679
832
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
833
|
+
// clang-format off
|
|
834
|
+
return html`
|
|
835
|
+
<ol class="log"
|
|
836
|
+
slot="interactions-log-content"
|
|
837
|
+
${Lit.Directives.ref(el => {
|
|
838
|
+
if (el instanceof HTMLElement) {
|
|
839
|
+
output.shouldKeepInteractionsScrolledToBottom = () => {
|
|
840
|
+
return shouldKeepScrolledToBottom(el);
|
|
841
|
+
};
|
|
842
|
+
output.keepInteractionsScrolledToBottom = () => {
|
|
843
|
+
keepScrolledToBottom(el);
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
})}
|
|
847
|
+
>
|
|
848
|
+
${input.interactions.values().map(interaction => {
|
|
849
|
+
const metricValue = renderMetricValue(
|
|
850
|
+
'timeline.landing.interaction-event-timing',
|
|
851
|
+
interaction.duration,
|
|
852
|
+
INP_THRESHOLDS,
|
|
853
|
+
v => i18n.TimeUtilities.preciseMillisToString(v),
|
|
854
|
+
{dim: true},
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
const isP98Excluded = input.inpValue && input.inpValue.value < interaction.duration;
|
|
858
|
+
const isInp = input.inpValue?.interactionId === interaction.interactionId;
|
|
859
|
+
|
|
860
|
+
return html`
|
|
861
|
+
<li id=${interaction.interactionId} class="log-item interaction" tabindex="-1">
|
|
862
|
+
<details>
|
|
863
|
+
<summary>
|
|
864
|
+
<span class="interaction-type">
|
|
865
|
+
${interaction.interactionType} ${isInp ?
|
|
866
|
+
html`<span class="interaction-inp-chip" title=${i18nString(UIStrings.inpInteraction)}>INP</span>`
|
|
867
|
+
: nothing}
|
|
868
|
+
</span>
|
|
869
|
+
<span class="interaction-node">
|
|
870
|
+
${widget(PanelsCommon.DOMLinkifier.DOMNodeLink, {node: interaction.nodeRef})}
|
|
871
|
+
</span>
|
|
872
|
+
${isP98Excluded ? html`<devtools-icon
|
|
873
|
+
class="interaction-info"
|
|
874
|
+
name="info"
|
|
875
|
+
title=${i18nString(UIStrings.interactionExcluded)}
|
|
876
|
+
></devtools-icon>` : nothing}
|
|
877
|
+
<span class="interaction-duration">${metricValue}</span>
|
|
878
|
+
</summary>
|
|
879
|
+
<div class="phase-table" role="table">
|
|
880
|
+
<div class="phase-table-row phase-table-header-row" role="row">
|
|
881
|
+
<div role="columnheader">${i18nString(UIStrings.phase)}</div>
|
|
882
|
+
<div role="columnheader">
|
|
883
|
+
${interaction.longAnimationFrameTimings.length ? html`
|
|
884
|
+
<button
|
|
885
|
+
class="log-extra-details-button"
|
|
886
|
+
title=${i18nString(UIStrings.logToConsole)}
|
|
887
|
+
@click=${() => input.logExtraInteractionDetails(interaction)}
|
|
888
|
+
>${i18nString(UIStrings.duration)}</button>
|
|
889
|
+
` : i18nString(UIStrings.duration)}
|
|
890
|
+
</div>
|
|
891
|
+
</div>
|
|
892
|
+
<div class="phase-table-row" role="row">
|
|
893
|
+
<div role="cell">${i18nString(UIStrings.inputDelay)}</div>
|
|
894
|
+
<div role="cell">${Math.round(interaction.phases.inputDelay)}</div>
|
|
895
|
+
</div>
|
|
896
|
+
<div class="phase-table-row" role="row">
|
|
897
|
+
<div role="cell">${i18nString(UIStrings.processingDuration)}</div>
|
|
898
|
+
<div role="cell">${Math.round(interaction.phases.processingDuration)}</div>
|
|
899
|
+
</div>
|
|
900
|
+
<div class="phase-table-row" role="row">
|
|
901
|
+
<div role="cell">${i18nString(UIStrings.presentationDelay)}</div>
|
|
902
|
+
<div role="cell">${Math.round(interaction.phases.presentationDelay)}</div>
|
|
903
|
+
</div>
|
|
904
|
+
</div>
|
|
905
|
+
</details>
|
|
906
|
+
</li>
|
|
907
|
+
`;
|
|
908
|
+
})}
|
|
909
|
+
</ol>
|
|
910
|
+
`;
|
|
911
|
+
// clang-format on
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function renderLayoutShiftsLog(input: ViewInput, output: ViewOutput): Lit.LitTemplate {
|
|
915
|
+
if (!input.layoutShifts.length) {
|
|
916
|
+
return Lit.nothing;
|
|
687
917
|
}
|
|
688
918
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
919
|
+
// clang-format off
|
|
920
|
+
return html`
|
|
921
|
+
<ol class="log"
|
|
922
|
+
slot="layout-shifts-log-content"
|
|
923
|
+
${Lit.Directives.ref(el => {
|
|
924
|
+
if (el instanceof HTMLElement) {
|
|
925
|
+
output.shouldKeepLayoutShiftsScrolledToBottom = () => {
|
|
926
|
+
return shouldKeepScrolledToBottom(el);
|
|
927
|
+
};
|
|
928
|
+
output.keepLayoutShiftsScrolledToBottom = () => {
|
|
929
|
+
keepScrolledToBottom(el);
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
})}
|
|
933
|
+
>
|
|
934
|
+
${input.layoutShifts.map(layoutShift => {
|
|
935
|
+
const metricValue = renderMetricValue(
|
|
936
|
+
'timeline.landing.layout-shift-event-score',
|
|
937
|
+
layoutShift.score,
|
|
938
|
+
CLS_THRESHOLDS,
|
|
939
|
+
// CLS value is 2 decimal places, but individual shift scores tend to be much smaller
|
|
940
|
+
// so we expand the precision here.
|
|
941
|
+
v => v.toFixed(4),
|
|
942
|
+
{dim: true},
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
return html`
|
|
946
|
+
<li id=${layoutShift.uniqueLayoutShiftId} class="log-item layout-shift" tabindex="-1">
|
|
947
|
+
<div class="layout-shift-score">Layout shift score: ${metricValue}</div>
|
|
948
|
+
<div class="layout-shift-nodes">
|
|
949
|
+
${layoutShift.affectedNodeRefs.map(node => html`
|
|
950
|
+
<div class="layout-shift-node">
|
|
951
|
+
${widget(PanelsCommon.DOMLinkifier.DOMNodeLink, {node})}
|
|
952
|
+
</div>
|
|
953
|
+
`)}
|
|
954
|
+
</div>
|
|
955
|
+
</li>
|
|
956
|
+
`;
|
|
957
|
+
})}
|
|
958
|
+
</ol>
|
|
959
|
+
`;
|
|
960
|
+
// clang-format on
|
|
961
|
+
}
|
|
693
962
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
.showDivider=${true}
|
|
710
|
-
.showArrow=${true}
|
|
711
|
-
.sideButton=${false}
|
|
712
|
-
.showSelectedItem=${true}
|
|
713
|
-
.buttonTitle=${buttonTitle}
|
|
714
|
-
.disabled=${shouldDisable}
|
|
715
|
-
title=${accessibleTitle}
|
|
963
|
+
function renderLogSection(input: ViewInput, output: ViewOutput): Lit.LitTemplate {
|
|
964
|
+
// clang-format off
|
|
965
|
+
return html`
|
|
966
|
+
<section
|
|
967
|
+
class="logs-section"
|
|
968
|
+
aria-label=${i18nString(UIStrings.eventLogs)}
|
|
969
|
+
>
|
|
970
|
+
<devtools-widget ${widget(LiveMetricsLogs)}
|
|
971
|
+
${widgetRef(LiveMetricsLogs, widget => {
|
|
972
|
+
if (input.highlightedInteractionId) {
|
|
973
|
+
widget.selectTab('interactions');
|
|
974
|
+
} else if (input.highlightedLayoutShiftClusterIds?.size) {
|
|
975
|
+
widget.selectTab('layout-shifts');
|
|
976
|
+
}
|
|
977
|
+
})}
|
|
716
978
|
>
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
.value=${'origin'}
|
|
725
|
-
.selected=${this.#cruxManager.fieldPageScope === 'origin'}
|
|
726
|
-
>
|
|
727
|
-
${originLabel}
|
|
728
|
-
</devtools-menu-item>
|
|
729
|
-
</devtools-select-menu>
|
|
730
|
-
`;
|
|
731
|
-
/* eslint-enable @devtools/no-deprecated-component-usages */
|
|
732
|
-
}
|
|
979
|
+
${renderInteractionsLog(input, output)}
|
|
980
|
+
${renderLayoutShiftsLog(input, output)}
|
|
981
|
+
</devtools-widget>
|
|
982
|
+
</section>
|
|
983
|
+
`;
|
|
984
|
+
// clang-format on
|
|
985
|
+
}
|
|
733
986
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
987
|
+
function renderNodeView(input: ViewInput): Lit.LitTemplate {
|
|
988
|
+
return html`
|
|
989
|
+
<style>${liveMetricsViewStyles}</style>
|
|
990
|
+
<style>${metricValueStyles}</style>
|
|
991
|
+
<div class="node-view">
|
|
992
|
+
<main>
|
|
993
|
+
<h2 class="section-title">${i18nString(UIStrings.nodePerformanceTimeline)}</h2>
|
|
994
|
+
<div class="node-description">${i18nString(UIStrings.nodeClickToRecord)}</div>
|
|
995
|
+
<div class="record-action-card">${renderRecordAction(input.toggleRecordAction)}</div>
|
|
996
|
+
</main>
|
|
997
|
+
</div>
|
|
998
|
+
`;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
export const DEFAULT_VIEW: View = (input, output, target) => {
|
|
1002
|
+
if (input.isNode) {
|
|
1003
|
+
Lit.render(renderNodeView(input), target, {host: input});
|
|
1004
|
+
return;
|
|
745
1005
|
}
|
|
746
1006
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1007
|
+
const fieldEnabled = input.cruxManager.getConfigSetting().get().enabled;
|
|
1008
|
+
const liveMetricsTitle =
|
|
1009
|
+
fieldEnabled ? i18nString(UIStrings.localAndFieldMetrics) : i18nString(UIStrings.localMetrics);
|
|
1010
|
+
|
|
1011
|
+
const helpLink = 'https://web.dev/articles/lab-and-field-data-differences#lab_data_versus_field_data' as
|
|
1012
|
+
Platform.DevToolsPath.UrlString;
|
|
1013
|
+
|
|
1014
|
+
// clang-format off
|
|
1015
|
+
const outputTemplate = html`
|
|
1016
|
+
<style>${liveMetricsViewStyles}</style>
|
|
1017
|
+
<style>${metricValueStyles}</style>
|
|
1018
|
+
<div class="container">
|
|
1019
|
+
<div class="live-metrics-view">
|
|
1020
|
+
<main class="live-metrics">
|
|
1021
|
+
<h2 class="section-title">${liveMetricsTitle}</h2>
|
|
1022
|
+
<div class="metric-cards">
|
|
1023
|
+
<div id="lcp">
|
|
1024
|
+
${renderLcpCard(input)}
|
|
1025
|
+
</div>
|
|
1026
|
+
<div id="cls">
|
|
1027
|
+
${renderClsCard(input)}
|
|
1028
|
+
</div>
|
|
1029
|
+
<div id="inp">
|
|
1030
|
+
${renderInpCard(input)}
|
|
1031
|
+
</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
<devtools-link
|
|
1034
|
+
href=${helpLink}
|
|
1035
|
+
class="local-field-link"
|
|
1036
|
+
title=${i18nString(UIStrings.localFieldLearnMoreTooltip)}
|
|
1037
|
+
>${i18nString(UIStrings.localFieldLearnMoreLink)}</devtools-link>
|
|
1038
|
+
${renderLogSection(input, output)}
|
|
1039
|
+
</main>
|
|
1040
|
+
<aside class="next-steps" aria-labelledby="next-steps-section-title">
|
|
1041
|
+
<h2 id="next-steps-section-title" class="section-title">${i18nString(UIStrings.nextSteps)}</h2>
|
|
1042
|
+
<div id="field-setup" class="settings-card">
|
|
1043
|
+
<h3 class="card-title">${i18nString(UIStrings.fieldMetricsTitle)}</h3>
|
|
1044
|
+
${renderFieldDataMessage(input.cruxManager)}
|
|
1045
|
+
${renderPageScopeSetting(input)}
|
|
1046
|
+
${renderDeviceScopeSetting(input)}
|
|
1047
|
+
<div class="field-setup-buttons">
|
|
1048
|
+
<devtools-field-settings-dialog></devtools-field-settings-dialog>
|
|
1049
|
+
</div>
|
|
1050
|
+
</div>
|
|
1051
|
+
<div id="recording-settings" class="settings-card">
|
|
1052
|
+
${renderRecordingSettings(input)}
|
|
1053
|
+
</div>
|
|
1054
|
+
<div id="record" class="record-action-card">
|
|
1055
|
+
${renderRecordAction(input.toggleRecordAction)}
|
|
1056
|
+
</div>
|
|
1057
|
+
<div id="record-page-load" class="record-action-card">
|
|
1058
|
+
${renderRecordAction(input.recordReloadAction)}
|
|
1059
|
+
</div>
|
|
1060
|
+
</aside>
|
|
1061
|
+
</div>
|
|
1062
|
+
</div>
|
|
1063
|
+
`;
|
|
1064
|
+
Lit.render(outputTemplate, target, {host: input});
|
|
1065
|
+
|
|
1066
|
+
if (input.highlightedInteractionId) {
|
|
1067
|
+
const interactionEl = target.querySelector<HTMLElement>('#' + CSS.escape(input.highlightedInteractionId));
|
|
1068
|
+
if (interactionEl) {
|
|
1069
|
+
void RenderCoordinator.write(() => {
|
|
1070
|
+
interactionEl.scrollIntoView({
|
|
1071
|
+
block: 'center',
|
|
1072
|
+
});
|
|
1073
|
+
interactionEl.focus();
|
|
1074
|
+
UI.UIUtils.runCSSAnimationOnce(interactionEl, 'highlight');
|
|
1075
|
+
});
|
|
755
1076
|
}
|
|
1077
|
+
}
|
|
756
1078
|
|
|
757
|
-
|
|
758
|
-
|
|
1079
|
+
if (input.highlightedLayoutShiftClusterIds?.size) {
|
|
1080
|
+
const layoutShiftEls: HTMLElement[] = [];
|
|
1081
|
+
for (const shiftId of input.highlightedLayoutShiftClusterIds) {
|
|
1082
|
+
const layoutShiftEl = target.querySelector<HTMLElement>('#' + CSS.escape(shiftId));
|
|
1083
|
+
if (layoutShiftEl) {
|
|
1084
|
+
layoutShiftEls.push(layoutShiftEl);
|
|
1085
|
+
}
|
|
759
1086
|
}
|
|
760
1087
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1088
|
+
if (layoutShiftEls.length) {
|
|
1089
|
+
void RenderCoordinator.write(() => {
|
|
1090
|
+
layoutShiftEls[0].scrollIntoView({
|
|
1091
|
+
block: 'start',
|
|
1092
|
+
});
|
|
1093
|
+
layoutShiftEls[0].focus();
|
|
1094
|
+
for (const layoutShiftEl of layoutShiftEls) {
|
|
1095
|
+
UI.UIUtils.runCSSAnimationOnce(layoutShiftEl, 'highlight');
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
764
1098
|
}
|
|
765
|
-
|
|
766
|
-
return baseLabel;
|
|
767
1099
|
}
|
|
1100
|
+
};
|
|
768
1101
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
|
|
772
|
-
}
|
|
1102
|
+
export class LiveMetricsView extends UI.Widget.Widget {
|
|
1103
|
+
isNode = Root.Runtime.Runtime.isNode();
|
|
773
1104
|
|
|
774
|
-
#
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1105
|
+
#lcpValue?: LiveMetrics.LcpValue;
|
|
1106
|
+
#clsValue?: LiveMetrics.ClsValue;
|
|
1107
|
+
#inpValue?: LiveMetrics.InpValue;
|
|
1108
|
+
#interactions: LiveMetrics.InteractionMap = new Map();
|
|
1109
|
+
#layoutShifts: LiveMetrics.LayoutShift[] = [];
|
|
778
1110
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const shouldDisable = !this.#cruxManager.getFieldResponse(this.#cruxManager.fieldPageScope, 'ALL');
|
|
782
|
-
|
|
783
|
-
const currentDeviceLabel = this.#getLabelForDeviceOption(this.#cruxManager.fieldDeviceOption);
|
|
784
|
-
|
|
785
|
-
// clang-format off
|
|
786
|
-
/* eslint-disable @devtools/no-deprecated-component-usages */
|
|
787
|
-
return html`
|
|
788
|
-
<devtools-select-menu
|
|
789
|
-
id="device-scope-select"
|
|
790
|
-
class="field-data-option"
|
|
791
|
-
@selectmenuselected=${this.#onDeviceOptionMenuItemSelected}
|
|
792
|
-
.showDivider=${true}
|
|
793
|
-
.showArrow=${true}
|
|
794
|
-
.sideButton=${false}
|
|
795
|
-
.showSelectedItem=${true}
|
|
796
|
-
.buttonTitle=${i18nString(UIStrings.device, {PH1: currentDeviceLabel})}
|
|
797
|
-
.disabled=${shouldDisable}
|
|
798
|
-
title=${i18nString(UIStrings.showFieldDataForDevice, {PH1: currentDeviceLabel})}
|
|
799
|
-
>
|
|
800
|
-
${DEVICE_OPTION_LIST.map(deviceOption => {
|
|
801
|
-
return html`
|
|
802
|
-
<devtools-menu-item
|
|
803
|
-
.value=${deviceOption}
|
|
804
|
-
.selected=${this.#cruxManager.fieldDeviceOption === deviceOption}
|
|
805
|
-
>
|
|
806
|
-
${this.#getLabelForDeviceOption(deviceOption)}
|
|
807
|
-
</devtools-menu-item>
|
|
808
|
-
`;
|
|
809
|
-
})}
|
|
810
|
-
</devtools-select-menu>
|
|
811
|
-
`;
|
|
812
|
-
/* eslint-enable @devtools/no-deprecated-component-usages */
|
|
813
|
-
// clang-format on
|
|
814
|
-
}
|
|
1111
|
+
#highlightedInteractionId = '';
|
|
1112
|
+
#highlightedLayoutShiftClusterIds = new Set<string>();
|
|
815
1113
|
|
|
816
|
-
#
|
|
817
|
-
const selectedResponse = this.#cruxManager.getSelectedFieldResponse();
|
|
818
|
-
if (!selectedResponse) {
|
|
819
|
-
return null;
|
|
820
|
-
}
|
|
1114
|
+
#cruxManager = CrUXManager.CrUXManager.instance();
|
|
821
1115
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
const formattedFirstDate = new Date(
|
|
825
|
-
firstDate.year,
|
|
826
|
-
// CrUX month is 1-indexed but `Date` month is 0-indexed
|
|
827
|
-
firstDate.month - 1,
|
|
828
|
-
firstDate.day,
|
|
829
|
-
);
|
|
830
|
-
const formattedLastDate = new Date(
|
|
831
|
-
lastDate.year,
|
|
832
|
-
// CrUX month is 1-indexed but `Date` month is 0-indexed
|
|
833
|
-
lastDate.month - 1,
|
|
834
|
-
lastDate.day,
|
|
835
|
-
);
|
|
836
|
-
|
|
837
|
-
const options: Intl.DateTimeFormatOptions = {
|
|
838
|
-
year: 'numeric',
|
|
839
|
-
month: 'short',
|
|
840
|
-
day: 'numeric',
|
|
841
|
-
};
|
|
1116
|
+
#toggleRecordAction: UI.ActionRegistration.Action;
|
|
1117
|
+
#recordReloadAction: UI.ActionRegistration.Action;
|
|
842
1118
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
});
|
|
847
|
-
}
|
|
1119
|
+
#view: View;
|
|
1120
|
+
#viewOutput: ViewOutput = {};
|
|
1121
|
+
#deviceModeModel = EmulationModel.DeviceModeModel.DeviceModeModel.tryInstance();
|
|
848
1122
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
}
|
|
853
|
-
const normalizedUrl = this.#cruxManager.pageResult?.normalizedUrl;
|
|
854
|
-
if (!normalizedUrl) {
|
|
855
|
-
return Lit.nothing;
|
|
856
|
-
}
|
|
857
|
-
const tmp = new URL('https://cruxvis.withgoogle.com/');
|
|
858
|
-
tmp.searchParams.set('view', 'cwvsummary');
|
|
859
|
-
tmp.searchParams.set('url', normalizedUrl);
|
|
860
|
-
// identifier must be 'origin' or 'url'.
|
|
861
|
-
const identifier = this.#cruxManager.fieldPageScope;
|
|
862
|
-
tmp.searchParams.set('identifier', identifier);
|
|
863
|
-
// device must be one 'PHONE', 'DESKTOP', 'TABLET', or 'ALL'.
|
|
864
|
-
const device = this.#cruxManager.getSelectedDeviceScope();
|
|
865
|
-
tmp.searchParams.set('device', device);
|
|
866
|
-
const cruxVis = `${tmp.origin}/#/${tmp.search}`;
|
|
867
|
-
return html`
|
|
868
|
-
(<devtools-link href=${cruxVis}
|
|
869
|
-
class="local-field-link"
|
|
870
|
-
title=${i18nString(UIStrings.fieldDataHistoryTooltip)}
|
|
871
|
-
>${i18nString(UIStrings.fieldDataHistoryLink)}</devtools-link>)
|
|
872
|
-
`;
|
|
873
|
-
}
|
|
1123
|
+
constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) {
|
|
1124
|
+
super(element, {useShadowDom: true});
|
|
1125
|
+
this.#view = view;
|
|
874
1126
|
|
|
875
|
-
|
|
876
|
-
|
|
1127
|
+
this.#toggleRecordAction = UI.ActionRegistry.ActionRegistry.instance().getAction('timeline.toggle-recording');
|
|
1128
|
+
this.#recordReloadAction = UI.ActionRegistry.ActionRegistry.instance().getAction('timeline.record-reload');
|
|
1129
|
+
}
|
|
877
1130
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1131
|
+
async #onMetricStatus(event: {data: LiveMetrics.StatusEvent}): Promise<void> {
|
|
1132
|
+
this.#lcpValue = event.data.lcp;
|
|
1133
|
+
this.#clsValue = event.data.cls;
|
|
1134
|
+
this.#inpValue = event.data.inp;
|
|
881
1135
|
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
});
|
|
1136
|
+
const hasNewLS = this.#layoutShifts.length < event.data.layoutShifts.length;
|
|
1137
|
+
this.#layoutShifts = [...event.data.layoutShifts];
|
|
885
1138
|
|
|
886
|
-
const
|
|
1139
|
+
const hasNewInteraction = this.#interactions.size < event.data.interactions.size;
|
|
1140
|
+
this.#interactions = new Map(event.data.interactions);
|
|
887
1141
|
|
|
888
|
-
const
|
|
1142
|
+
const shouldScrollInteractions = hasNewInteraction && this.#viewOutput.shouldKeepInteractionsScrolledToBottom?.();
|
|
1143
|
+
const shouldScrollLS = hasNewLS && this.#viewOutput.shouldKeepLayoutShiftsScrolledToBottom?.();
|
|
889
1144
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
<div>${message} ${fieldDataHistoryLink}</div>
|
|
893
|
-
${warnings.map(warning => html`
|
|
894
|
-
<div class="field-data-warning">${warning}</div>
|
|
895
|
-
`)}
|
|
896
|
-
</div>
|
|
897
|
-
`;
|
|
898
|
-
}
|
|
1145
|
+
this.requestUpdate();
|
|
1146
|
+
await this.updateComplete;
|
|
899
1147
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
return this.#renderCollectionPeriod();
|
|
1148
|
+
if (shouldScrollInteractions) {
|
|
1149
|
+
this.#viewOutput.keepInteractionsScrolledToBottom?.();
|
|
903
1150
|
}
|
|
904
1151
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
${uiI18n.getFormatLocalizedStringTemplate(
|
|
909
|
-
str_,
|
|
910
|
-
UIStrings.seeHowYourLocalMetricsCompare,
|
|
911
|
-
{ PH1: html`<devtools-link href="https://developer.chrome.com/docs/crux">${i18n.i18n.lockedString('Chrome UX Report')}</devtools-link>` },
|
|
912
|
-
)}
|
|
913
|
-
</div>
|
|
914
|
-
`;
|
|
915
|
-
// clang-format on
|
|
1152
|
+
if (shouldScrollLS) {
|
|
1153
|
+
this.#viewOutput.keepLayoutShiftsScrolledToBottom?.();
|
|
1154
|
+
}
|
|
916
1155
|
}
|
|
917
1156
|
|
|
918
|
-
#
|
|
919
|
-
|
|
920
|
-
return html`
|
|
921
|
-
<section
|
|
922
|
-
class="logs-section"
|
|
923
|
-
aria-label=${i18nString(UIStrings.eventLogs)}
|
|
924
|
-
>
|
|
925
|
-
<devtools-live-metrics-logs
|
|
926
|
-
${Lit.Directives.ref(el => {
|
|
927
|
-
if (el instanceof HTMLElement) {
|
|
928
|
-
this.#logsEl = el as LiveMetricsLogs;
|
|
929
|
-
}
|
|
930
|
-
})}
|
|
931
|
-
>
|
|
932
|
-
${this.#renderInteractionsLog()}
|
|
933
|
-
${this.#renderLayoutShiftsLog()}
|
|
934
|
-
</devtools-live-metrics-logs>
|
|
935
|
-
</section>
|
|
936
|
-
`;
|
|
937
|
-
// clang-format on
|
|
1157
|
+
#onFieldDataChanged(): void {
|
|
1158
|
+
this.requestUpdate();
|
|
938
1159
|
}
|
|
939
1160
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (!interactionEl || !this.#logsEl) {
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
const success = this.#logsEl.selectTab('interactions');
|
|
947
|
-
if (!success) {
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
await RenderCoordinator.write(() => {
|
|
952
|
-
interactionEl.scrollIntoView({
|
|
953
|
-
block: 'center',
|
|
954
|
-
});
|
|
955
|
-
interactionEl.focus();
|
|
956
|
-
UI.UIUtils.runCSSAnimationOnce(interactionEl, 'highlight');
|
|
957
|
-
});
|
|
1161
|
+
#onEmulationChanged(): void {
|
|
1162
|
+
this.requestUpdate();
|
|
958
1163
|
}
|
|
959
1164
|
|
|
960
|
-
async #
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
await Common.Console.Console.instance().showPromise();
|
|
1165
|
+
async #refreshFieldDataForCurrentPage(): Promise<void> {
|
|
1166
|
+
if (!this.isNode) {
|
|
1167
|
+
await this.#cruxManager.refresh();
|
|
964
1168
|
}
|
|
1169
|
+
this.requestUpdate();
|
|
965
1170
|
}
|
|
966
1171
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1172
|
+
override wasShown(): void {
|
|
1173
|
+
super.wasShown();
|
|
1174
|
+
const liveMetrics = LiveMetrics.LiveMetrics.instance();
|
|
1175
|
+
liveMetrics.addEventListener(LiveMetrics.Events.STATUS, this.#onMetricStatus, this);
|
|
971
1176
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
<ol class="log"
|
|
975
|
-
slot="interactions-log-content"
|
|
976
|
-
${Lit.Directives.ref(el => {
|
|
977
|
-
if (el instanceof HTMLElement) {
|
|
978
|
-
this.#interactionsListEl = el as HTMLElement;
|
|
979
|
-
}
|
|
980
|
-
})}
|
|
981
|
-
>
|
|
982
|
-
${this.#interactions.values().map(interaction => {
|
|
983
|
-
const metricValue = renderMetricValue(
|
|
984
|
-
'timeline.landing.interaction-event-timing',
|
|
985
|
-
interaction.duration,
|
|
986
|
-
INP_THRESHOLDS,
|
|
987
|
-
v => i18n.TimeUtilities.preciseMillisToString(v),
|
|
988
|
-
{dim: true},
|
|
989
|
-
);
|
|
990
|
-
|
|
991
|
-
const isP98Excluded = this.#inpValue && this.#inpValue.value < interaction.duration;
|
|
992
|
-
const isInp = this.#inpValue?.interactionId === interaction.interactionId;
|
|
993
|
-
|
|
994
|
-
return html`
|
|
995
|
-
<li id=${interaction.interactionId} class="log-item interaction" tabindex="-1">
|
|
996
|
-
<details>
|
|
997
|
-
<summary>
|
|
998
|
-
<span class="interaction-type">
|
|
999
|
-
${interaction.interactionType} ${isInp ?
|
|
1000
|
-
html`<span class="interaction-inp-chip" title=${i18nString(UIStrings.inpInteraction)}>INP</span>`
|
|
1001
|
-
: nothing}
|
|
1002
|
-
</span>
|
|
1003
|
-
<span class="interaction-node">
|
|
1004
|
-
${widget(PanelsCommon.DOMLinkifier.DOMNodeLink, {node: interaction.nodeRef})}
|
|
1005
|
-
</span>
|
|
1006
|
-
${isP98Excluded ? html`<devtools-icon
|
|
1007
|
-
class="interaction-info"
|
|
1008
|
-
name="info"
|
|
1009
|
-
title=${i18nString(UIStrings.interactionExcluded)}
|
|
1010
|
-
></devtools-icon>` : nothing}
|
|
1011
|
-
<span class="interaction-duration">${metricValue}</span>
|
|
1012
|
-
</summary>
|
|
1013
|
-
<div class="phase-table" role="table">
|
|
1014
|
-
<div class="phase-table-row phase-table-header-row" role="row">
|
|
1015
|
-
<div role="columnheader">${i18nString(UIStrings.phase)}</div>
|
|
1016
|
-
<div role="columnheader">
|
|
1017
|
-
${interaction.longAnimationFrameTimings.length ? html`
|
|
1018
|
-
<button
|
|
1019
|
-
class="log-extra-details-button"
|
|
1020
|
-
title=${i18nString(UIStrings.logToConsole)}
|
|
1021
|
-
@click=${() => this.#logExtraInteractionDetails(interaction)}
|
|
1022
|
-
>${i18nString(UIStrings.duration)}</button>
|
|
1023
|
-
` : i18nString(UIStrings.duration)}
|
|
1024
|
-
</div>
|
|
1025
|
-
</div>
|
|
1026
|
-
<div class="phase-table-row" role="row">
|
|
1027
|
-
<div role="cell">${i18nString(UIStrings.inputDelay)}</div>
|
|
1028
|
-
<div role="cell">${Math.round(interaction.phases.inputDelay)}</div>
|
|
1029
|
-
</div>
|
|
1030
|
-
<div class="phase-table-row" role="row">
|
|
1031
|
-
<div role="cell">${i18nString(UIStrings.processingDuration)}</div>
|
|
1032
|
-
<div role="cell">${Math.round(interaction.phases.processingDuration)}</div>
|
|
1033
|
-
</div>
|
|
1034
|
-
<div class="phase-table-row" role="row">
|
|
1035
|
-
<div role="cell">${i18nString(UIStrings.presentationDelay)}</div>
|
|
1036
|
-
<div role="cell">${Math.round(interaction.phases.presentationDelay)}</div>
|
|
1037
|
-
</div>
|
|
1038
|
-
</div>
|
|
1039
|
-
</details>
|
|
1040
|
-
</li>
|
|
1041
|
-
`;
|
|
1042
|
-
})}
|
|
1043
|
-
</ol>
|
|
1044
|
-
`;
|
|
1045
|
-
// clang-format on
|
|
1046
|
-
}
|
|
1177
|
+
const cruxManager = CrUXManager.CrUXManager.instance();
|
|
1178
|
+
cruxManager.addEventListener(CrUXManager.Events.FIELD_DATA_CHANGED, this.#onFieldDataChanged, this);
|
|
1047
1179
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1180
|
+
this.#deviceModeModel?.addEventListener(
|
|
1181
|
+
EmulationModel.DeviceModeModel.Events.UPDATED, this.#onEmulationChanged, this);
|
|
1052
1182
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
const layoutShiftEl = this.#shadow.getElementById(shiftId);
|
|
1056
|
-
if (layoutShiftEl) {
|
|
1057
|
-
layoutShiftEls.push(layoutShiftEl);
|
|
1058
|
-
}
|
|
1183
|
+
if (cruxManager.getConfigSetting().get().enabled) {
|
|
1184
|
+
void this.#refreshFieldDataForCurrentPage();
|
|
1059
1185
|
}
|
|
1060
1186
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1187
|
+
this.#lcpValue = liveMetrics.lcpValue;
|
|
1188
|
+
this.#clsValue = liveMetrics.clsValue;
|
|
1189
|
+
this.#inpValue = liveMetrics.inpValue;
|
|
1190
|
+
this.#interactions = liveMetrics.interactions;
|
|
1191
|
+
this.#layoutShifts = liveMetrics.layoutShifts;
|
|
1192
|
+
this.requestUpdate();
|
|
1193
|
+
}
|
|
1064
1194
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
}
|
|
1195
|
+
override willHide(): void {
|
|
1196
|
+
super.willHide();
|
|
1197
|
+
LiveMetrics.LiveMetrics.instance().removeEventListener(LiveMetrics.Events.STATUS, this.#onMetricStatus, this);
|
|
1069
1198
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
for (const layoutShiftEl of layoutShiftEls) {
|
|
1076
|
-
UI.UIUtils.runCSSAnimationOnce(layoutShiftEl, 'highlight');
|
|
1077
|
-
}
|
|
1078
|
-
});
|
|
1199
|
+
const cruxManager = CrUXManager.CrUXManager.instance();
|
|
1200
|
+
cruxManager.removeEventListener(CrUXManager.Events.FIELD_DATA_CHANGED, this.#onFieldDataChanged, this);
|
|
1201
|
+
|
|
1202
|
+
this.#deviceModeModel?.removeEventListener(
|
|
1203
|
+
EmulationModel.DeviceModeModel.Events.UPDATED, this.#onEmulationChanged, this);
|
|
1079
1204
|
}
|
|
1080
1205
|
|
|
1081
|
-
#
|
|
1082
|
-
if (
|
|
1083
|
-
|
|
1206
|
+
#onPageScopeMenuItemSelected(event: Menus.SelectMenu.SelectMenuItemSelectedEvent): void {
|
|
1207
|
+
if (event.itemValue === 'url') {
|
|
1208
|
+
this.#cruxManager.fieldPageScope = 'url';
|
|
1209
|
+
} else {
|
|
1210
|
+
this.#cruxManager.fieldPageScope = 'origin';
|
|
1084
1211
|
}
|
|
1212
|
+
this.requestUpdate();
|
|
1213
|
+
}
|
|
1085
1214
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
slot="layout-shifts-log-content"
|
|
1090
|
-
${Lit.Directives.ref(el => {
|
|
1091
|
-
if (el instanceof HTMLElement) {
|
|
1092
|
-
this.#layoutShiftsListEl = el as HTMLElement;
|
|
1093
|
-
}
|
|
1094
|
-
})}
|
|
1095
|
-
>
|
|
1096
|
-
${this.#layoutShifts.map(layoutShift => {
|
|
1097
|
-
const metricValue = renderMetricValue(
|
|
1098
|
-
'timeline.landing.layout-shift-event-score',
|
|
1099
|
-
layoutShift.score,
|
|
1100
|
-
CLS_THRESHOLDS,
|
|
1101
|
-
// CLS value is 2 decimal places, but individual shift scores tend to be much smaller
|
|
1102
|
-
// so we expand the precision here.
|
|
1103
|
-
v => v.toFixed(4),
|
|
1104
|
-
{dim: true},
|
|
1105
|
-
);
|
|
1106
|
-
|
|
1107
|
-
return html`
|
|
1108
|
-
<li id=${layoutShift.uniqueLayoutShiftId} class="log-item layout-shift" tabindex="-1">
|
|
1109
|
-
<div class="layout-shift-score">Layout shift score: ${metricValue}</div>
|
|
1110
|
-
<div class="layout-shift-nodes">
|
|
1111
|
-
${layoutShift.affectedNodeRefs.map(node => html`
|
|
1112
|
-
<div class="layout-shift-node">
|
|
1113
|
-
${widget(PanelsCommon.DOMLinkifier.DOMNodeLink, {node})}
|
|
1114
|
-
</div>
|
|
1115
|
-
`)}
|
|
1116
|
-
</div>
|
|
1117
|
-
</li>
|
|
1118
|
-
`;
|
|
1119
|
-
})}
|
|
1120
|
-
</ol>
|
|
1121
|
-
`;
|
|
1122
|
-
// clang-format on
|
|
1215
|
+
#onDeviceOptionMenuItemSelected(event: Menus.SelectMenu.SelectMenuItemSelectedEvent): void {
|
|
1216
|
+
this.#cruxManager.fieldDeviceOption = event.itemValue as DeviceOption;
|
|
1217
|
+
this.requestUpdate();
|
|
1123
1218
|
}
|
|
1124
1219
|
|
|
1125
|
-
#
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
<main>
|
|
1131
|
-
<h2 class="section-title">${i18nString(UIStrings.nodePerformanceTimeline)}</h2>
|
|
1132
|
-
<div class="node-description">${i18nString(UIStrings.nodeClickToRecord)}</div>
|
|
1133
|
-
<div class="record-action-card">${this.#renderRecordAction(this.#toggleRecordAction)}</div>
|
|
1134
|
-
</main>
|
|
1135
|
-
</div>
|
|
1136
|
-
`;
|
|
1220
|
+
async #revealInteraction(interaction: LiveMetrics.Interaction): Promise<void> {
|
|
1221
|
+
this.#highlightedInteractionId = interaction.interactionId;
|
|
1222
|
+
this.requestUpdate();
|
|
1223
|
+
await this.updateComplete;
|
|
1224
|
+
this.#highlightedInteractionId = '';
|
|
1137
1225
|
}
|
|
1138
1226
|
|
|
1139
|
-
#
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1227
|
+
async #logExtraInteractionDetails(interaction: LiveMetrics.Interaction): Promise<void> {
|
|
1228
|
+
const success = await LiveMetrics.LiveMetrics.instance().logInteractionScripts(interaction);
|
|
1229
|
+
if (success) {
|
|
1230
|
+
await Common.Console.Console.instance().showPromise();
|
|
1143
1231
|
}
|
|
1232
|
+
}
|
|
1144
1233
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
// clang-format off
|
|
1153
|
-
const output = html`
|
|
1154
|
-
<style>${liveMetricsViewStyles}</style>
|
|
1155
|
-
<style>${metricValueStyles}</style>
|
|
1156
|
-
<div class="container">
|
|
1157
|
-
<div class="live-metrics-view">
|
|
1158
|
-
<main class="live-metrics">
|
|
1159
|
-
<h2 class="section-title">${liveMetricsTitle}</h2>
|
|
1160
|
-
<div class="metric-cards"
|
|
1161
|
-
${Lit.Directives.ref(el => {
|
|
1162
|
-
if (el instanceof HTMLElement) {
|
|
1163
|
-
this.#tooltipContainerEl = el;
|
|
1164
|
-
}
|
|
1165
|
-
})}
|
|
1166
|
-
>
|
|
1167
|
-
<div id="lcp">
|
|
1168
|
-
${this.#renderLcpCard()}
|
|
1169
|
-
</div>
|
|
1170
|
-
<div id="cls">
|
|
1171
|
-
${this.#renderClsCard()}
|
|
1172
|
-
</div>
|
|
1173
|
-
<div id="inp">
|
|
1174
|
-
${this.#renderInpCard()}
|
|
1175
|
-
</div>
|
|
1176
|
-
</div>
|
|
1177
|
-
<devtools-link
|
|
1178
|
-
href=${helpLink}
|
|
1179
|
-
class="local-field-link"
|
|
1180
|
-
title=${i18nString(UIStrings.localFieldLearnMoreTooltip)}
|
|
1181
|
-
>${i18nString(UIStrings.localFieldLearnMoreLink)}</devtools-link>
|
|
1182
|
-
${this.#renderLogSection()}
|
|
1183
|
-
</main>
|
|
1184
|
-
<aside class="next-steps" aria-labelledby="next-steps-section-title">
|
|
1185
|
-
<h2 id="next-steps-section-title" class="section-title">${i18nString(UIStrings.nextSteps)}</h2>
|
|
1186
|
-
<div id="field-setup" class="settings-card">
|
|
1187
|
-
<h3 class="card-title">${i18nString(UIStrings.fieldMetricsTitle)}</h3>
|
|
1188
|
-
${this.#renderFieldDataMessage()}
|
|
1189
|
-
${this.#renderPageScopeSetting()}
|
|
1190
|
-
${this.#renderDeviceScopeSetting()}
|
|
1191
|
-
<div class="field-setup-buttons">
|
|
1192
|
-
<devtools-field-settings-dialog></devtools-field-settings-dialog>
|
|
1193
|
-
</div>
|
|
1194
|
-
</div>
|
|
1195
|
-
<div id="recording-settings" class="settings-card">
|
|
1196
|
-
${this.#renderRecordingSettings()}
|
|
1197
|
-
</div>
|
|
1198
|
-
<div id="record" class="record-action-card">
|
|
1199
|
-
${this.#renderRecordAction(this.#toggleRecordAction)}
|
|
1200
|
-
</div>
|
|
1201
|
-
<div id="record-page-load" class="record-action-card">
|
|
1202
|
-
${this.#renderRecordAction(this.#recordReloadAction)}
|
|
1203
|
-
</div>
|
|
1204
|
-
</aside>
|
|
1205
|
-
</div>
|
|
1206
|
-
</div>
|
|
1207
|
-
`;
|
|
1208
|
-
Lit.render(output, this.#shadow, {host: this});
|
|
1209
|
-
};
|
|
1210
|
-
// clang-format on
|
|
1211
|
-
}
|
|
1234
|
+
async #revealLayoutShiftCluster(clusterIds: Set<LiveMetrics.LayoutShift['uniqueLayoutShiftId']>): Promise<void> {
|
|
1235
|
+
this.#highlightedLayoutShiftClusterIds = clusterIds;
|
|
1236
|
+
this.requestUpdate();
|
|
1237
|
+
await this.updateComplete;
|
|
1238
|
+
this.#highlightedLayoutShiftClusterIds = new Set();
|
|
1239
|
+
}
|
|
1212
1240
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1241
|
+
override performUpdate(): void {
|
|
1242
|
+
const viewInput: ViewInput = {
|
|
1243
|
+
isNode: this.isNode,
|
|
1244
|
+
lcpValue: this.#lcpValue,
|
|
1245
|
+
clsValue: this.#clsValue,
|
|
1246
|
+
inpValue: this.#inpValue,
|
|
1247
|
+
interactions: this.#interactions,
|
|
1248
|
+
layoutShifts: this.#layoutShifts,
|
|
1249
|
+
toggleRecordAction: this.#toggleRecordAction,
|
|
1250
|
+
recordReloadAction: this.#recordReloadAction,
|
|
1251
|
+
cruxManager: this.#cruxManager,
|
|
1252
|
+
handlePageScopeSelected: this.#onPageScopeMenuItemSelected.bind(this),
|
|
1253
|
+
handleDeviceOptionSelected: this.#onDeviceOptionMenuItemSelected.bind(this),
|
|
1254
|
+
revealLayoutShiftCluster: this.#revealLayoutShiftCluster.bind(this),
|
|
1255
|
+
revealInteraction: this.#revealInteraction.bind(this),
|
|
1256
|
+
logExtraInteractionDetails: this.#logExtraInteractionDetails.bind(this),
|
|
1257
|
+
highlightedInteractionId: this.#highlightedInteractionId,
|
|
1258
|
+
highlightedLayoutShiftClusterIds: this.#highlightedLayoutShiftClusterIds,
|
|
1259
|
+
};
|
|
1215
1260
|
|
|
1216
|
-
|
|
1217
|
-
super();
|
|
1218
|
-
this.style.display = 'contents';
|
|
1261
|
+
this.#view(viewInput, this.#viewOutput, this.contentElement);
|
|
1219
1262
|
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
class LiveMetricsLogs extends UI.Widget.Widget {
|
|
1266
|
+
#tabbedPane: UI.TabbedPane.TabbedPane;
|
|
1220
1267
|
|
|
1221
1268
|
/**
|
|
1222
1269
|
* Returns `true` if selecting the tab was successful.
|
|
1223
1270
|
*/
|
|
1224
1271
|
selectTab(tabId: string): boolean {
|
|
1225
|
-
if (!this.#tabbedPane) {
|
|
1226
|
-
return false;
|
|
1227
|
-
}
|
|
1228
1272
|
return this.#tabbedPane.selectTab(tabId);
|
|
1229
1273
|
}
|
|
1230
1274
|
|
|
1231
1275
|
#clearCurrentLog(): void {
|
|
1232
1276
|
const liveMetrics = LiveMetrics.LiveMetrics.instance();
|
|
1233
1277
|
|
|
1234
|
-
switch (this.#tabbedPane
|
|
1278
|
+
switch (this.#tabbedPane.selectedTabId) {
|
|
1235
1279
|
case 'interactions':
|
|
1236
1280
|
liveMetrics.clearInteractions();
|
|
1237
1281
|
break;
|
|
@@ -1240,16 +1284,12 @@ class LiveMetricsLogs extends UI.Widget.WidgetElement<UI.Widget.Widget> {
|
|
|
1240
1284
|
break;
|
|
1241
1285
|
}
|
|
1242
1286
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
// of web component slots. Passing `this` into the container widget will make `this` the root element
|
|
1247
|
-
// of that widget.
|
|
1248
|
-
//
|
|
1287
|
+
constructor(element: HTMLElement) {
|
|
1288
|
+
super(element, {useShadowDom: true});
|
|
1289
|
+
this.element.style.display = 'contents';
|
|
1249
1290
|
// Any children of the root element `this` will be matched to the slots defined within the container
|
|
1250
1291
|
// widget's shadow DOM.
|
|
1251
|
-
|
|
1252
|
-
containerWidget.contentElement.style.display = 'contents';
|
|
1292
|
+
this.contentElement.style.display = 'contents';
|
|
1253
1293
|
|
|
1254
1294
|
this.#tabbedPane = new UI.TabbedPane.TabbedPane();
|
|
1255
1295
|
|
|
@@ -1273,18 +1313,6 @@ class LiveMetricsLogs extends UI.Widget.WidgetElement<UI.Widget.Widget> {
|
|
|
1273
1313
|
i18nString(UIStrings.clearCurrentLog), 'clear', undefined, 'timeline.landing.clear-log');
|
|
1274
1314
|
clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, this.#clearCurrentLog, this);
|
|
1275
1315
|
this.#tabbedPane.rightToolbar().appendToolbarItem(clearButton);
|
|
1276
|
-
this.#tabbedPane.show(
|
|
1277
|
-
|
|
1278
|
-
return containerWidget;
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
customElements.define('devtools-live-metrics-view', LiveMetricsView);
|
|
1283
|
-
customElements.define('devtools-live-metrics-logs', LiveMetricsLogs);
|
|
1284
|
-
|
|
1285
|
-
declare global {
|
|
1286
|
-
interface HTMLElementTagNameMap {
|
|
1287
|
-
'devtools-live-metrics-view': LiveMetricsView;
|
|
1288
|
-
'devtools-live-metrics-logs': LiveMetricsLogs;
|
|
1316
|
+
this.#tabbedPane.show(this.contentElement);
|
|
1289
1317
|
}
|
|
1290
1318
|
}
|