chrome-devtools-frontend 1.0.1651641 → 1.0.1654411

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.
Files changed (96) hide show
  1. package/.agents/skills/fixing-skipped-tests/SKILL.md +32 -0
  2. package/.agents/skills/migrate-chromium-test/SKILL.md +144 -0
  3. package/docs/get_the_code.md +1 -2
  4. package/docs/ui_engineering.md +3 -1
  5. package/eslint.config.mjs +0 -5
  6. package/front_end/core/sdk/CPUThrottlingManager.ts +13 -8
  7. package/front_end/core/sdk/CSSMetadata.ts +23 -0
  8. package/front_end/core/sdk/CompilerSourceMappingContentProvider.ts +8 -4
  9. package/front_end/core/sdk/DOMModel.ts +4 -0
  10. package/front_end/core/sdk/EmulationModel.ts +4 -0
  11. package/front_end/core/sdk/ResourceTreeModel.ts +3 -2
  12. package/front_end/entrypoints/heap_snapshot_worker/HeapSnapshot.ts +459 -74
  13. package/front_end/entrypoints/main/MainImpl.ts +0 -6
  14. package/front_end/foundation/Universe.ts +18 -0
  15. package/front_end/generated/InspectorBackendCommands.ts +7 -6
  16. package/front_end/generated/SupportedCSSProperties.js +208 -34
  17. package/front_end/generated/protocol.ts +64 -7
  18. package/front_end/models/ai_assistance/AiAgent2.ts +3 -3
  19. package/front_end/models/ai_assistance/AiConversation.ts +2 -1
  20. package/front_end/models/ai_assistance/README.md +5 -4
  21. package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +1 -1
  22. package/front_end/models/ai_assistance/agents/PerformanceAgent.snapshot.txt +2 -2
  23. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +11 -164
  24. package/front_end/models/ai_assistance/ai_assistance.ts +6 -2
  25. package/front_end/models/ai_assistance/contexts/PerformanceTraceContext.ts +295 -0
  26. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +10 -10
  27. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +12 -11
  28. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +6 -6
  29. package/front_end/models/ai_assistance/skills/accessibility.md +4 -2
  30. package/front_end/models/ai_assistance/tools/GetElementAccessibilityDetails.ts +142 -0
  31. package/front_end/models/ai_assistance/tools/GetStyles.ts +1 -1
  32. package/front_end/models/ai_assistance/tools/README.md +22 -2
  33. package/front_end/models/ai_assistance/tools/{ResolveLighthousePath.ts → ResolveDevtoolsNodePath.ts} +18 -17
  34. package/front_end/models/ai_assistance/tools/Tool.ts +7 -2
  35. package/front_end/models/ai_assistance/tools/ToolRegistry.ts +10 -8
  36. package/front_end/models/autofill_manager/AutofillManager.ts +7 -15
  37. package/front_end/models/bindings/NetworkProject.ts +4 -8
  38. package/front_end/models/breakpoints/BreakpointManager.ts +9 -9
  39. package/front_end/models/emulation/DeviceModeModel.ts +24 -0
  40. package/front_end/models/emulation/EmulatedDevices.ts +108 -0
  41. package/front_end/models/heap_snapshot/HeapSnapshotModel.ts +36 -0
  42. package/front_end/models/heap_snapshot/HeapSnapshotProxy.ts +8 -0
  43. package/front_end/models/issues_manager/IssuesManager.ts +10 -6
  44. package/front_end/models/javascript_metadata/JavaScriptMetadata.ts +4 -5
  45. package/front_end/models/live-metrics/LiveMetrics.ts +6 -6
  46. package/front_end/models/live-metrics/web-vitals-injected/spec/spec.ts +5 -5
  47. package/front_end/models/live-metrics/web-vitals-injected/web-vitals-injected.ts +3 -3
  48. package/front_end/models/logs/LogManager.ts +25 -10
  49. package/front_end/models/logs/NetworkLog.ts +24 -19
  50. package/front_end/models/persistence/AutomaticFileSystemWorkspaceBinding.ts +14 -5
  51. package/front_end/models/persistence/IsolatedFileSystem.ts +9 -11
  52. package/front_end/models/persistence/IsolatedFileSystemManager.ts +5 -3
  53. package/front_end/models/persistence/NetworkPersistenceManager.ts +73 -44
  54. package/front_end/models/persistence/PersistenceImpl.ts +4 -5
  55. package/front_end/models/trace/EntityMapper.ts +1 -1
  56. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +9 -9
  57. package/front_end/panels/ai_assistance/ExportConversation.ts +4 -1
  58. package/front_end/panels/ai_assistance/components/ChatInput.ts +2 -2
  59. package/front_end/panels/application/IndexedDBViews.ts +38 -36
  60. package/front_end/panels/application/components/AdsView.ts +4 -0
  61. package/front_end/panels/autofill/AutofillView.ts +1 -1
  62. package/front_end/panels/autofill/autofill-meta.ts +3 -2
  63. package/front_end/panels/elements/AccessibilityTreeUtils.ts +26 -15
  64. package/front_end/panels/elements/AccessibilityTreeView.ts +12 -9
  65. package/front_end/panels/elements/ElementsTreeOutline.ts +5 -0
  66. package/front_end/panels/layer_viewer/LayerTreeOutline.ts +118 -68
  67. package/front_end/panels/layer_viewer/Layers3DView.ts +1 -1
  68. package/front_end/panels/layer_viewer/layerTreeOutline.css +20 -13
  69. package/front_end/panels/layers/LayersPanel.ts +1 -1
  70. package/front_end/panels/mobile_throttling/ThrottlingSettingsTab.ts +64 -17
  71. package/front_end/panels/mobile_throttling/throttlingSettingsTab.css +5 -0
  72. package/front_end/panels/network/LinkPreloadGenerator.ts +101 -0
  73. package/front_end/panels/network/NetworkLogView.ts +28 -0
  74. package/front_end/panels/network/network.ts +2 -0
  75. package/front_end/panels/profiler/HeapSnapshotView.ts +121 -33
  76. package/front_end/panels/profiler/ModuleUIStrings.ts +4 -0
  77. package/front_end/panels/sources/ScopeChainSidebarPane.ts +0 -12
  78. package/front_end/panels/timeline/TimelineHistoryManager.ts +38 -37
  79. package/front_end/panels/timeline/TimelinePanel.ts +32 -8
  80. package/front_end/panels/timeline/components/CPUThrottlingSelector.ts +7 -6
  81. package/front_end/panels/timeline/components/LiveMetricsView.ts +38 -38
  82. package/front_end/panels/timeline/components/MetricCard.ts +18 -18
  83. package/front_end/panels/timeline/components/liveMetricsView.css +6 -6
  84. package/front_end/panels/timeline/components/metricCard.css +4 -4
  85. package/front_end/panels/whats_new/ReleaseNoteText.ts +3 -3
  86. package/front_end/panels/whats_new/resources/WNDT.md +1 -1
  87. package/front_end/third_party/chromium/README.chromium +1 -1
  88. package/front_end/ui/legacy/InspectorDrawerView.ts +7 -5
  89. package/front_end/ui/legacy/ListWidget.ts +12 -0
  90. package/front_end/ui/legacy/SplitWidget.ts +12 -11
  91. package/front_end/ui/legacy/components/data_grid/DataGridElement.ts +23 -19
  92. package/front_end/ui/legacy/components/object_ui/CustomPreviewComponent.ts +8 -1
  93. package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +110 -17
  94. package/front_end/ui/legacy/treeoutline.css +1 -1
  95. package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
  96. package/package.json +4 -3
@@ -2,12 +2,11 @@
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
4
 
5
- /* eslint-disable @devtools/no-imperative-dom-api */
6
-
7
5
  import * as Common from '../../core/common/common.js';
8
6
  import * as i18n from '../../core/i18n/i18n.js';
9
7
  import type * as SDK from '../../core/sdk/sdk.js';
10
8
  import * as UI from '../../ui/legacy/legacy.js';
9
+ import {html, render} from '../../ui/lit/lit.js';
11
10
 
12
11
  import layerTreeOutlineStyles from './layerTreeOutline.css.js';
13
12
  import {
@@ -41,21 +40,49 @@ const UIStrings = {
41
40
  } as const;
42
41
  const str_ = i18n.i18n.registerUIStrings('panels/layer_viewer/LayerTreeOutline.ts', UIStrings);
43
42
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
44
- export class LayerTreeOutline extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.TreeOutline.TreeOutline>(
45
- UI.TreeOutline.TreeOutline) implements Common.EventTarget.EventTarget<EventTypes>, LayerView {
43
+
44
+ export interface ViewInput {
45
+ treeOutlineElement: HTMLElement;
46
+ layerCount: number;
47
+ totalLayerMemory: number;
48
+ }
49
+ export type View = (input: ViewInput, output: object, target: HTMLElement) => void;
50
+
51
+ const DEFAULT_VIEW: View = (input, _output, target) => {
52
+ render(html`
53
+ <style>${layerTreeOutlineStyles}</style>
54
+ <div class="vbox layer-tree-wrapper">
55
+ <div style="flex-grow: 1; overflow: auto; display: flex;">
56
+ ${input.treeOutlineElement}
57
+ </div>
58
+ <div class="hbox layer-summary">
59
+ <span class="layer-count">${i18nString(UIStrings.layerCount, {
60
+ PH1: input.layerCount
61
+ })}</span>
62
+ <span>${i18n.ByteUtilities.bytesToString(input.totalLayerMemory)}</span>
63
+ </div>
64
+ </div>
65
+ `,
66
+ target);
67
+ };
68
+
69
+ export class LayerTreeOutline extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.Widget>(
70
+ UI.Widget.Widget) implements Common.EventTarget.EventTarget<EventTypes>, LayerView {
46
71
  private layerViewHost: LayerViewHost;
47
72
  private treeOutline: UI.TreeOutline.TreeOutlineInShadow;
48
73
  private lastHoveredNode: LayerTreeElement|null;
49
- private layerCountElement: HTMLSpanElement;
50
- private layerMemoryElement: HTMLSpanElement;
51
- override element: HTMLElement;
52
74
  private layerTree?: SDK.LayerTreeBase.LayerTreeBase|null;
53
75
  private layerSnapshotMap?: Map<SDK.LayerTreeBase.Layer, SnapshotSelection>;
54
76
 
55
- constructor(layerViewHost: LayerViewHost) {
77
+ #view: View;
78
+ #layerCount = 0;
79
+ #totalLayerMemory = 0;
80
+
81
+ constructor(layerViewHost: LayerViewHost, view: View = DEFAULT_VIEW) {
56
82
  super();
57
83
  this.layerViewHost = layerViewHost;
58
84
  this.layerViewHost.registerView(this);
85
+ this.#view = view;
59
86
 
60
87
  this.treeOutline = new UI.TreeOutline.TreeOutlineInShadow();
61
88
  this.treeOutline.element.classList.add('layer-tree', 'overflow-auto');
@@ -66,24 +93,23 @@ export class LayerTreeOutline extends Common.ObjectWrapper.eventMixin<EventTypes
66
93
 
67
94
  this.lastHoveredNode = null;
68
95
 
69
- const summaryElement = document.createElement('div');
70
- summaryElement.classList.add('hbox', 'layer-summary');
71
- this.layerCountElement = document.createElement('span');
72
- this.layerCountElement.classList.add('layer-count');
73
- this.layerMemoryElement = document.createElement('span');
74
- summaryElement.appendChild(this.layerCountElement);
75
- summaryElement.appendChild(this.layerMemoryElement);
76
-
77
- const wrapperElement = document.createElement('div');
78
- wrapperElement.classList.add('vbox', 'layer-tree-wrapper');
79
- wrapperElement.appendChild(this.treeOutline.element);
80
- wrapperElement.appendChild(summaryElement);
81
- this.element = wrapperElement;
82
- UI.DOMUtilities.appendStyle(this.element, layerTreeOutlineStyles);
83
-
84
96
  this.layerViewHost.showInternalLayersSetting().addChangeListener(this.update, this);
85
97
  }
86
98
 
99
+ override wasShown(): void {
100
+ super.wasShown();
101
+ this.requestUpdate();
102
+ }
103
+
104
+ override performUpdate(): void {
105
+ this.#view({
106
+ treeOutlineElement: this.treeOutline.element,
107
+ layerCount: this.#layerCount,
108
+ totalLayerMemory: this.#totalLayerMemory,
109
+ },
110
+ {}, this.contentElement);
111
+ }
112
+
87
113
  override focus(): void {
88
114
  this.treeOutline.focus();
89
115
  }
@@ -121,7 +147,7 @@ export class LayerTreeOutline extends Common.ObjectWrapper.eventMixin<EventTypes
121
147
 
122
148
  private update(): void {
123
149
  const showInternalLayers = this.layerViewHost.showInternalLayersSetting().get();
124
- const seenLayers = new Map<SDK.LayerTreeBase.Layer, boolean>();
150
+ const seenLayers = new Set<SDK.LayerTreeBase.Layer>();
125
151
  let root: (SDK.LayerTreeBase.Layer|null)|null = null;
126
152
  if (this.layerTree) {
127
153
  if (!showInternalLayers) {
@@ -135,58 +161,81 @@ export class LayerTreeOutline extends Common.ObjectWrapper.eventMixin<EventTypes
135
161
  let layerCount = 0;
136
162
  let totalLayerMemory = 0;
137
163
 
138
- function updateLayer(this: LayerTreeOutline, layer: SDK.LayerTreeBase.Layer): void {
139
- if (!layer.drawsContent() && !showInternalLayers) {
140
- return;
141
- }
142
- if (seenLayers.get(layer)) {
143
- console.assert(false, 'Duplicate layer: ' + layer.id());
144
- }
145
- seenLayers.set(layer, true);
164
+ const childrenMap = new Map<SDK.LayerTreeBase.Layer, SDK.LayerTreeBase.Layer[]>();
146
165
 
147
- layerCount++;
148
- totalLayerMemory += layer.gpuMemoryUsage();
166
+ if (this.layerTree && root) {
167
+ const buildTree = (layer: SDK.LayerTreeBase.Layer): void => {
168
+ if (!layer.drawsContent() && !showInternalLayers) {
169
+ return;
170
+ }
149
171
 
150
- let node: LayerTreeElement|null = layerToTreeElement.get(layer) || null;
151
- let parentLayer = layer.parent();
152
- // Skip till nearest visible ancestor.
153
- while (parentLayer && parentLayer !== root && !parentLayer.drawsContent() && !showInternalLayers) {
154
- parentLayer = parentLayer.parent();
155
- }
156
- const parent =
157
- layer === root ? this.treeOutline.rootElement() : parentLayer && layerToTreeElement.get(parentLayer);
158
- if (!parent) {
159
- console.assert(false, 'Parent is not in the tree');
160
- return;
161
- }
162
- if (!node) {
163
- node = new LayerTreeElement(this, layer);
164
- parent.appendChild(node);
165
- // Expand all new non-content layers to expose content layers better.
166
- if (!layer.drawsContent()) {
167
- node.expand();
172
+ layerCount++;
173
+ totalLayerMemory += layer.gpuMemoryUsage();
174
+
175
+ if (layer === root) {
176
+ return;
168
177
  }
169
- } else {
170
- if (node.parent !== parent) {
171
- const oldSelection = this.treeOutline.selectedTreeElement;
172
- if (node.parent) {
173
- node.parent.removeChild(node);
174
- }
175
- parent.appendChild(node);
176
- if (oldSelection && oldSelection !== this.treeOutline.selectedTreeElement) {
177
- oldSelection.select();
178
+
179
+ let parentLayer = layer.parent();
180
+ // Skip till nearest visible ancestor.
181
+ while (parentLayer && parentLayer !== root && !parentLayer.drawsContent() && !showInternalLayers) {
182
+ parentLayer = parentLayer.parent();
183
+ }
184
+
185
+ if (parentLayer) {
186
+ let children = childrenMap.get(parentLayer);
187
+ if (!children) {
188
+ children = [];
189
+ childrenMap.set(parentLayer, children);
178
190
  }
191
+ children.push(layer);
192
+ } else {
193
+ console.assert(false, 'Internal error: multiple root layers');
179
194
  }
180
- node.update();
181
- }
195
+ };
196
+
197
+ this.layerTree.forEachLayer(buildTree, root);
182
198
  }
183
- if (root && this.layerTree) {
184
- this.layerTree.forEachLayer(updateLayer.bind(this), root);
199
+
200
+ const syncNode =
201
+ (layer: SDK.LayerTreeBase.Layer, parent: UI.TreeOutline.TreeOutline|UI.TreeOutline.TreeElement): void => {
202
+ seenLayers.add(layer);
203
+ let node: LayerTreeElement|null = layerToTreeElement.get(layer) || null;
204
+ if (!node) {
205
+ node = new LayerTreeElement(this, layer);
206
+ parent.appendChild(node);
207
+ // Expand all new non-content layers to expose content layers better.
208
+ if (!layer.drawsContent()) {
209
+ node.expand();
210
+ }
211
+ } else {
212
+ if (node.parent !== parent) {
213
+ const oldSelection = this.treeOutline.selectedTreeElement;
214
+ if (node.parent) {
215
+ node.parent.removeChild(node);
216
+ }
217
+ parent.appendChild(node);
218
+ if (oldSelection && oldSelection !== this.treeOutline.selectedTreeElement) {
219
+ oldSelection.select();
220
+ }
221
+ }
222
+ node.update();
223
+ }
224
+
225
+ const children = childrenMap.get(layer) || [];
226
+ for (const child of children) {
227
+ syncNode(child, node);
228
+ }
229
+ };
230
+
231
+ if (root && (root.drawsContent() || showInternalLayers)) {
232
+ syncNode(root, this.treeOutline.rootElement());
185
233
  }
234
+
186
235
  // Clean up layers that don't exist anymore from tree.
187
236
  const rootElement = this.treeOutline.rootElement();
188
237
  for (let node = rootElement.firstChild(); node instanceof LayerTreeElement && !node.root;) {
189
- if (seenLayers.get(node.layer)) {
238
+ if (seenLayers.has(node.layer)) {
190
239
  node = node.traverseNextTreeElement(false);
191
240
  } else {
192
241
  const nextNode = node.nextSibling || node.parent;
@@ -209,8 +258,9 @@ export class LayerTreeOutline extends Common.ObjectWrapper.eventMixin<EventTypes
209
258
  }
210
259
  }
211
260
 
212
- this.layerCountElement.textContent = i18nString(UIStrings.layerCount, {PH1: layerCount});
213
- this.layerMemoryElement.textContent = i18n.ByteUtilities.bytesToString(totalLayerMemory);
261
+ this.#layerCount = layerCount;
262
+ this.#totalLayerMemory = totalLayerMemory;
263
+ this.requestUpdate();
214
264
  }
215
265
 
216
266
  private onMouseMove(event: MouseEvent): void {
@@ -120,7 +120,7 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
120
120
  uiI18n.getFormatLocalizedString(
121
121
  str_, UIStrings.checkSForPossibleReasons,
122
122
  {
123
- PH1: Link.create('about:gpu', undefined, undefined, 'about-gpu')
123
+ PH1: Link.create('chrome://gpu', undefined, undefined, 'about-gpu', 0, true)
124
124
  }
125
125
  )
126
126
  ],
@@ -4,19 +4,26 @@
4
4
  * found in the LICENSE file.
5
5
  */
6
6
 
7
- .layer-summary {
8
- border-top: 1px solid var(--sys-color-divider);
9
- justify-content: space-between;
10
- padding: 4px 10px;
11
- flex-shrink: 0;
12
- }
7
+ @scope to (devtools-widget > *) {
8
+ .layer-summary {
9
+ border-top: 1px solid var(--sys-color-divider);
10
+ justify-content: space-between;
11
+ padding: 4px 10px;
12
+ flex-shrink: 0;
13
+ }
13
14
 
14
- .layer-count {
15
- white-space: nowrap;
16
- overflow: hidden;
17
- text-overflow: ellipsis;
18
- }
15
+ .layer-count {
16
+ white-space: nowrap;
17
+ overflow: hidden;
18
+ text-overflow: ellipsis;
19
+ }
20
+
21
+ .layer-tree-wrapper {
22
+ height: 100%;
23
+ flex-grow: 1;
24
+ }
19
25
 
20
- .layer-tree-wrapper {
21
- flex-grow: 1;
26
+ .layer-tree {
27
+ flex-grow: 1;
28
+ }
22
29
  }
@@ -47,7 +47,7 @@ export class LayersPanel extends UI.Panel.PanelWithSidebar implements SDK.Target
47
47
  this.layerTreeOutline = new LayerViewer.LayerTreeOutline.LayerTreeOutline(this.layerViewHost);
48
48
  this.layerTreeOutline.addEventListener(
49
49
  LayerViewer.LayerTreeOutline.Events.PAINT_PROFILER_REQUESTED, this.onPaintProfileRequested, this);
50
- this.panelSidebarElement().appendChild(this.layerTreeOutline.element);
50
+ this.layerTreeOutline.show(this.panelSidebarElement());
51
51
  this.setDefaultFocusedElement(this.layerTreeOutline.element);
52
52
 
53
53
  this.rightSplitWidget = new UI.SplitWidget.SplitWidget(false, true, 'layer-details-split-view-state');
@@ -14,13 +14,18 @@ import * as UI from '../../ui/legacy/legacy.js';
14
14
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
15
15
 
16
16
  import {CalibrationController} from './CalibrationController.js';
17
+ import {ThrottlingPresets} from './ThrottlingPresets.js';
17
18
  import throttlingSettingsTabStyles from './throttlingSettingsTab.css.js';
18
19
 
19
20
  const UIStrings = {
20
21
  /**
21
- * @description Text in Throttling Settings Tab of the Network panel
22
+ * @description Title for default network throttling profiles card
23
+ */
24
+ defaultProfiles: 'Default profiles',
25
+ /**
26
+ * @description Title for custom network throttling profiles card
22
27
  */
23
- networkThrottlingProfiles: 'Network throttling profiles',
28
+ customProfiles: 'Custom profiles',
24
29
  /**
25
30
  * @description Text of add conditions button in Throttling Settings Tab of the Network panel
26
31
  */
@@ -452,7 +457,10 @@ function extractCustomSettingIndex(key: SDK.NetworkManager.UserDefinedThrottling
452
457
 
453
458
  export class ThrottlingSettingsTab extends UI.Widget.VBox implements
454
459
  UI.ListWidget.Delegate<SDK.NetworkManager.Conditions> {
455
- private readonly list: UI.ListWidget.ListWidget<SDK.NetworkManager.Conditions>;
460
+ /** List of default network throttling presets (read-only in UI) */
461
+ private readonly presetsList: UI.ListWidget.ListWidget<SDK.NetworkManager.Conditions>;
462
+ /** List of custom user-defined network throttling profiles */
463
+ private readonly customList: UI.ListWidget.ListWidget<SDK.NetworkManager.Conditions>;
456
464
  private readonly customUserConditions: Common.Settings.Setting<SDK.NetworkManager.Conditions[]>;
457
465
  private editor?: UI.ListWidget.Editor<SDK.NetworkManager.Conditions>;
458
466
  private cpuThrottlingCard: CPUThrottlingCard;
@@ -489,15 +497,18 @@ export class ThrottlingSettingsTab extends UI.Widget.VBox implements
489
497
  addButton.textContent = i18nString(UIStrings.addCustomProfile);
490
498
  addButton.addEventListener('click', () => this.addButtonClicked());
491
499
 
492
- const card = settingsContent.createChild('devtools-card');
493
- card.heading = i18nString(UIStrings.networkThrottlingProfiles);
494
- const container = card.createChild('div');
500
+ this.presetsList = new UI.ListWidget.ListWidget(this);
501
+ this.presetsList.setHeader(createHeaderRow());
502
+ createProfilesCard(i18nString(UIStrings.defaultProfiles), this.presetsList, settingsContent);
503
+ const presets = ThrottlingPresets.networkPresets;
504
+ for (let i = 0; i < presets.length; ++i) {
505
+ this.presetsList.appendItem(presets[i], false);
506
+ }
495
507
 
496
- this.list = new UI.ListWidget.ListWidget(this);
497
- this.list.element.classList.add('conditions-list');
498
- this.list.registerRequiredCSS(throttlingSettingsTabStyles);
499
- this.list.show(container);
500
- container.appendChild(addButton);
508
+ this.customList = new UI.ListWidget.ListWidget(this);
509
+ this.customList.setHeader(createHeaderRow());
510
+ const customContainer = createProfilesCard(i18nString(UIStrings.customProfiles), this.customList, settingsContent);
511
+ customContainer.appendChild(addButton);
501
512
 
502
513
  this.customUserConditions = SDK.NetworkManager.customUserNetworkConditionsSetting();
503
514
  this.customUserConditions.addChangeListener(this.conditionsUpdated, this);
@@ -533,19 +544,17 @@ export class ThrottlingSettingsTab extends UI.Widget.VBox implements
533
544
  }
534
545
 
535
546
  private conditionsUpdated(): void {
536
- this.list.clear();
547
+ this.customList.clear();
537
548
 
538
549
  const conditions = this.customUserConditions.get();
539
550
  for (let i = 0; i < conditions.length; ++i) {
540
- this.list.appendItem(conditions[i], true);
551
+ this.customList.appendItem(conditions[i], true);
541
552
  }
542
-
543
- this.list.appendSeparator();
544
553
  }
545
554
 
546
555
  private addButtonClicked(): void {
547
556
  this.#customUserConditionsCount++;
548
- this.list.addNewItem(this.customUserConditions.get().length, {
557
+ this.customList.addNewItem(this.customList.items.length, {
549
558
  key: `USER_CUSTOM_SETTING_${this.#customUserConditionsCount}`,
550
559
  title: () => '',
551
560
  download: -1,
@@ -563,7 +572,7 @@ export class ThrottlingSettingsTab extends UI.Widget.VBox implements
563
572
  const titleText = title.createChild('div', 'conditions-list-title-text');
564
573
  const castedTitle = this.retrieveOptionsTitle(conditions);
565
574
  titleText.textContent = castedTitle;
566
- UI.Tooltip.Tooltip.install(titleText, castedTitle);
575
+ UI.Tooltip.Tooltip.install(title, castedTitle);
567
576
  element.createChild('div', 'conditions-list-separator');
568
577
  element.createChild('div', 'conditions-list-text').textContent = throughputText(conditions.download);
569
578
  element.createChild('div', 'conditions-list-separator');
@@ -853,3 +862,41 @@ function percentText(percent: number): string {
853
862
  }
854
863
  return String(percent) + '%';
855
864
  }
865
+
866
+ function createProfilesCard(heading: string, list: UI.ListWidget.ListWidget<SDK.NetworkManager.Conditions>,
867
+ parent: Element): HTMLElement {
868
+ const card = parent.createChild('devtools-card');
869
+ card.heading = heading;
870
+ const container = card.createChild('div');
871
+ list.element.classList.add('conditions-list');
872
+ list.registerRequiredCSS(throttlingSettingsTabStyles);
873
+ list.show(container);
874
+ return container;
875
+ }
876
+
877
+ function appendHeaderColumn(element: Element, text: string): void {
878
+ element.createChild('div', 'conditions-list-separator');
879
+ const column = element.createChild('div', 'conditions-list-text');
880
+ column.textContent = text;
881
+ UI.Tooltip.Tooltip.install(column, text);
882
+ }
883
+
884
+ function createHeaderRow(): Element {
885
+ const element = document.createElement('div');
886
+ element.classList.add('conditions-list-item', 'conditions-list-header');
887
+
888
+ const title = element.createChild('div', 'conditions-list-text conditions-list-title');
889
+ const titleText = title.createChild('div', 'conditions-list-title-text');
890
+ const profileName = i18nString(UIStrings.profileName);
891
+ titleText.textContent = profileName;
892
+ UI.Tooltip.Tooltip.install(title, profileName);
893
+
894
+ appendHeaderColumn(element, i18nString(UIStrings.download));
895
+ appendHeaderColumn(element, i18nString(UIStrings.upload));
896
+ appendHeaderColumn(element, i18nString(UIStrings.latency));
897
+ appendHeaderColumn(element, i18nString(UIStrings.packetLoss));
898
+ appendHeaderColumn(element, i18nString(UIStrings.packetQueueLength));
899
+ appendHeaderColumn(element, i18nString(UIStrings.packetReordering));
900
+
901
+ return element;
902
+ }
@@ -148,3 +148,8 @@
148
148
  align-items: center;
149
149
  gap: 5px;
150
150
  }
151
+
152
+ .conditions-list-header {
153
+ font-weight: bold;
154
+ border-bottom: 1px solid var(--sys-color-divider);
155
+ }
@@ -0,0 +1,101 @@
1
+ // Copyright 2026 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import * as Common from '../../core/common/common.js';
6
+ import type * as SDK from '../../core/sdk/sdk.js';
7
+
8
+ const resourceTypeToAsAttribute = new Map<Common.ResourceType.ResourceType, string>([
9
+ [Common.ResourceType.resourceTypes.Document, 'document'],
10
+ [Common.ResourceType.resourceTypes.Stylesheet, 'style'],
11
+ [Common.ResourceType.resourceTypes.Image, 'image'],
12
+ [Common.ResourceType.resourceTypes.Font, 'font'],
13
+ [Common.ResourceType.resourceTypes.Script, 'script'],
14
+ [Common.ResourceType.resourceTypes.TextTrack, 'track'],
15
+ [Common.ResourceType.resourceTypes.Manifest, 'manifest'],
16
+ [Common.ResourceType.resourceTypes.Fetch, 'fetch'],
17
+ [Common.ResourceType.resourceTypes.XHR, 'fetch'],
18
+ [Common.ResourceType.resourceTypes.Wasm, 'fetch'],
19
+ ]);
20
+
21
+ /**
22
+ * Escapes HTML special characters.
23
+ * Crucial for URLs in href attributes to prevent characters like '&' from being
24
+ * interpreted as HTML entities (e.g., '&copy' becoming '©').
25
+ */
26
+ function escapeHTML(str: string): string {
27
+ return str.replace(/&/g, '&amp;')
28
+ .replace(/</g, '&lt;')
29
+ .replace(/>/g, '&gt;')
30
+ .replace(/"/g, '&quot;')
31
+ .replace(/'/g, '&#039;');
32
+ }
33
+
34
+ function parsePreloadLinkOrigins(request: SDK.NetworkRequest.NetworkRequest):
35
+ {requestOrigin: string|null, documentOrigin: string|null} {
36
+ const requestOrigin = request.parsedURL.securityOrigin();
37
+ const documentURL = request.documentURL;
38
+ const documentOrigin =
39
+ documentURL ? (Common.ParsedURL.ParsedURL.fromString(documentURL)?.securityOrigin() ?? null) : null;
40
+ return {requestOrigin, documentOrigin};
41
+ }
42
+
43
+ function determinePreloadLinkHref(request: SDK.NetworkRequest.NetworkRequest, isSameOrigin: boolean): string {
44
+ // For same-origin resources, use root-relative URL for environment portability.
45
+ if (isSameOrigin && request.parsedURL.isValid) {
46
+ return request.parsedURL.path + (request.parsedURL.queryParams ? '?' + request.parsedURL.queryParams : '');
47
+ }
48
+ return request.url();
49
+ }
50
+
51
+ function determinePreloadLinkCrossOrigin(request: SDK.NetworkRequest.NetworkRequest, isCrossOrigin: boolean): string {
52
+ const resourceType = request.resourceType();
53
+ const asValue = resourceTypeToAsAttribute.get(resourceType);
54
+ const isFont = resourceType === Common.ResourceType.resourceTypes.Font;
55
+ const isFetch = asValue === 'fetch';
56
+ const secFetchMode = request.requestHeaderValue('sec-fetch-mode')?.toLowerCase();
57
+
58
+ const needsCrossOrigin = isFont || isFetch || secFetchMode === 'cors';
59
+
60
+ if (!needsCrossOrigin) {
61
+ return '';
62
+ }
63
+
64
+ const hasCredentials = request.includedRequestCookies().length > 0 ||
65
+ Boolean(request.requestHeaderValue('cookie') || request.requestHeaderValue('authorization'));
66
+
67
+ // Only use credentials mode if it is cross-origin AND has credentials.
68
+ // Same-origin CORS (anonymous) automatically sends credentials.
69
+ const useCredentials = isCrossOrigin && hasCredentials;
70
+ return useCredentials ? ' crossorigin="use-credentials"' : ' crossorigin';
71
+ }
72
+
73
+ /**
74
+ * Determines whether a network request can be preloaded.
75
+ */
76
+ export function canPreloadRequest(request: SDK.NetworkRequest.NetworkRequest): boolean {
77
+ return resourceTypeToAsAttribute.has(request.resourceType());
78
+ }
79
+
80
+ /**
81
+ * Generates an HTML `<link rel="preload">` element string for a given network request.
82
+ */
83
+ export function generatePreloadLink(request: SDK.NetworkRequest.NetworkRequest): string {
84
+ const {requestOrigin, documentOrigin} = parsePreloadLinkOrigins(request);
85
+ const isSameOrigin = Boolean(requestOrigin && documentOrigin && requestOrigin === documentOrigin);
86
+ const isCrossOrigin = Boolean(requestOrigin && documentOrigin && requestOrigin !== documentOrigin);
87
+
88
+ const url = determinePreloadLinkHref(request, isSameOrigin);
89
+ const escapedUrl = escapeHTML(url);
90
+
91
+ const resourceType = request.resourceType();
92
+ const escapedMimeType = request.mimeType ? escapeHTML(request.mimeType) : '';
93
+
94
+ const asValue = resourceTypeToAsAttribute.get(resourceType);
95
+ const asAttr = `as="${asValue ?? 'fetch'}"`;
96
+
97
+ const crossoriginAttr = determinePreloadLinkCrossOrigin(request, isCrossOrigin);
98
+ const typeAttr = escapedMimeType ? ` type="${escapedMimeType}"` : '';
99
+
100
+ return `<link rel="preload" href="${escapedUrl}" ${asAttr}${typeAttr}${crossoriginAttr}>`;
101
+ }
@@ -61,6 +61,7 @@ import * as Components from '../../ui/legacy/components/utils/utils.js';
61
61
  import * as UI from '../../ui/legacy/legacy.js';
62
62
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
63
63
 
64
+ import {canPreloadRequest, generatePreloadLink} from './LinkPreloadGenerator.js';
64
65
  import {
65
66
  Events,
66
67
  type EventTypes,
@@ -284,6 +285,11 @@ const UIStrings = {
284
285
  * refers to the format the data will be copied as, which is compatible with the fetch web API.
285
286
  */
286
287
  copyAsFetch: 'Copy as `fetch`',
288
+ /**
289
+ * @description A context menu command in the Network panel, for copying a resource's link element
290
+ * to the clipboard. 'preload' refers to the HTML link relation format the data will be copied as.
291
+ */
292
+ copyAsPreload: 'Copy as preload element',
287
293
  /**
288
294
  * @description Text in Network Log View of the Network panel. An action that copies a command to
289
295
  * the developer's clipboard. The command allows the developer to replay this specific network
@@ -1812,6 +1818,7 @@ export class NetworkLogView extends Common.ObjectWrapper.eventMixin<EventTypes,
1812
1818
  copyMenu.defaultSection().appendItem(
1813
1819
  i18nString(UIStrings.copyAsNodejsFetch), this.copyFetchCall.bind(this, request, FetchStyle.NODE_JS),
1814
1820
  {disabled: disableIfBlob, jslogContext: 'copy-as-nodejs-fetch'});
1821
+ this.appendCopyAsPreloadItem(copyMenu, request);
1815
1822
 
1816
1823
  if (Host.Platform.isWin()) {
1817
1824
  copyMenu.footerSection().appendItem(
@@ -2011,6 +2018,27 @@ export class NetworkLogView extends Common.ObjectWrapper.eventMixin<EventTypes,
2011
2018
  Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(commands);
2012
2019
  }
2013
2020
 
2021
+ private copyPreloadElement(request: SDK.NetworkRequest.NetworkRequest): void {
2022
+ const preloadLink = generatePreloadLink(request);
2023
+ Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(preloadLink);
2024
+ }
2025
+
2026
+ private appendCopyAsPreloadItem(copyMenu: UI.ContextMenu.SubMenu, request: SDK.NetworkRequest.NetworkRequest): void {
2027
+ const isHttpOrHttps = request.parsedURL.scheme === 'http' || request.parsedURL.scheme === 'https';
2028
+ const isGetRequest = request.requestMethod === 'GET';
2029
+
2030
+ const networkManager = SDK.NetworkManager.NetworkManager.forRequest(request);
2031
+ const resourceTreeModel = networkManager?.target().model(SDK.ResourceTreeModel.ResourceTreeModel);
2032
+ const isMainDocument = Boolean(resourceTreeModel?.mainFrame && resourceTreeModel.mainFrame.id === request.frameId &&
2033
+ request.resourceType() === Common.ResourceType.resourceTypes.Document);
2034
+
2035
+ const disablePreload =
2036
+ !isHttpOrHttps || request.isBlobRequest() || !isGetRequest || isMainDocument || !canPreloadRequest(request);
2037
+ copyMenu.defaultSection().appendItem(i18nString(UIStrings.copyAsPreload),
2038
+ this.copyPreloadElement.bind(this, request),
2039
+ {disabled: disablePreload, jslogContext: 'copy-as-preload'});
2040
+ }
2041
+
2014
2042
  private async copyFetchCall(request: SDK.NetworkRequest.NetworkRequest, style: FetchStyle): Promise<void> {
2015
2043
  const command = await this.generateFetchCall(request, style);
2016
2044
  Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(command);
@@ -31,6 +31,7 @@ import './NetworkPanel.js';
31
31
 
32
32
  import * as BinaryResourceView from './BinaryResourceView.js';
33
33
  import * as EventSourceMessagesView from './EventSourceMessagesView.js';
34
+ import * as LinkPreloadGenerator from './LinkPreloadGenerator.js';
34
35
  import * as NetworkConfigView from './NetworkConfigView.js';
35
36
  import * as NetworkDataGridNode from './NetworkDataGridNode.js';
36
37
  import * as NetworkFrameGrouper from './NetworkFrameGrouper.js';
@@ -60,6 +61,7 @@ import * as SignedExchangeInfoView from './SignedExchangeInfoView.js';
60
61
  export {
61
62
  BinaryResourceView,
62
63
  EventSourceMessagesView,
64
+ LinkPreloadGenerator,
63
65
  NetworkConfigView,
64
66
  NetworkDataGridNode,
65
67
  NetworkFrameGrouper,