chrome-devtools-frontend 1.0.1652307 → 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.
- package/.agents/skills/fixing-skipped-tests/SKILL.md +32 -0
- package/.agents/skills/migrate-chromium-test/SKILL.md +144 -0
- package/docs/get_the_code.md +1 -2
- package/docs/ui_engineering.md +3 -1
- package/eslint.config.mjs +0 -5
- package/front_end/core/sdk/CPUThrottlingManager.ts +13 -8
- package/front_end/core/sdk/CSSMetadata.ts +23 -0
- package/front_end/core/sdk/CompilerSourceMappingContentProvider.ts +8 -4
- package/front_end/core/sdk/DOMModel.ts +4 -0
- package/front_end/core/sdk/EmulationModel.ts +4 -0
- package/front_end/core/sdk/ResourceTreeModel.ts +3 -2
- package/front_end/entrypoints/heap_snapshot_worker/HeapSnapshot.ts +459 -74
- package/front_end/entrypoints/main/MainImpl.ts +0 -6
- package/front_end/foundation/Universe.ts +18 -0
- package/front_end/generated/InspectorBackendCommands.ts +6 -5
- package/front_end/generated/SupportedCSSProperties.js +122 -28
- package/front_end/generated/protocol.ts +58 -0
- package/front_end/models/ai_assistance/AiConversation.ts +2 -1
- package/front_end/models/ai_assistance/README.md +4 -3
- package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +1 -1
- package/front_end/models/ai_assistance/agents/PerformanceAgent.snapshot.txt +2 -2
- package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +11 -164
- package/front_end/models/ai_assistance/ai_assistance.ts +6 -2
- package/front_end/models/ai_assistance/contexts/PerformanceTraceContext.ts +295 -0
- package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +10 -10
- package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +12 -11
- package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +6 -6
- package/front_end/models/ai_assistance/skills/accessibility.md +4 -2
- package/front_end/models/ai_assistance/tools/GetElementAccessibilityDetails.ts +142 -0
- package/front_end/models/ai_assistance/tools/GetStyles.ts +1 -1
- package/front_end/models/ai_assistance/tools/README.md +21 -1
- package/front_end/models/ai_assistance/tools/{ResolveLighthousePath.ts → ResolveDevtoolsNodePath.ts} +18 -17
- package/front_end/models/ai_assistance/tools/Tool.ts +6 -1
- package/front_end/models/ai_assistance/tools/ToolRegistry.ts +4 -2
- package/front_end/models/autofill_manager/AutofillManager.ts +7 -15
- package/front_end/models/bindings/NetworkProject.ts +4 -8
- package/front_end/models/breakpoints/BreakpointManager.ts +9 -9
- package/front_end/models/emulation/DeviceModeModel.ts +24 -0
- package/front_end/models/emulation/EmulatedDevices.ts +108 -0
- package/front_end/models/heap_snapshot/HeapSnapshotModel.ts +36 -0
- package/front_end/models/heap_snapshot/HeapSnapshotProxy.ts +8 -0
- package/front_end/models/issues_manager/IssuesManager.ts +10 -6
- package/front_end/models/javascript_metadata/JavaScriptMetadata.ts +4 -5
- package/front_end/models/live-metrics/LiveMetrics.ts +6 -6
- package/front_end/models/live-metrics/web-vitals-injected/spec/spec.ts +5 -5
- package/front_end/models/live-metrics/web-vitals-injected/web-vitals-injected.ts +3 -3
- package/front_end/models/logs/LogManager.ts +25 -10
- package/front_end/models/logs/NetworkLog.ts +24 -19
- package/front_end/models/persistence/AutomaticFileSystemWorkspaceBinding.ts +14 -5
- package/front_end/models/persistence/IsolatedFileSystem.ts +9 -11
- package/front_end/models/persistence/IsolatedFileSystemManager.ts +5 -3
- package/front_end/models/persistence/NetworkPersistenceManager.ts +73 -44
- package/front_end/models/persistence/PersistenceImpl.ts +4 -5
- package/front_end/models/trace/EntityMapper.ts +1 -1
- package/front_end/panels/ai_assistance/AiAssistancePanel.ts +9 -9
- package/front_end/panels/ai_assistance/ExportConversation.ts +4 -1
- package/front_end/panels/ai_assistance/components/ChatInput.ts +2 -2
- package/front_end/panels/application/IndexedDBViews.ts +38 -36
- package/front_end/panels/application/components/AdsView.ts +4 -0
- package/front_end/panels/autofill/AutofillView.ts +1 -1
- package/front_end/panels/autofill/autofill-meta.ts +3 -2
- package/front_end/panels/elements/AccessibilityTreeUtils.ts +26 -15
- package/front_end/panels/elements/AccessibilityTreeView.ts +12 -9
- package/front_end/panels/elements/ElementsTreeOutline.ts +5 -0
- package/front_end/panels/layer_viewer/LayerTreeOutline.ts +118 -68
- package/front_end/panels/layer_viewer/Layers3DView.ts +1 -1
- package/front_end/panels/layer_viewer/layerTreeOutline.css +20 -13
- package/front_end/panels/layers/LayersPanel.ts +1 -1
- package/front_end/panels/mobile_throttling/ThrottlingSettingsTab.ts +64 -17
- package/front_end/panels/mobile_throttling/throttlingSettingsTab.css +5 -0
- package/front_end/panels/network/LinkPreloadGenerator.ts +101 -0
- package/front_end/panels/network/NetworkLogView.ts +28 -0
- package/front_end/panels/network/network.ts +2 -0
- package/front_end/panels/profiler/HeapSnapshotView.ts +121 -33
- package/front_end/panels/profiler/ModuleUIStrings.ts +4 -0
- package/front_end/panels/timeline/TimelineHistoryManager.ts +38 -37
- package/front_end/panels/timeline/TimelinePanel.ts +32 -8
- package/front_end/panels/timeline/components/CPUThrottlingSelector.ts +7 -6
- package/front_end/panels/timeline/components/LiveMetricsView.ts +38 -38
- package/front_end/panels/timeline/components/MetricCard.ts +18 -18
- package/front_end/panels/timeline/components/liveMetricsView.css +6 -6
- package/front_end/panels/timeline/components/metricCard.css +4 -4
- package/front_end/panels/whats_new/ReleaseNoteText.ts +3 -3
- package/front_end/panels/whats_new/resources/WNDT.md +1 -1
- package/front_end/third_party/chromium/README.chromium +1 -1
- package/front_end/ui/legacy/InspectorDrawerView.ts +7 -5
- package/front_end/ui/legacy/ListWidget.ts +12 -0
- package/front_end/ui/legacy/SplitWidget.ts +12 -11
- package/front_end/ui/legacy/components/data_grid/DataGridElement.ts +23 -19
- package/front_end/ui/legacy/components/object_ui/CustomPreviewComponent.ts +8 -1
- package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +110 -17
- package/front_end/ui/legacy/treeoutline.css +1 -1
- package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
- 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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
166
|
+
if (this.layerTree && root) {
|
|
167
|
+
const buildTree = (layer: SDK.LayerTreeBase.Layer): void => {
|
|
168
|
+
if (!layer.drawsContent() && !showInternalLayers) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
149
171
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
this.layerTree.forEachLayer(buildTree, root);
|
|
182
198
|
}
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
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
|
|
213
|
-
this
|
|
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('
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
21
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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.
|
|
497
|
-
this.
|
|
498
|
-
this.
|
|
499
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
+
}
|
|
@@ -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., '©' becoming '©').
|
|
25
|
+
*/
|
|
26
|
+
function escapeHTML(str: string): string {
|
|
27
|
+
return str.replace(/&/g, '&')
|
|
28
|
+
.replace(/</g, '<')
|
|
29
|
+
.replace(/>/g, '>')
|
|
30
|
+
.replace(/"/g, '"')
|
|
31
|
+
.replace(/'/g, ''');
|
|
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,
|