chrome-ai-bridge 1.0.2 → 1.0.4

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 (69) hide show
  1. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Base64.js +20 -2
  2. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Gzip.js +11 -0
  3. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Object.js +6 -1
  4. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/ParsedURL.js +3 -0
  5. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/ResourceType.js +6 -0
  6. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Settings.js +18 -8
  7. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/InspectorFrontendHostStub.js +3 -3
  8. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/ResourceLoader.js +1 -1
  9. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +17 -1
  10. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/ArrayUtilities.js +10 -0
  11. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/StringUtilities.js +63 -12
  12. package/build/node_modules/chrome-devtools-frontend/front_end/core/protocol_client/CDPConnection.js +1 -0
  13. package/build/node_modules/chrome-devtools-frontend/front_end/core/protocol_client/InspectorBackend.js +4 -1
  14. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +44 -9
  15. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSMetadata.js +6 -6
  16. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSModel.js +1 -1
  17. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/DOMModel.js +169 -12
  18. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/DebuggerModel.js +2 -1
  19. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/IsolateManager.js +6 -0
  20. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +18 -4
  21. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +7 -21
  22. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/OverlayModel.js +17 -5
  23. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js +5 -1
  24. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ResourceTreeModel.js +8 -5
  25. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMap.js +14 -2
  26. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMapManager.js +1 -1
  27. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMapScopesInfo.js +11 -4
  28. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/Target.js +3 -1
  29. package/build/node_modules/chrome-devtools-frontend/front_end/generated/ARIAProperties.js +1 -1
  30. package/build/node_modules/chrome-devtools-frontend/front_end/generated/Deprecation.js +1 -16
  31. package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +35 -14
  32. package/build/node_modules/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +197 -101
  33. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.js +2 -1
  34. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +10 -16
  35. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +97 -26
  36. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +35 -0
  37. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CompilerScriptMapping.js +5 -3
  38. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DebuggerWorkspaceBinding.js +7 -3
  39. package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/DeviceModeModel.js +1 -1
  40. package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/EmulatedDevices.js +14 -0
  41. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +8 -5
  42. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceImpl.js +70 -1
  43. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceModel.js +82 -30
  44. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/EventsSerializer.js +7 -2
  45. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/LanternComputationData.js +2 -2
  46. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/Processor.js +18 -19
  47. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/Styles.js +12 -4
  48. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/extras/Initiators.js +46 -0
  49. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/extras/TraceTree.js +4 -3
  50. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/extras/extras.js +1 -0
  51. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/LargestImagePaintHandler.js +2 -2
  52. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/LayoutShiftsHandler.js +1 -1
  53. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/MetaHandler.js +6 -0
  54. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/NetworkRequestsHandler.js +10 -1
  55. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/PageLoadMetricsHandler.js +44 -27
  56. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Timing.js +9 -2
  57. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/Common.js +1 -6
  58. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LCPBreakdown.js +2 -2
  59. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LCPDiscovery.js +2 -4
  60. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/NetworkDependencyTree.js +3 -2
  61. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/RenderBlocking.js +1 -1
  62. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js +30 -11
  63. package/build/node_modules/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/decode/decode.js +28 -13
  64. package/build/node_modules/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/encode/encoder.js +1 -1
  65. package/build/node_modules/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/scopes.js +4 -0
  66. package/build/src/tools/chatgpt-web.js +102 -64
  67. package/build/src/tools/gemini-web.js +43 -16
  68. package/build/src/tools/pages.js +0 -1
  69. package/package.json +1 -1
@@ -9,11 +9,13 @@ export class StackTraceImpl extends Common.ObjectWrapper.ObjectWrapper {
9
9
  super();
10
10
  this.syncFragment = syncFragment;
11
11
  this.asyncFragments = asyncFragments;
12
- syncFragment.stackTraces.add(this);
12
+ const fragment = syncFragment instanceof DebuggableFragmentImpl ? syncFragment.fragment : syncFragment;
13
+ fragment.stackTraces.add(this);
13
14
  this.asyncFragments.forEach(asyncFragment => asyncFragment.fragment.stackTraces.add(this));
14
15
  }
15
16
  }
16
17
  export class FragmentImpl {
18
+ static EMPTY_FRAGMENT = new FragmentImpl();
17
19
  node;
18
20
  stackTraces = new Set();
19
21
  /**
@@ -31,6 +33,9 @@ export class FragmentImpl {
31
33
  this.node = node;
32
34
  }
33
35
  get frames() {
36
+ if (!this.node) {
37
+ return [];
38
+ }
34
39
  const frames = [];
35
40
  for (const node of this.node.getCallStack()) {
36
41
  frames.push(...node.frames);
@@ -65,3 +70,67 @@ export class FrameImpl {
65
70
  this.missingDebugInfo = missingDebugInfo;
66
71
  }
67
72
  }
73
+ /**
74
+ * A DebuggableFragmentImpl wraps an existing FragmentImpl. This is important: We can pause at the
75
+ * same location multiple times and the paused information changes each and everytime while the underlying
76
+ * FragmentImpl will stay the same.
77
+ */
78
+ export class DebuggableFragmentImpl {
79
+ fragment;
80
+ callFrames;
81
+ constructor(fragment, callFrames) {
82
+ this.fragment = fragment;
83
+ this.callFrames = callFrames;
84
+ }
85
+ get frames() {
86
+ if (!this.fragment.node) {
87
+ return [];
88
+ }
89
+ const frames = [];
90
+ let index = 0;
91
+ for (const node of this.fragment.node.getCallStack()) {
92
+ for (const [inlineIdx, frame] of node.frames.entries()) {
93
+ // Create virtual frames for inlined frames.
94
+ const sdkFrame = inlineIdx === 0 ? this.callFrames[index] :
95
+ this.callFrames[index].createVirtualCallFrame(inlineIdx, frame.name ?? '');
96
+ frames.push(new DebuggableFrameImpl(frame, sdkFrame));
97
+ }
98
+ index++;
99
+ }
100
+ return frames;
101
+ }
102
+ }
103
+ /**
104
+ * A DebuggableFrameImpl wraps an existing FrameImpl. This is important: We can pause at the
105
+ * same location multiple times and the paused information changes each and everytime while the underlying
106
+ * FrameImpl will stay the same.
107
+ */
108
+ export class DebuggableFrameImpl {
109
+ #frame;
110
+ #sdkFrame;
111
+ constructor(frame, sdkFrame) {
112
+ this.#frame = frame;
113
+ this.#sdkFrame = sdkFrame;
114
+ }
115
+ get url() {
116
+ return this.#frame.url;
117
+ }
118
+ get uiSourceCode() {
119
+ return this.#frame.uiSourceCode;
120
+ }
121
+ get name() {
122
+ return this.#frame.name;
123
+ }
124
+ get line() {
125
+ return this.#frame.line;
126
+ }
127
+ get column() {
128
+ return this.#frame.column;
129
+ }
130
+ get missingDebugInfo() {
131
+ return this.#frame.missingDebugInfo;
132
+ }
133
+ get sdkFrame() {
134
+ return this.#sdkFrame;
135
+ }
136
+ }
@@ -1,10 +1,12 @@
1
1
  // Copyright 2025 The Chromium Authors
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
+ var _a;
5
+ import * as Common from '../../core/common/common.js';
4
6
  import * as SDK from '../../core/sdk/sdk.js';
5
7
  // eslint-disable-next-line @devtools/es-modules-import
6
8
  import * as StackTrace from './stack_trace.js';
7
- import { AsyncFragmentImpl, FragmentImpl, FrameImpl, StackTraceImpl } from './StackTraceImpl.js';
9
+ import { AsyncFragmentImpl, DebuggableFragmentImpl, FragmentImpl, FrameImpl, StackTraceImpl } from './StackTraceImpl.js';
8
10
  import { Trie } from './Trie.js';
9
11
  /**
10
12
  * The {@link StackTraceModel} is a thin wrapper around a fragment trie.
@@ -13,54 +15,103 @@ import { Trie } from './Trie.js';
13
15
  */
14
16
  export class StackTraceModel extends SDK.SDKModel.SDKModel {
15
17
  #trie = new Trie();
18
+ #mutex = new Common.Mutex.Mutex();
16
19
  /** @returns the {@link StackTraceModel} for the target, or the model for the primaryPageTarget when passing null/undefined */
17
20
  static #modelForTarget(target) {
18
- const model = (target ?? SDK.TargetManager.TargetManager.instance().primaryPageTarget())?.model(StackTraceModel);
21
+ const model = (target ?? SDK.TargetManager.TargetManager.instance().primaryPageTarget())?.model(_a);
19
22
  if (!model) {
20
23
  throw new Error('Unable to find StackTraceModel');
21
24
  }
22
25
  return model;
23
26
  }
24
27
  async createFromProtocolRuntime(stackTrace, rawFramesToUIFrames) {
25
- const translatePromises = [];
26
- const fragment = this.#createFragment(stackTrace.callFrames);
27
- translatePromises.push(this.#translateFragment(fragment, rawFramesToUIFrames));
28
+ const [syncFragment, asyncFragments] = await Promise.all([
29
+ this.#createFragment(stackTrace.callFrames, rawFramesToUIFrames),
30
+ this.#createAsyncFragments(stackTrace, rawFramesToUIFrames),
31
+ ]);
32
+ return new StackTraceImpl(syncFragment, asyncFragments);
33
+ }
34
+ async createFromDebuggerPaused(pausedDetails, rawFramesToUIFrames) {
35
+ const [syncFragment, asyncFragments] = await Promise.all([
36
+ this.#createDebuggableFragment(pausedDetails, rawFramesToUIFrames),
37
+ this.#createAsyncFragments(pausedDetails, rawFramesToUIFrames),
38
+ ]);
39
+ return new StackTraceImpl(syncFragment, asyncFragments);
40
+ }
41
+ /** Trigger re-translation of all fragments with the provide script in their call stack */
42
+ async scriptInfoChanged(script, translateRawFrames) {
43
+ const release = await this.#mutex.acquire();
44
+ try {
45
+ const translatePromises = [];
46
+ let stackTracesToUpdate = new Set();
47
+ for (const fragment of this.#affectedFragments(script)) {
48
+ // We trigger re-translation only for fragments of leaf-nodes. Any fragment along the ancestor-chain
49
+ // is re-translated as a side-effect.
50
+ // We just need to remember the stack traces of the skipped over fragments, so we can send the
51
+ // UPDATED event also to them.
52
+ if (fragment.node?.children.length === 0) {
53
+ translatePromises.push(this.#translateFragment(fragment, translateRawFrames));
54
+ }
55
+ stackTracesToUpdate = stackTracesToUpdate.union(fragment.stackTraces);
56
+ }
57
+ await Promise.all(translatePromises);
58
+ for (const stackTrace of stackTracesToUpdate) {
59
+ stackTrace.dispatchEventToListeners("UPDATED" /* StackTrace.StackTrace.Events.UPDATED */);
60
+ }
61
+ }
62
+ finally {
63
+ release();
64
+ }
65
+ }
66
+ async #createDebuggableFragment(pausedDetails, rawFramesToUIFrames) {
67
+ const fragment = await this.#createFragment(pausedDetails.callFrames.map(frame => ({
68
+ scriptId: frame.script.scriptId,
69
+ url: frame.script.sourceURL,
70
+ functionName: frame.functionName,
71
+ lineNumber: frame.location().lineNumber,
72
+ columnNumber: frame.location().columnNumber,
73
+ })), rawFramesToUIFrames);
74
+ return new DebuggableFragmentImpl(fragment, pausedDetails.callFrames);
75
+ }
76
+ async #createAsyncFragments(stackTraceOrPausedEvent, rawFramesToUIFrames) {
28
77
  const asyncFragments = [];
29
78
  const debuggerModel = this.target().model(SDK.DebuggerModel.DebuggerModel);
30
79
  if (debuggerModel) {
31
- for await (const { stackTrace: asyncStackTrace, target } of debuggerModel.iterateAsyncParents(stackTrace)) {
32
- const model = StackTraceModel.#modelForTarget(target);
33
- const asyncFragment = model.#createFragment(asyncStackTrace.callFrames);
34
- translatePromises.push(model.#translateFragment(asyncFragment, rawFramesToUIFrames));
35
- asyncFragments.push(new AsyncFragmentImpl(asyncStackTrace.description ?? '', asyncFragment));
80
+ for await (const { stackTrace: asyncStackTrace, target } of debuggerModel.iterateAsyncParents(stackTraceOrPausedEvent)) {
81
+ if (asyncStackTrace.callFrames.length === 0) {
82
+ // Skip empty async fragments, they don't add value.
83
+ continue;
84
+ }
85
+ const model = _a.#modelForTarget(target);
86
+ const asyncFragmentPromise = model.#createFragment(asyncStackTrace.callFrames, rawFramesToUIFrames)
87
+ .then(fragment => new AsyncFragmentImpl(asyncStackTrace.description ?? '', fragment));
88
+ asyncFragments.push(asyncFragmentPromise);
36
89
  }
37
90
  }
38
- await Promise.all(translatePromises);
39
- return new StackTraceImpl(fragment, asyncFragments);
91
+ return await Promise.all(asyncFragments);
40
92
  }
41
- /** Trigger re-translation of all fragments with the provide script in their call stack */
42
- async scriptInfoChanged(script, translateRawFrames) {
43
- const translatePromises = [];
44
- let stackTracesToUpdate = new Set();
45
- for (const fragment of this.#affectedFragments(script)) {
46
- // We trigger re-translation only for fragments of leaf-nodes. Any fragment along the ancestor-chain
47
- // is re-translated as a side-effect.
48
- // We just need to remember the stack traces of the skipped over fragments, so we can send the
49
- // UPDATED event also to them.
50
- if (fragment.node.children.length === 0) {
51
- translatePromises.push(this.#translateFragment(fragment, translateRawFrames));
93
+ async #createFragment(frames, rawFramesToUIFrames) {
94
+ if (frames.length === 0) {
95
+ return FragmentImpl.EMPTY_FRAGMENT;
96
+ }
97
+ const release = await this.#mutex.acquire();
98
+ try {
99
+ const node = this.#trie.insert(frames);
100
+ const requiresTranslation = !Boolean(node.fragment);
101
+ const fragment = FragmentImpl.getOrCreate(node);
102
+ if (requiresTranslation) {
103
+ await this.#translateFragment(fragment, rawFramesToUIFrames);
52
104
  }
53
- stackTracesToUpdate = stackTracesToUpdate.union(fragment.stackTraces);
105
+ return fragment;
54
106
  }
55
- await Promise.all(translatePromises);
56
- for (const stackTrace of stackTracesToUpdate) {
57
- stackTrace.dispatchEventToListeners("UPDATED" /* StackTrace.StackTrace.Events.UPDATED */);
107
+ finally {
108
+ release();
58
109
  }
59
110
  }
60
- #createFragment(frames) {
61
- return FragmentImpl.getOrCreate(this.#trie.insert(frames));
62
- }
63
111
  async #translateFragment(fragment, rawFramesToUIFrames) {
112
+ if (!fragment.node) {
113
+ return;
114
+ }
64
115
  const rawFrames = fragment.node.getCallStack().map(node => node.rawFrame).toArray();
65
116
  const uiFrames = await rawFramesToUIFrames(rawFrames, this.target());
66
117
  console.assert(rawFrames.length === uiFrames.length, 'Broken rawFramesToUIFrames implementation');
@@ -94,4 +145,5 @@ export class StackTraceModel extends SDK.SDKModel.SDKModel {
94
145
  return fragments;
95
146
  }
96
147
  }
148
+ _a = StackTraceModel;
97
149
  SDK.SDKModel.SDKModel.register(StackTraceModel, { capabilities: 0 /* SDK.Target.Capability.NONE */, autostart: false });
@@ -13,9 +13,14 @@ export class EventsSerializer {
13
13
  return `${"l" /* Types.File.EventKeyType.LEGACY_TIMELINE_FRAME */}-${event.index}`;
14
14
  }
15
15
  const rawEvents = Helpers.SyntheticEvents.SyntheticEventsManager.getActiveManager().getRawTraceEvents();
16
+ const isSynthetic = Types.Events.isSyntheticBased(event);
17
+ const index = rawEvents.indexOf(isSynthetic ? event.rawSourceEvent : event);
18
+ if (index === -1) {
19
+ throw new Error(`Unknown trace event: ${event.name}`);
20
+ }
16
21
  const key = Types.Events.isSyntheticBased(event) ?
17
- `${"s" /* Types.File.EventKeyType.SYNTHETIC_EVENT */}-${rawEvents.indexOf(event.rawSourceEvent)}` :
18
- `${"r" /* Types.File.EventKeyType.RAW_EVENT */}-${rawEvents.indexOf(event)}`;
22
+ `${"s" /* Types.File.EventKeyType.SYNTHETIC_EVENT */}-${index}` :
23
+ `${"r" /* Types.File.EventKeyType.RAW_EVENT */}-${index}`;
19
24
  if (key.length < 3) {
20
25
  return null;
21
26
  }
@@ -3,12 +3,12 @@
3
3
  // found in the LICENSE file.
4
4
  import * as Handlers from './handlers/handlers.js';
5
5
  import * as Lantern from './lantern/lantern.js';
6
- function createProcessedNavigation(data, frameId, navigationId) {
6
+ function createProcessedNavigation(data, frameId, navigation) {
7
7
  const scoresByNav = data.PageLoadMetrics.metricScoresByFrameId.get(frameId);
8
8
  if (!scoresByNav) {
9
9
  throw new Lantern.Core.LanternError('missing metric scores for frame');
10
10
  }
11
- const scores = scoresByNav.get(navigationId);
11
+ const scores = scoresByNav.get(navigation);
12
12
  if (!scores) {
13
13
  throw new Lantern.Core.LanternError('missing metric scores for specified navigation');
14
14
  }
@@ -212,7 +212,7 @@ export class TraceProcessor extends EventTarget {
212
212
  }
213
213
  return this.#insights;
214
214
  }
215
- #createLanternContext(data, traceEvents, frameId, navigationId, options) {
215
+ #createLanternContext(data, traceEvents, frameId, navigation, options) {
216
216
  // Check for required handlers.
217
217
  if (!data.NetworkRequests || !data.Workers || !data.PageLoadMetrics) {
218
218
  return;
@@ -221,7 +221,7 @@ export class TraceProcessor extends EventTarget {
221
221
  throw new Lantern.Core.LanternError('No network requests found in trace');
222
222
  }
223
223
  const navStarts = data.Meta.navigationsByFrameId.get(frameId);
224
- const navStartIndex = navStarts?.findIndex(n => n.args.data?.navigationId === navigationId);
224
+ const navStartIndex = navStarts?.findIndex(n => n === navigation);
225
225
  if (!navStarts || navStartIndex === undefined || navStartIndex === -1) {
226
226
  throw new Lantern.Core.LanternError('Could not find navigation start');
227
227
  }
@@ -235,7 +235,7 @@ export class TraceProcessor extends EventTarget {
235
235
  };
236
236
  const requests = LanternComputationData.createNetworkRequests(trace, data, startTime, endTime);
237
237
  const graph = LanternComputationData.createGraph(requests, trace, data);
238
- const processedNavigation = LanternComputationData.createProcessedNavigation(data, frameId, navigationId);
238
+ const processedNavigation = LanternComputationData.createProcessedNavigation(data, frameId, navigation);
239
239
  const networkAnalysis = Lantern.Core.NetworkAnalyzer.analyze(requests);
240
240
  if (!networkAnalysis) {
241
241
  return;
@@ -304,10 +304,10 @@ export class TraceProcessor extends EventTarget {
304
304
  const observedInpScore = Insights.Common.evaluateINPMetricScore(observedInp);
305
305
  const observedClsScore = Insights.Common.evaluateCLSMetricScore(observedCls);
306
306
  const insightToSortingRank = new Map();
307
- for (const [name, model] of Object.entries(insightSet.model)) {
308
- const lcp = model.metricSavings?.LCP ?? 0;
309
- const inp = model.metricSavings?.INP ?? 0;
310
- const cls = model.metricSavings?.CLS ?? 0;
307
+ for (const [name, insight] of Object.entries(insightSet.model)) {
308
+ const lcp = insight.metricSavings?.LCP ?? 0;
309
+ const inp = insight.metricSavings?.INP ?? 0;
310
+ const cls = insight.metricSavings?.CLS ?? 0;
311
311
  const lcpPostSavings = observedLcp !== undefined ? Math.max(0, observedLcp - lcp) : undefined;
312
312
  const inpPostSavings = Math.max(0, observedInp - inp);
313
313
  const clsPostSavings = Math.max(0, observedCls - cls);
@@ -365,28 +365,28 @@ export class TraceProcessor extends EventTarget {
365
365
  urlString = data.Meta.finalDisplayUrlByNavigationId.get('') ?? data.Meta.mainFrameURL;
366
366
  }
367
367
  const insightSetModel = {};
368
+ const insightSetModelErrors = {};
368
369
  for (const [name, insight] of Object.entries(_a.getInsightRunners())) {
369
- let model;
370
370
  try {
371
371
  logger?.start(`insights:${name}`);
372
- model = insight.generateInsight(data, context);
372
+ const model = insight.generateInsight(data, context);
373
373
  model.frameId = context.frameId;
374
374
  const navId = context.navigation?.args.data?.navigationId;
375
375
  if (navId) {
376
- model.navigationId = navId;
376
+ model.navigation = context.navigation;
377
377
  }
378
378
  model.createOverlays = () => {
379
379
  // @ts-expect-error: model is a union of all possible insight model types.
380
380
  return insight.createOverlays(model);
381
381
  };
382
+ Object.assign(insightSetModel, { [name]: model });
382
383
  }
383
384
  catch (err) {
384
- model = err;
385
+ Object.assign(insightSetModelErrors, { [name]: err });
385
386
  }
386
387
  finally {
387
388
  logger?.end(`insights:${name}`);
388
389
  }
389
- Object.assign(insightSetModel, { [name]: model });
390
390
  }
391
391
  // We may choose to exclude the insightSet if it's trivial. Trivial means:
392
392
  // 1. There's no navigation (it's an initial trace period)
@@ -396,12 +396,10 @@ export class TraceProcessor extends EventTarget {
396
396
  // Generally, these cases are the short time ranges before a page reload starts.
397
397
  const isNavigation = id === Types.Events.NO_NAVIGATION;
398
398
  const trivialThreshold = Helpers.Timing.milliToMicro(Types.Timing.Milli(5000));
399
- const everyInsightPasses = Object.values(insightSetModel)
400
- .filter(model => !(model instanceof Error))
401
- .every(model => model.state === 'pass');
402
- const noLcp = !insightSetModel.LCPBreakdown.lcpEvent;
403
- const noInp = !insightSetModel.INPBreakdown.longestInteractionEvent;
404
- const noLayoutShifts = insightSetModel.CLSCulprits.shifts?.size === 0;
399
+ const everyInsightPasses = Object.values(insightSetModel).every(model => model && model.state === 'pass');
400
+ const noLcp = !insightSetModel.LCPBreakdown?.lcpEvent;
401
+ const noInp = !insightSetModel.INPBreakdown?.longestInteractionEvent;
402
+ const noLayoutShifts = insightSetModel.CLSCulprits?.shifts?.size === 0;
405
403
  const shouldExclude = isNavigation && context.bounds.range < trivialThreshold && everyInsightPasses && noLcp &&
406
404
  noInp && noLayoutShifts;
407
405
  if (shouldExclude) {
@@ -423,6 +421,7 @@ export class TraceProcessor extends EventTarget {
423
421
  frameId: context.frameId,
424
422
  bounds: context.bounds,
425
423
  model: insightSetModel,
424
+ modelErrors: insightSetModelErrors,
426
425
  };
427
426
  this.#insights.set(insightSet.id, insightSet);
428
427
  this.sortInsightSet(insightSet, context.options.metadata ?? null);
@@ -474,7 +473,7 @@ export class TraceProcessor extends EventTarget {
474
473
  let lantern;
475
474
  try {
476
475
  options.logger?.start('insights:createLanternContext');
477
- lantern = this.#createLanternContext(data, traceEvents, frameId, navigationId, options);
476
+ lantern = this.#createLanternContext(data, traceEvents, frameId, navigation, options);
478
477
  }
479
478
  catch (e) {
480
479
  // Handle Lantern errors gracefully
@@ -307,6 +307,10 @@ const UIStrings = {
307
307
  * @description Text in Timeline UIUtils of the Performance panel
308
308
  */
309
309
  largestContentfulPaint: 'Largest Contentful Paint',
310
+ /**
311
+ * @description Text in Timeline UIUtils of the Performance panel
312
+ */
313
+ softLargestContentfulPaint: 'Soft Largest Contentful Paint',
310
314
  /**
311
315
  * @description Text for timestamps of items
312
316
  */
@@ -684,6 +688,7 @@ export function maybeInitSylesMap() {
684
688
  ["firstPaint" /* Types.Events.Name.MARK_FIRST_PAINT */]: new TimelineRecordStyle(i18nString(UIStrings.firstPaint), defaultCategoryStyles.painting, true),
685
689
  ["firstContentfulPaint" /* Types.Events.Name.MARK_FCP */]: new TimelineRecordStyle(i18nString(UIStrings.firstContentfulPaint), defaultCategoryStyles.rendering, true),
686
690
  ["largestContentfulPaint::Candidate" /* Types.Events.Name.MARK_LCP_CANDIDATE */]: new TimelineRecordStyle(i18nString(UIStrings.largestContentfulPaint), defaultCategoryStyles.rendering, true),
691
+ ["largestContentfulPaint::CandidateForSoftNavigation" /* Types.Events.Name.MARK_LCP_CANDIDATE_FOR_SOFT_NAVIGATION */]: new TimelineRecordStyle(i18nString(UIStrings.softLargestContentfulPaint), defaultCategoryStyles.rendering, true),
687
692
  ["TimeStamp" /* Types.Events.Name.TIME_STAMP */]: new TimelineRecordStyle(i18nString(UIStrings.timestamp), defaultCategoryStyles.scripting),
688
693
  ["ConsoleTime" /* Types.Events.Name.CONSOLE_TIME */]: new TimelineRecordStyle(i18nString(UIStrings.consoleTime), defaultCategoryStyles.scripting),
689
694
  ["UserTiming" /* Types.Events.Name.USER_TIMING */]: new TimelineRecordStyle(i18nString(UIStrings.userTiming), defaultCategoryStyles.scripting),
@@ -795,13 +800,16 @@ export function markerDetailsForEvent(event) {
795
800
  color = 'var(--sys-color-green-bright)';
796
801
  title = "FCP" /* Handlers.ModelHandlers.PageLoadMetrics.MetricName.FCP */;
797
802
  }
798
- if (Types.Events.isLargestContentfulPaintCandidate(event)) {
803
+ if (Types.Events.isAnyLargestContentfulPaintCandidate(event)) {
799
804
  color = 'var(--sys-color-green)';
800
- title = "LCP" /* Handlers.ModelHandlers.PageLoadMetrics.MetricName.LCP */;
805
+ title = Types.Events.isSoftLargestContentfulPaintCandidate(event) ?
806
+ "LCP*" /* Handlers.ModelHandlers.PageLoadMetrics.MetricName.SOFT_LCP */ :
807
+ "LCP" /* Handlers.ModelHandlers.PageLoadMetrics.MetricName.LCP */;
801
808
  }
802
- if (Types.Events.isNavigationStart(event)) {
809
+ if (Types.Events.isNavigationStart(event) || Types.Events.isSoftNavigationStart(event)) {
803
810
  color = 'var(--color-text-primary)';
804
- title = "Nav" /* Handlers.ModelHandlers.PageLoadMetrics.MetricName.NAV */;
811
+ title = Types.Events.isSoftNavigationStart(event) ? "Nav*" /* Handlers.ModelHandlers.PageLoadMetrics.MetricName.SOFT_NAV */ :
812
+ "Nav" /* Handlers.ModelHandlers.PageLoadMetrics.MetricName.NAV */;
805
813
  }
806
814
  if (Types.Events.isMarkDOMContent(event)) {
807
815
  color = 'var(--color-text-disabled)';
@@ -0,0 +1,46 @@
1
+ import { get as getStackTrace } from './StackTraceForEvent.js';
2
+ /**
3
+ * There are bugs in the backend tracing that means that network requests are
4
+ * often incorrectly tied to an initiator. This function exists as a utility to
5
+ * look up an event's initiator regardless of the type of event, but also to
6
+ * provide a post-parsing fix for network initiators.
7
+ * The TL;DR is that images injected by a script will incorrectly have their
8
+ * initiator set to the root document. To fix this, we look at the stack trace
9
+ * when the request was sent, and use that.
10
+ */
11
+ export function getNetworkInitiator(data, event) {
12
+ const networkHandlerInitiator = data.NetworkRequests.incompleteInitiator.get(event);
13
+ if (networkHandlerInitiator?.args.data.mimeType === 'text/css') {
14
+ // The bugs in tracing & initiators apply mostly to scripts; we have not
15
+ // seen a case where the trace events identify a CSS stylesheet as the
16
+ // initiator that is incorrect. Therefore, if a stylesheet is identified as
17
+ // the initiator, we trust that it is accurate and can exit early.
18
+ return networkHandlerInitiator;
19
+ }
20
+ // For network requests, it is more reliable to calculate the initiator via
21
+ // the stack trace if we have one.
22
+ // We have to use the raw source event (`ResourceSendRequest`) as that is
23
+ // the event with the `sampleStackId` property which is required to
24
+ // calculate this stacktrace correctly.
25
+ const stack = getStackTrace(event.rawSourceEvent, data);
26
+ // If the resource was injected by a script, it will have a parent call
27
+ // frame that points to the script. Otherwise, there is no parent and
28
+ // therefore we fallthrough to looking at the initiator directly on the
29
+ // network request.
30
+ const initiatorCallFrame = stack?.parent?.callFrames.at(0);
31
+ if (!initiatorCallFrame) {
32
+ return networkHandlerInitiator;
33
+ }
34
+ // Find all the requests for the URL we are searching for. Most of the time
35
+ // there is only 1, but there can be multiple requests for the same URL. The
36
+ // filtering by the timestamp ensures that we can never pick an initiator
37
+ // that happened after the initiated event.
38
+ const matchingRequestIds = data.NetworkRequests.requestIdsByURL.get(initiatorCallFrame.url) ?? [];
39
+ const matchingRequests = matchingRequestIds.map(id => data.NetworkRequests.byId.get(id))
40
+ .filter(req => req !== undefined)
41
+ .filter(req => req.ts < event.ts);
42
+ // Now we have filtered and have a list of requests that are before the
43
+ // event, we take the last one - the one closest to the initiated event.
44
+ // In the case that there are >1 requests, this is an educated guess.
45
+ return matchingRequests.at(-1);
46
+ }
@@ -413,8 +413,9 @@ export class BottomUpRootNode extends Node {
413
413
  node.totalTime += totalTimeById.get(id) || 0;
414
414
  totalTimeById.delete(id);
415
415
  }
416
- // TODO: this may be wrong. See the skipped test in TraceTree.test.ts.
417
- if (firstNodeStack.length) {
416
+ // An item on this stack means that this current node has a caller. Therefore,
417
+ // in a bottom-up view it has children.
418
+ if (idStack.length > 0) {
418
419
  node.setHasChildren(true);
419
420
  }
420
421
  }
@@ -587,7 +588,7 @@ export function generateEventID(event) {
587
588
  SamplesIntegrator.nativeGroup(event.callFrame.functionName) :
588
589
  event.callFrame.functionName;
589
590
  const location = event.callFrame.scriptId || event.callFrame.url || '';
590
- return `f:${name}@${location}`;
591
+ return `f:${name}@${location}:${event.callFrame.lineNumber}:${event.callFrame.columnNumber}`;
591
592
  }
592
593
  if (Types.Events.isConsoleTimeStamp(event) && event.args.data) {
593
594
  return `${event.name}:${event.args.data.name}`;
@@ -2,6 +2,7 @@
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
  export * as FilmStrip from './FilmStrip.js';
5
+ export * as Initiators from './Initiators.js';
5
6
  export * as MainThreadActivity from './MainThreadActivity.js';
6
7
  export * as ScriptDuplication from './ScriptDuplication.js';
7
8
  export * as StackTraceForEvent from './StackTraceForEvent.js';
@@ -43,9 +43,9 @@ export async function finalize() {
43
43
  const { traceBounds, navigationsByNavigationId } = metaData();
44
44
  const metricScoresByFrameId = pageLoadMetricsData().metricScoresByFrameId;
45
45
  for (const [navigationId, navigation] of navigationsByNavigationId) {
46
- const lcpMetric = metricScoresByFrameId.get(navigation.args.frame)?.get(navigationId)?.get("LCP" /* MetricName.LCP */);
46
+ const lcpMetric = metricScoresByFrameId.get(navigation.args.frame)?.get(navigation)?.get("LCP" /* MetricName.LCP */);
47
47
  const lcpEvent = lcpMetric?.event;
48
- if (!lcpEvent || !Types.Events.isLargestContentfulPaintCandidate(lcpEvent)) {
48
+ if (!lcpEvent || !Types.Events.isAnyLargestContentfulPaintCandidate(lcpEvent)) {
49
49
  continue;
50
50
  }
51
51
  const nodeId = lcpEvent.args.data?.nodeId;
@@ -395,7 +395,7 @@ async function buildLayoutShiftsClusters() {
395
395
  // Update the cluster's worst layout shift.
396
396
  if (worstShiftEvent) {
397
397
  cluster.worstShiftEvent = worstShiftEvent;
398
- cluster.rawSourceEvent = worstShiftEvent;
398
+ cluster.rawSourceEvent = worstShiftEvent.rawSourceEvent;
399
399
  }
400
400
  // layout shifts are already sorted by time ascending.
401
401
  // Capture the time range of the cluster.
@@ -47,6 +47,7 @@ let traceBounds = makeNewTraceBounds();
47
47
  */
48
48
  let navigationsByFrameId = new Map();
49
49
  let navigationsByNavigationId = new Map();
50
+ let softNavigationsById = new Map();
50
51
  let finalDisplayUrlByNavigationId = new Map();
51
52
  let mainFrameNavigations = [];
52
53
  // Represents all the threads in the trace, organized by process. This is mostly for internal
@@ -76,6 +77,7 @@ const CHROME_WEB_TRACE_EVENTS = new Set([
76
77
  export function reset() {
77
78
  navigationsByFrameId = new Map();
78
79
  navigationsByNavigationId = new Map();
80
+ softNavigationsById = new Map();
79
81
  finalDisplayUrlByNavigationId = new Map();
80
82
  processNames = new Map();
81
83
  mainFrameNavigations = [];
@@ -276,6 +278,9 @@ export function handleEvent(event) {
276
278
  }
277
279
  return;
278
280
  }
281
+ if (Types.Events.isSoftNavigationStart(event)) {
282
+ softNavigationsById.set(event.args.context.performanceTimelineNavigationId, event);
283
+ }
279
284
  // Update `finalDisplayUrlByNavigationId` to reflect the latest redirect for each navigation.
280
285
  if (Types.Events.isResourceSendRequest(event)) {
281
286
  if (event.args.data.resourceType !== 'Document') {
@@ -393,6 +398,7 @@ export function data() {
393
398
  mainFrameURL,
394
399
  navigationsByFrameId,
395
400
  navigationsByNavigationId,
401
+ softNavigationsById,
396
402
  finalDisplayUrlByNavigationId,
397
403
  threadsInProcess,
398
404
  rendererProcessesByFrame: rendererProcessesByFrameId,
@@ -13,6 +13,10 @@ let linkPreconnectEvents = [];
13
13
  let requestMap = new Map();
14
14
  let requestsById = new Map();
15
15
  let requestsByTime = [];
16
+ /**
17
+ * URL => RequestId[]. There can be multiple requests for a single URL.
18
+ */
19
+ let requestIdsByURL = new Map();
16
20
  let networkRequestEventByInitiatorUrl = new Map();
17
21
  let eventToInitiatorMap = new Map();
18
22
  /**
@@ -61,6 +65,7 @@ export function reset() {
61
65
  networkRequestEventByInitiatorUrl = new Map();
62
66
  eventToInitiatorMap = new Map();
63
67
  webSocketData = new Map();
68
+ requestIdsByURL = new Map();
64
69
  entityMappings = {
65
70
  eventsByEntity: new Map(),
66
71
  entityByEvent: new Map(),
@@ -476,6 +481,9 @@ export async function finalize() {
476
481
  // the captured requests, so here we store all of them together.
477
482
  requestsByTime.push(networkEvent);
478
483
  requestsById.set(networkEvent.args.data.requestId, networkEvent);
484
+ const requestsForUrl = requestIdsByURL.get(networkEvent.args.data.url) ?? [];
485
+ requestsForUrl.push(networkEvent.args.data.requestId);
486
+ requestIdsByURL.set(networkEvent.args.data.url, requestsForUrl);
479
487
  // Update entity relationships for network events
480
488
  HandlerHelpers.addNetworkRequestToEntityMapping(networkEvent, entityMappings, request);
481
489
  // Establish initiator relationships
@@ -501,7 +509,8 @@ export function data() {
501
509
  return {
502
510
  byId: requestsById,
503
511
  byTime: requestsByTime,
504
- eventToInitiator: eventToInitiatorMap,
512
+ requestIdsByURL,
513
+ incompleteInitiator: eventToInitiatorMap,
505
514
  webSocket: [...webSocketData.values()],
506
515
  entityMappings: {
507
516
  entityByEvent: entityMappings.entityByEvent,