@paulirish/trace_engine 0.0.45 → 0.0.47

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 (170) hide show
  1. package/.tmp/tsbuildinfo/models/trace/extras/polyfills.d.ts +4 -0
  2. package/.tmp/tsbuildinfo/models/trace/extras/polyfills.d.ts.map +1 -0
  3. package/.tmp/tsbuildinfo/tsconfig.tsbuildinfo +1 -1
  4. package/core/platform/Constructor.d.ts +3 -0
  5. package/core/platform/Constructor.js +5 -0
  6. package/core/platform/Constructor.js.map +1 -0
  7. package/core/platform/MapUtilities.js +1 -0
  8. package/core/platform/MapUtilities.js.map +1 -1
  9. package/core/platform/TypescriptUtilities.d.ts +7 -0
  10. package/core/platform/TypescriptUtilities.js.map +1 -1
  11. package/core/platform/platform-tsconfig.json +1 -0
  12. package/core/platform/platform.d.ts +2 -1
  13. package/core/platform/platform.js +2 -1
  14. package/core/platform/platform.js.map +1 -1
  15. package/generated/protocol.d.ts +214 -4
  16. package/locales/af.json +360 -0
  17. package/locales/am.json +360 -0
  18. package/locales/ar.json +360 -0
  19. package/locales/as.json +360 -0
  20. package/locales/az.json +360 -0
  21. package/locales/be.json +360 -0
  22. package/locales/bg.json +360 -0
  23. package/locales/bn.json +360 -0
  24. package/locales/bs.json +360 -0
  25. package/locales/ca.json +360 -0
  26. package/locales/cs.json +360 -0
  27. package/locales/cy.json +360 -0
  28. package/locales/da.json +360 -0
  29. package/locales/de.json +360 -0
  30. package/locales/el.json +360 -0
  31. package/locales/en-GB.json +360 -0
  32. package/locales/en-US.json +370 -10
  33. package/locales/en-XL.json +370 -10
  34. package/locales/es-419.json +360 -0
  35. package/locales/es.json +360 -0
  36. package/locales/et.json +360 -0
  37. package/locales/eu.json +360 -0
  38. package/locales/fa.json +360 -0
  39. package/locales/fi.json +360 -0
  40. package/locales/fil.json +360 -0
  41. package/locales/fr-CA.json +360 -0
  42. package/locales/fr.json +360 -0
  43. package/locales/gl.json +360 -0
  44. package/locales/gu.json +360 -0
  45. package/locales/he.json +360 -0
  46. package/locales/hi.json +360 -0
  47. package/locales/hr.json +360 -0
  48. package/locales/hu.json +360 -0
  49. package/locales/hy.json +360 -0
  50. package/locales/id.json +360 -0
  51. package/locales/is.json +360 -0
  52. package/locales/it.json +360 -0
  53. package/locales/ja.json +360 -0
  54. package/locales/ka.json +360 -0
  55. package/locales/kk.json +360 -0
  56. package/locales/km.json +360 -0
  57. package/locales/kn.json +360 -0
  58. package/locales/ko.json +360 -0
  59. package/locales/ky.json +360 -0
  60. package/locales/lo.json +360 -0
  61. package/locales/lt.json +360 -0
  62. package/locales/lv.json +360 -0
  63. package/locales/mk.json +360 -0
  64. package/locales/ml.json +360 -0
  65. package/locales/mn.json +360 -0
  66. package/locales/mr.json +360 -0
  67. package/locales/ms.json +360 -0
  68. package/locales/my.json +360 -0
  69. package/locales/ne.json +360 -0
  70. package/locales/nl.json +360 -0
  71. package/locales/no.json +360 -0
  72. package/locales/or.json +360 -0
  73. package/locales/pa.json +360 -0
  74. package/locales/pl.json +360 -0
  75. package/locales/pt-PT.json +360 -0
  76. package/locales/pt.json +360 -0
  77. package/locales/ro.json +360 -0
  78. package/locales/ru.json +360 -0
  79. package/locales/si.json +360 -0
  80. package/locales/sk.json +360 -0
  81. package/locales/sl.json +360 -0
  82. package/locales/sq.json +360 -0
  83. package/locales/sr-Latn.json +360 -0
  84. package/locales/sr.json +360 -0
  85. package/locales/sv.json +360 -0
  86. package/locales/sw.json +360 -0
  87. package/locales/ta.json +360 -0
  88. package/locales/te.json +360 -0
  89. package/locales/th.json +360 -0
  90. package/locales/tr.json +360 -0
  91. package/locales/uk.json +360 -0
  92. package/locales/ur.json +360 -0
  93. package/locales/uz.json +360 -0
  94. package/locales/vi.json +360 -0
  95. package/locales/zh-HK.json +360 -0
  96. package/locales/zh-TW.json +360 -0
  97. package/locales/zh.json +360 -0
  98. package/locales/zu.json +360 -0
  99. package/models/trace/Processor.d.ts +4 -1
  100. package/models/trace/Processor.js +10 -16
  101. package/models/trace/Processor.js.map +1 -1
  102. package/models/trace/extras/ScriptDuplication.d.ts +1 -1
  103. package/models/trace/extras/ScriptDuplication.js +2 -2
  104. package/models/trace/extras/ScriptDuplication.js.map +1 -1
  105. package/models/trace/extras/extras.d.ts +3798 -0
  106. package/models/trace/extras/extras.js +3798 -0
  107. package/models/trace/handlers/LayoutShiftsHandler.d.ts +7 -1
  108. package/models/trace/handlers/LayoutShiftsHandler.js +16 -5
  109. package/models/trace/handlers/LayoutShiftsHandler.js.map +1 -1
  110. package/models/trace/handlers/PageLoadMetricsHandler.d.ts +5 -0
  111. package/models/trace/handlers/PageLoadMetricsHandler.js +3 -0
  112. package/models/trace/handlers/PageLoadMetricsHandler.js.map +1 -1
  113. package/models/trace/handlers/ScriptsHandler.d.ts +5 -1
  114. package/models/trace/handlers/ScriptsHandler.js +24 -11
  115. package/models/trace/handlers/ScriptsHandler.js.map +1 -1
  116. package/models/trace/handlers/types.d.ts +1 -1
  117. package/models/trace/handlers/types.js.map +1 -1
  118. package/models/trace/insights/CLSCulprits.d.ts +5 -5
  119. package/models/trace/insights/CLSCulprits.js +1 -3
  120. package/models/trace/insights/CLSCulprits.js.map +1 -1
  121. package/models/trace/insights/DOMSize.d.ts +3 -3
  122. package/models/trace/insights/DOMSize.js +2 -4
  123. package/models/trace/insights/DOMSize.js.map +1 -1
  124. package/models/trace/insights/DocumentLatency.d.ts +3 -3
  125. package/models/trace/insights/DocumentLatency.js +4 -6
  126. package/models/trace/insights/DocumentLatency.js.map +1 -1
  127. package/models/trace/insights/DuplicateJavaScript.d.ts +3 -3
  128. package/models/trace/insights/DuplicateJavaScript.js +6 -9
  129. package/models/trace/insights/DuplicateJavaScript.js.map +1 -1
  130. package/models/trace/insights/FontDisplay.d.ts +12 -9
  131. package/models/trace/insights/FontDisplay.js +16 -15
  132. package/models/trace/insights/FontDisplay.js.map +1 -1
  133. package/models/trace/insights/ForcedReflow.d.ts +3 -3
  134. package/models/trace/insights/ForcedReflow.js +2 -4
  135. package/models/trace/insights/ForcedReflow.js.map +1 -1
  136. package/models/trace/insights/ImageDelivery.d.ts +7 -6
  137. package/models/trace/insights/ImageDelivery.js +27 -20
  138. package/models/trace/insights/ImageDelivery.js.map +1 -1
  139. package/models/trace/insights/InteractionToNextPaint.d.ts +3 -3
  140. package/models/trace/insights/InteractionToNextPaint.js +4 -4
  141. package/models/trace/insights/InteractionToNextPaint.js.map +1 -1
  142. package/models/trace/insights/LCPDiscovery.d.ts +4 -3
  143. package/models/trace/insights/LCPDiscovery.js +9 -6
  144. package/models/trace/insights/LCPDiscovery.js.map +1 -1
  145. package/models/trace/insights/LCPPhases.d.ts +4 -4
  146. package/models/trace/insights/LCPPhases.js +7 -6
  147. package/models/trace/insights/LCPPhases.js.map +1 -1
  148. package/models/trace/insights/NetworkDependencyTree.d.ts +9 -3
  149. package/models/trace/insights/NetworkDependencyTree.js +41 -10
  150. package/models/trace/insights/NetworkDependencyTree.js.map +1 -1
  151. package/models/trace/insights/RenderBlocking.d.ts +4 -3
  152. package/models/trace/insights/RenderBlocking.js +6 -3
  153. package/models/trace/insights/RenderBlocking.js.map +1 -1
  154. package/models/trace/insights/SlowCSSSelector.d.ts +3 -3
  155. package/models/trace/insights/SlowCSSSelector.js +1 -3
  156. package/models/trace/insights/SlowCSSSelector.js.map +1 -1
  157. package/models/trace/insights/ThirdParties.d.ts +3 -3
  158. package/models/trace/insights/ThirdParties.js +2 -4
  159. package/models/trace/insights/ThirdParties.js.map +1 -1
  160. package/models/trace/insights/Viewport.d.ts +3 -3
  161. package/models/trace/insights/Viewport.js +14 -9
  162. package/models/trace/insights/Viewport.js.map +1 -1
  163. package/models/trace/insights/types.d.ts +5 -9
  164. package/models/trace/insights/types.js.map +1 -1
  165. package/models/trace/types/File.d.ts +8 -0
  166. package/models/trace/types/File.js.map +1 -1
  167. package/models/trace/types/TraceEvents.d.ts +16 -1
  168. package/models/trace/types/TraceEvents.js +3 -0
  169. package/models/trace/types/TraceEvents.js.map +1 -1
  170. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import * as Platform from '../../../core/platform/platform.js';
2
+ import type * as Handlers from '../handlers/handlers.js';
2
3
  import * as Types from '../types/types.js';
3
- import { type InsightModel, type InsightSetContext, type RequiredData } from './types.js';
4
+ import { type InsightModel, type InsightSetContext } from './types.js';
4
5
  export declare const UIStrings: {
5
6
  /** Title of an insight that provides details about the fonts used on the page, and the value of their `font-display` properties. */
6
7
  readonly title: "Font display";
@@ -13,13 +14,15 @@ export declare const UIStrings: {
13
14
  /** Column for the amount of time wasted. */
14
15
  readonly wastedTimeColumn: "Wasted time";
15
16
  };
16
- export declare const i18nString: (id: string, values?: Record<string, string> | undefined) => Platform.UIString.LocalizedString;
17
- export declare function deps(): ['Meta', 'NetworkRequests', 'LayoutShifts'];
17
+ export declare const i18nString: (id: string, values?: Record<string, string> | undefined) => {i18nId: string, values: Record<string, string|number>, formattedDefault: string};
18
+ interface RemoteFont {
19
+ name?: string;
20
+ request: Types.Events.SyntheticNetworkRequest;
21
+ display: string;
22
+ wastedTime: Types.Timing.Milli;
23
+ }
18
24
  export type FontDisplayInsightModel = InsightModel<typeof UIStrings, {
19
- fonts: Array<{
20
- request: Types.Events.SyntheticNetworkRequest;
21
- display: string;
22
- wastedTime: Types.Timing.Milli;
23
- }>;
25
+ fonts: RemoteFont[];
24
26
  }>;
25
- export declare function generateInsight(parsedTrace: RequiredData<typeof deps>, context: InsightSetContext): FontDisplayInsightModel;
27
+ export declare function generateInsight(parsedTrace: Handlers.Types.ParsedTrace, context: InsightSetContext): FontDisplayInsightModel;
28
+ export {};
@@ -5,7 +5,7 @@
5
5
  import * as Platform from '../../../core/platform/platform.js';
6
6
  import * as Helpers from '../helpers/helpers.js';
7
7
  import * as Types from '../types/types.js';
8
- import { InsightCategory } from './types.js';
8
+ import { InsightCategory, } from './types.js';
9
9
  export const UIStrings = {
10
10
  /** Title of an insight that provides details about the fonts used on the page, and the value of their `font-display` properties. */
11
11
  title: 'Font display',
@@ -20,9 +20,6 @@ export const UIStrings = {
20
20
  };
21
21
  // const str_ = i18n.i18n.registerUIStrings('models/trace/insights/FontDisplay.ts', UIStrings);
22
22
  export const i18nString = (i18nId, values) => ({i18nId, values}); // i18n.i18n.getLocalizedString.bind(undefined, str_);
23
- export function deps() {
24
- return ['Meta', 'NetworkRequests', 'LayoutShifts'];
25
- }
26
23
  function finalize(partialModel) {
27
24
  return {
28
25
  insightKey: "FontDisplay" /* InsightKeys.FONT_DISPLAY */,
@@ -36,7 +33,8 @@ function finalize(partialModel) {
36
33
  }
37
34
  export function generateInsight(parsedTrace, context) {
38
35
  const fonts = [];
39
- for (const event of parsedTrace.LayoutShifts.beginRemoteFontLoadEvents) {
36
+ for (const remoteFont of parsedTrace.LayoutShifts.remoteFonts) {
37
+ const event = remoteFont.beginRemoteFontLoadEvent;
40
38
  if (!Helpers.Timing.eventIsInBounds(event, context.bounds)) {
41
39
  continue;
42
40
  }
@@ -45,25 +43,28 @@ export function generateInsight(parsedTrace, context) {
45
43
  if (!request) {
46
44
  continue;
47
45
  }
48
- const display = event.args.display;
49
- let wastedTime = Types.Timing.Milli(0);
50
- if (/^(block|fallback|auto)$/.test(display)) {
51
- const wastedTimeMicro = Types.Timing.Micro(request.args.data.syntheticData.finishTime - request.args.data.syntheticData.sendStartTime);
52
- // TODO(crbug.com/352244504): should really end at the time of the next Commit trace event.
53
- wastedTime =
54
- Platform.NumberUtilities.floor(Helpers.Timing.microToMilli(wastedTimeMicro), 1 / 5);
55
- // All browsers wait for no more than 3s.
56
- wastedTime = Math.min(wastedTime, 3000);
46
+ if (!/^(block|fallback|auto)$/.test(remoteFont.display)) {
47
+ continue;
48
+ }
49
+ const wastedTimeMicro = Types.Timing.Micro(request.args.data.syntheticData.finishTime - request.args.data.syntheticData.sendStartTime);
50
+ // TODO(crbug.com/352244504): should really end at the time of the next Commit trace event.
51
+ let wastedTime = Platform.NumberUtilities.floor(Helpers.Timing.microToMilli(wastedTimeMicro), 1 / 5);
52
+ if (wastedTime === 0) {
53
+ continue;
57
54
  }
55
+ // All browsers wait for no more than 3s.
56
+ wastedTime = Math.min(wastedTime, 3000);
58
57
  fonts.push({
58
+ name: remoteFont.name,
59
59
  request,
60
- display,
60
+ display: remoteFont.display,
61
61
  wastedTime,
62
62
  });
63
63
  }
64
64
  fonts.sort((a, b) => b.wastedTime - a.wastedTime);
65
65
  const savings = Math.max(...fonts.map(f => f.wastedTime));
66
66
  return finalize({
67
+ frameId: context.frameId,
67
68
  relatedEvents: fonts.map(f => f.request),
68
69
  fonts,
69
70
  metricSavings: { FCP: savings },
@@ -1 +1 @@
1
- {"version":3,"file":"FontDisplay.js","sourceRoot":"","sources":["../../../../../../../front_end/models/trace/insights/FontDisplay.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,yEAAyE;AACzE,6BAA6B;AAE7B,OAAO,KAAK,IAAI,MAAM,4BAA4B,CAAC;AACnD,OAAO,KAAK,QAAQ,MAAM,oCAAoC,CAAC;AAC/D,OAAO,KAAK,OAAO,MAAM,uBAAuB,CAAC;AACjD,OAAO,KAAK,KAAK,MAAM,mBAAmB,CAAC;AAE3C,OAAO,EACL,eAAe,EAMhB,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,oIAAoI;IACpI,KAAK,EAAE,cAAc;IACrB;;OAEG;IACH,WAAW,EACP,6RAA6R;IACjS,2DAA2D;IAC3D,UAAU,EAAE,MAAM;IAClB,4CAA4C;IAC5C,gBAAgB,EAAE,aAAa;CACvB,CAAC;AAEX,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,sCAAsC,EAAE,SAAS,CAAC,CAAC;AAC5F,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;AAE7E,MAAM,UAAU,IAAI;IAClB,OAAO,CAAC,MAAM,EAAE,iBAAiB,EAAE,cAAc,CAAC,CAAC;AACrD,CAAC;AAUD,SAAS,QAAQ,CAAC,YAA0D;IAC1E,OAAO;QACL,UAAU,8CAA0B;QACpC,OAAO,EAAE,SAAS;QAClB,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC;QAClC,WAAW,EAAE,UAAU,CAAC,SAAS,CAAC,WAAW,CAAC;QAC9C,QAAQ,EAAE,eAAe,CAAC,GAAG;QAC7B,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;QAC7E,GAAG,YAAY;KAChB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe,CAC3B,WAAsC,EAAE,OAA0B;IACpE,MAAM,KAAK,GAAG,EAAE,CAAC;IACjB,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,YAAY,CAAC,yBAAyB,EAAE,CAAC;QACvE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3D,SAAS;QACX,CAAC;QAED,MAAM,SAAS,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QAClD,MAAM,OAAO,GAAG,WAAW,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAChE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC;QACnC,IAAI,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAEvC,IAAI,yBAAyB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5C,MAAM,eAAe,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CACtC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;YAChG,2FAA2F;YAC3F,UAAU;gBACN,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,eAAe,CAAC,EAAE,CAAC,GAAG,CAAC,CAAuB,CAAC;YAC9G,yCAAyC;YACzC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAuB,CAAC;QAChE,CAAC;QAED,KAAK,CAAC,IAAI,CAAC;YACT,OAAO;YACP,OAAO;YACP,UAAU;SACX,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC;IAElD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAuB,CAAC;IAEhF,OAAO,QAAQ,CAAC;QACd,aAAa,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;QACxC,KAAK;QACL,aAAa,EAAE,EAAC,GAAG,EAAE,OAAO,EAAC;KAC9B,CAAC,CAAC;AACL,CAAC","sourcesContent":["// Copyright 2024 The Chromium Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\nimport * as i18n from '../../../core/i18n/i18n.js';\nimport * as Platform from '../../../core/platform/platform.js';\nimport * as Helpers from '../helpers/helpers.js';\nimport * as Types from '../types/types.js';\n\nimport {\n InsightCategory,\n InsightKeys,\n type InsightModel,\n type InsightSetContext,\n type PartialInsightModel,\n type RequiredData\n} from './types.js';\n\nexport const UIStrings = {\n /** Title of an insight that provides details about the fonts used on the page, and the value of their `font-display` properties. */\n title: 'Font display',\n /**\n * @description Text to tell the user about the font-display CSS feature to help improve a the UX of a page.\n */\n description:\n 'Consider setting [`font-display`](https://developer.chrome.com/blog/font-display) to `swap` or `optional` to ensure text is consistently visible. `swap` can be further optimized to mitigate layout shifts with [font metric overrides](https://developer.chrome.com/blog/font-fallbacks).',\n /** Column for a font loaded by the page to render text. */\n fontColumn: 'Font',\n /** Column for the amount of time wasted. */\n wastedTimeColumn: 'Wasted time',\n} as const;\n\nconst str_ = i18n.i18n.registerUIStrings('models/trace/insights/FontDisplay.ts', UIStrings);\nexport const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);\n\nexport function deps(): ['Meta', 'NetworkRequests', 'LayoutShifts'] {\n return ['Meta', 'NetworkRequests', 'LayoutShifts'];\n}\n\nexport type FontDisplayInsightModel = InsightModel<typeof UIStrings, {\n fonts: Array<{\n request: Types.Events.SyntheticNetworkRequest,\n display: string,\n wastedTime: Types.Timing.Milli,\n }>,\n}>;\n\nfunction finalize(partialModel: PartialInsightModel<FontDisplayInsightModel>): FontDisplayInsightModel {\n return {\n insightKey: InsightKeys.FONT_DISPLAY,\n strings: UIStrings,\n title: i18nString(UIStrings.title),\n description: i18nString(UIStrings.description),\n category: InsightCategory.INP,\n state: partialModel.fonts.find(font => font.wastedTime > 0) ? 'fail' : 'pass',\n ...partialModel,\n };\n}\n\nexport function generateInsight(\n parsedTrace: RequiredData<typeof deps>, context: InsightSetContext): FontDisplayInsightModel {\n const fonts = [];\n for (const event of parsedTrace.LayoutShifts.beginRemoteFontLoadEvents) {\n if (!Helpers.Timing.eventIsInBounds(event, context.bounds)) {\n continue;\n }\n\n const requestId = `${event.pid}.${event.args.id}`;\n const request = parsedTrace.NetworkRequests.byId.get(requestId);\n if (!request) {\n continue;\n }\n\n const display = event.args.display;\n let wastedTime = Types.Timing.Milli(0);\n\n if (/^(block|fallback|auto)$/.test(display)) {\n const wastedTimeMicro = Types.Timing.Micro(\n request.args.data.syntheticData.finishTime - request.args.data.syntheticData.sendStartTime);\n // TODO(crbug.com/352244504): should really end at the time of the next Commit trace event.\n wastedTime =\n Platform.NumberUtilities.floor(Helpers.Timing.microToMilli(wastedTimeMicro), 1 / 5) as Types.Timing.Milli;\n // All browsers wait for no more than 3s.\n wastedTime = Math.min(wastedTime, 3000) as Types.Timing.Milli;\n }\n\n fonts.push({\n request,\n display,\n wastedTime,\n });\n }\n\n fonts.sort((a, b) => b.wastedTime - a.wastedTime);\n\n const savings = Math.max(...fonts.map(f => f.wastedTime)) as Types.Timing.Milli;\n\n return finalize({\n relatedEvents: fonts.map(f => f.request),\n fonts,\n metricSavings: {FCP: savings},\n });\n}\n"]}
1
+ {"version":3,"file":"FontDisplay.js","sourceRoot":"","sources":["../../../../../../../front_end/models/trace/insights/FontDisplay.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 The Chromium Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\nimport * as i18n from '../../../core/i18n/i18n.js';\nimport * as Platform from '../../../core/platform/platform.js';\nimport type * as Handlers from '../handlers/handlers.js';\nimport * as Helpers from '../helpers/helpers.js';\nimport * as Types from '../types/types.js';\n\nimport {\n InsightCategory,\n InsightKeys,\n type InsightModel,\n type InsightSetContext,\n type PartialInsightModel,\n} from './types.js';\n\nexport const UIStrings = {\n /** Title of an insight that provides details about the fonts used on the page, and the value of their `font-display` properties. */\n title: 'Font display',\n /**\n * @description Text to tell the user about the font-display CSS feature to help improve a the UX of a page.\n */\n description:\n 'Consider setting [`font-display`](https://developer.chrome.com/blog/font-display) to `swap` or `optional` to ensure text is consistently visible. `swap` can be further optimized to mitigate layout shifts with [font metric overrides](https://developer.chrome.com/blog/font-fallbacks).',\n /** Column for a font loaded by the page to render text. */\n fontColumn: 'Font',\n /** Column for the amount of time wasted. */\n wastedTimeColumn: 'Wasted time',\n} as const;\n\nconst str_ = i18n.i18n.registerUIStrings('models/trace/insights/FontDisplay.ts', UIStrings);\nexport const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);\n\ninterface RemoteFont {\n name?: string;\n request: Types.Events.SyntheticNetworkRequest;\n display: string;\n wastedTime: Types.Timing.Milli;\n}\n\nexport type FontDisplayInsightModel = InsightModel<typeof UIStrings, {\n fonts: RemoteFont[],\n}>;\n\nfunction finalize(partialModel: PartialInsightModel<FontDisplayInsightModel>): FontDisplayInsightModel {\n return {\n insightKey: InsightKeys.FONT_DISPLAY,\n strings: UIStrings,\n title: i18nString(UIStrings.title),\n description: i18nString(UIStrings.description),\n category: InsightCategory.INP,\n state: partialModel.fonts.find(font => font.wastedTime > 0) ? 'fail' : 'pass',\n ...partialModel,\n };\n}\n\nexport function generateInsight(\n parsedTrace: Handlers.Types.ParsedTrace, context: InsightSetContext): FontDisplayInsightModel {\n const fonts: RemoteFont[] = [];\n for (const remoteFont of parsedTrace.LayoutShifts.remoteFonts) {\n const event = remoteFont.beginRemoteFontLoadEvent;\n if (!Helpers.Timing.eventIsInBounds(event, context.bounds)) {\n continue;\n }\n\n const requestId = `${event.pid}.${event.args.id}`;\n const request = parsedTrace.NetworkRequests.byId.get(requestId);\n if (!request) {\n continue;\n }\n\n if (!/^(block|fallback|auto)$/.test(remoteFont.display)) {\n continue;\n }\n\n const wastedTimeMicro =\n Types.Timing.Micro(request.args.data.syntheticData.finishTime - request.args.data.syntheticData.sendStartTime);\n // TODO(crbug.com/352244504): should really end at the time of the next Commit trace event.\n let wastedTime =\n Platform.NumberUtilities.floor(Helpers.Timing.microToMilli(wastedTimeMicro), 1 / 5) as Types.Timing.Milli;\n if (wastedTime === 0) {\n continue;\n }\n\n // All browsers wait for no more than 3s.\n wastedTime = Math.min(wastedTime, 3000) as Types.Timing.Milli;\n\n fonts.push({\n name: remoteFont.name,\n request,\n display: remoteFont.display,\n wastedTime,\n });\n }\n\n fonts.sort((a, b) => b.wastedTime - a.wastedTime);\n\n const savings = Math.max(...fonts.map(f => f.wastedTime)) as Types.Timing.Milli;\n\n return finalize({\n frameId: context.frameId,\n relatedEvents: fonts.map(f => f.request),\n fonts,\n metricSavings: {FCP: savings},\n });\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { type BottomUpCallStack, type ForcedReflowAggregatedData, type InsightModel, type RequiredData } from './types.js';
2
- export declare function deps(): ['Warnings', 'Renderer'];
1
+ import type * as Handlers from '../handlers/handlers.js';
2
+ import { type BottomUpCallStack, type ForcedReflowAggregatedData, type InsightModel, type InsightSetContext } from './types.js';
3
3
  export declare const UIStrings: {
4
4
  /**
5
5
  *@description Title of an insight that provides details about Forced reflow.
@@ -27,4 +27,4 @@ export type ForcedReflowInsightModel = InsightModel<typeof UIStrings, {
27
27
  topLevelFunctionCallData: ForcedReflowAggregatedData | undefined;
28
28
  aggregatedBottomUpData: BottomUpCallStack[];
29
29
  }>;
30
- export declare function generateInsight(traceParsedData: RequiredData<typeof deps>): ForcedReflowInsightModel;
30
+ export declare function generateInsight(traceParsedData: Handlers.Types.ParsedTrace, context: InsightSetContext): ForcedReflowInsightModel;
@@ -5,9 +5,6 @@
5
5
  import * as Helpers from '../helpers/helpers.js';
6
6
  import * as Types from '../types/types.js';
7
7
  import { InsightCategory, } from './types.js';
8
- export function deps() {
9
- return ['Warnings', 'Renderer'];
10
- }
11
8
  export const UIStrings = {
12
9
  /**
13
10
  *@description Title of an insight that provides details about Forced reflow.
@@ -165,7 +162,7 @@ function finalize(partialModel) {
165
162
  ...partialModel,
166
163
  };
167
164
  }
168
- export function generateInsight(traceParsedData) {
165
+ export function generateInsight(traceParsedData, context) {
169
166
  const warningsData = traceParsedData.Warnings;
170
167
  const entryToNodeMap = traceParsedData.Renderer.entryToNode;
171
168
  if (!warningsData) {
@@ -176,6 +173,7 @@ export function generateInsight(traceParsedData) {
176
173
  }
177
174
  const [topLevelFunctionCallData, aggregatedBottomUpData] = aggregateForcedReflow(warningsData.perWarning, entryToNodeMap);
178
175
  return finalize({
176
+ frameId: context.frameId,
179
177
  relatedEvents: topLevelFunctionCallData?.topLevelFunctionCallEvents,
180
178
  topLevelFunctionCallData,
181
179
  aggregatedBottomUpData,
@@ -1 +1 @@
1
- {"version":3,"file":"ForcedReflow.js","sourceRoot":"","sources":["../../../../../../../front_end/models/trace/insights/ForcedReflow.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,yEAAyE;AACzE,6BAA6B;AAE7B,OAAO,KAAK,IAAI,MAAM,4BAA4B,CAAC;AAGnD,OAAO,KAAK,OAAO,MAAM,uBAAuB,CAAC;AACjD,OAAO,KAAK,KAAK,MAAM,mBAAmB,CAAC;AAE3C,OAAO,EAGL,eAAe,GAKhB,MAAM,YAAY,CAAC;AAEpB,MAAM,UAAU,IAAI;IAClB,OAAO,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB;;OAEG;IACH,KAAK,EAAE,eAAe;IACtB;;OAEG;IACH,WAAW,EACP,8VAA8V;IAClW;;OAEG;IACH,iBAAiB,EAAE,aAAa;IAChC;;OAEG;IACH,4BAA4B,EAAE,mBAAmB;IACjD;;OAEG;IACH,eAAe,EAAE,mBAAmB;CAC5B,CAAC;AAEX,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,uCAAuC,EAAE,SAAS,CAAC,CAAC;AAC7F,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;AAO7E,SAAS,qBAAqB,CAC1B,IAAwC,EACxC,cAA2E;IAE7E,MAAM,sBAAsB,GAAG,IAAI,GAAG,EAAsC,CAAC;IAC7E,MAAM,eAAe,GAAG,IAAI,GAAG,EAA6B,CAAC;IAC7D,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACrD,IAAI,CAAC,kBAAkB,IAAI,kBAAkB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3D,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACzB,CAAC;IAED,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE;QAC7B,mDAAmD;QACnD,MAAM,SAAS,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAExC,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO;QACT,CAAC;QACD,2BAA2B;QAC3B,MAAM,YAAY,GAA6D,EAAE,CAAC;QAClF,IAAI,WAAW,GAAG,SAAS,CAAC;QAC5B,IAAI,YAAY,CAAC;QACjB,MAAM,UAAU,GAAiC,EAAE,CAAC;QAEpD,wEAAwE;QACxE,OAAO,WAAW,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvC,MAAM,SAAS,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAC1C,IAAI,CAAC,YAAY,EAAE,CAAC;gBAClB,YAAY,GAAG,SAAS,CAAC;YAC3B,CAAC;YACD,MAAM,SAAS,GAAG,SAAS,CAAC,KAAK,CAAC;YAClC,IAAI,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC1C,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;YACvC,CAAC;YACD,WAAW,GAAG,SAAS,CAAC;QAC1B,CAAC;QAED,kFAAkF;QAClF,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxD,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;YACnC,IAAI,SAAS,EAAE,CAAC;gBACd,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;QAED,IAAI,IAAI,GAAG,SAAS,CAAC,MAAM,CAAC;QAC5B,IAAI,oBAAoB,CAAC;QACzB,IAAI,yBAAuD,CAAC;QAC5D,OAAO,IAAI,EAAE,CAAC;YACZ,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC;YAC7B,IAAI,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC1C,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC5B,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;gBACzC,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,4CAA4C;gBAC5C,IAAI,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI;oBAC7D,KAAK,CAAC,MAAM,CAAC,iBAAiB,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBACxD,oBAAoB,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;oBAC3C,yBAAyB,GAAG,SAAS,CAAC;oBACtC,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;wBAC9B,YAAY,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;oBAC1C,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,+DAA+D;oBAC/D,wEAAwE;oBACxE,MAAM,YAAY,GAAG,YAAY,EAAE,KAAK,CAAC;oBACzC,IAAI,YAAY,IAAI,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,EAAE,CAAC;wBAC7D,oBAAoB,GAAG,YAAY,CAAC,SAAS,CAAC;wBAC9C,yBAAyB,GAAG,YAAY,EAAE,KAAK,CAAC;oBAClD,CAAC;gBACH,CAAC;gBACD,MAAM;YACR,CAAC;YACD,YAAY,GAAG,IAAI,CAAC;YACpB,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC;QACrB,CAAC;QAED,IAAI,CAAC,oBAAoB,IAAI,CAAC,yBAAyB,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrF,OAAO;QACT,CAAC;QACD,MAAM,cAAc,GAChB,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,GAAG,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,GAAG,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,GAAG,GAAG,CAAC;QAE3G,MAAM,IAAI,GAAG,eAAe,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI;YAClD,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC;YAC7B,SAAS,EAAE,CAAC;YACZ,aAAa,EAAE,EAAE;SAClB,CAAC;QACF,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;QAC/B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3B,eAAe,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;QAE1C,MAAM,gBAAgB,GAClB,oBAAoB,CAAC,QAAQ,GAAG,GAAG,GAAG,oBAAoB,CAAC,UAAU,GAAG,GAAG,GAAG,oBAAoB,CAAC,YAAY,CAAC;QACpH,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAClD,sBAAsB,CAAC,GAAG,CAAC,gBAAgB,EAAE;gBAC3C,oBAAoB;gBACpB,eAAe,EAAE,CAAC;gBAClB,YAAY,EAAE,IAAI,GAAG,EAAU;gBAC/B,0BAA0B,EAAE,EAAE;aAC/B,CAAC,CAAC;QACL,CAAC;QACD,MAAM,cAAc,GAAG,sBAAsB,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QACpE,IAAI,cAAc,EAAE,CAAC;YACnB,cAAc,CAAC,eAAe,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;YAC/C,cAAc,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;YAChD,cAAc,CAAC,0BAA0B,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,sBAAsB,GAAG,EAAE,CAAC;IAChC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,sBAAsB,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QAC5C,IAAI,KAAK,CAAC,eAAe,GAAG,OAAO,EAAE,CAAC;YACpC,OAAO,GAAG,KAAK,CAAC,eAAe,CAAC;YAChC,sBAAsB,GAAG,GAAG,CAAC;QAC/B,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,sBAAsB,GAAwB,EAAE,CAAC;IACvD,MAAM,wBAAwB,GAAG,sBAAsB,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IACpF,MAAM,OAAO,GAAG,sBAAsB,CAAC,GAAG,CAAC,sBAAsB,CAAC,EAAE,YAAY,CAAC;IACjF,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;YACtB,MAAM,aAAa,GAAG,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACjD,IAAI,aAAa,IAAI,aAAa,CAAC,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClG,sBAAsB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YAC7C,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,CAAC,wBAAwB,EAAE,sBAAsB,CAAC,CAAC;AAC5D,CAAC;AAED,SAAS,QAAQ,CAAC,YAA2D;IAC3E,OAAO;QACL,UAAU,gDAA2B;QACrC,OAAO,EAAE,SAAS;QAClB,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC;QAClC,WAAW,EAAE,UAAU,CAAC,SAAS,CAAC,WAAW,CAAC;QAC9C,QAAQ,EAAE,eAAe,CAAC,GAAG;QAC7B,KAAK,EAAE,YAAY,CAAC,wBAAwB,KAAK,SAAS,IAAI,YAAY,CAAC,sBAAsB,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC;YAC5G,MAAM,CAAC,CAAC;YACR,MAAM;QACV,GAAG,YAAY;KAChB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,eAA0C;IACxE,MAAM,YAAY,GAAG,eAAe,CAAC,QAAQ,CAAC;IAC9C,MAAM,cAAc,GAAG,eAAe,CAAC,QAAQ,CAAC,WAAW,CAAC;IAE5D,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;IACtC,CAAC;IAED,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,CAAC,wBAAwB,EAAE,sBAAsB,CAAC,GACpD,qBAAqB,CAAC,YAAY,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;IAEnE,OAAO,QAAQ,CAAC;QACd,aAAa,EAAE,wBAAwB,EAAE,0BAA0B;QACnE,wBAAwB;QACxB,sBAAsB;KACvB,CAAC,CAAC;AACL,CAAC","sourcesContent":["// Copyright 2024 The Chromium Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\nimport * as i18n from '../../../core/i18n/i18n.js';\nimport type * as Protocol from '../../../generated/protocol.js';\nimport type {Warning} from '../handlers/WarningsHandler.js';\nimport * as Helpers from '../helpers/helpers.js';\nimport * as Types from '../types/types.js';\n\nimport {\n type BottomUpCallStack,\n type ForcedReflowAggregatedData,\n InsightCategory,\n InsightKeys,\n type InsightModel,\n type PartialInsightModel,\n type RequiredData,\n} from './types.js';\n\nexport function deps(): ['Warnings', 'Renderer'] {\n return ['Warnings', 'Renderer'];\n}\n\nexport const UIStrings = {\n /**\n *@description Title of an insight that provides details about Forced reflow.\n */\n title: 'Forced reflow',\n /**\n * @description Text to describe the forced reflow.\n */\n description:\n 'Many APIs, typically reading layout geometry, force the rendering engine to pause script execution in order to calculate the style and layout. Learn more about [forced reflow](https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing#avoid-forced-synchronous-layouts) and its mitigations.',\n /**\n *@description Title of a list to provide related stack trace data\n */\n relatedStackTrace: 'Stack trace',\n /**\n *@description Text to describe the top time-consuming function call\n */\n topTimeConsumingFunctionCall: 'Top function call',\n /**\n * @description Text to describe the total reflow time\n */\n totalReflowTime: 'Total reflow time',\n} as const;\n\nconst str_ = i18n.i18n.registerUIStrings('models/trace/insights/ForcedReflow.ts', UIStrings);\nexport const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);\n\nexport type ForcedReflowInsightModel = InsightModel<typeof UIStrings, {\n topLevelFunctionCallData: ForcedReflowAggregatedData | undefined,\n aggregatedBottomUpData: BottomUpCallStack[],\n}>;\n\nfunction aggregateForcedReflow(\n data: Map<Warning, Types.Events.Event[]>,\n entryToNodeMap: Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>):\n [ForcedReflowAggregatedData|undefined, BottomUpCallStack[]] {\n const dataByTopLevelFunction = new Map<string, ForcedReflowAggregatedData>();\n const bottomUpDataMap = new Map<string, BottomUpCallStack>();\n const forcedReflowEvents = data.get('FORCED_REFLOW');\n if (!forcedReflowEvents || forcedReflowEvents.length === 0) {\n return [undefined, []];\n }\n\n forcedReflowEvents.forEach(e => {\n // Gather the stack traces by searching in the tree\n const traceNode = entryToNodeMap.get(e);\n\n if (!traceNode) {\n return;\n }\n // Compute call stack fully\n const bottomUpData: Array<Types.Events.CallFrame|Protocol.Runtime.CallFrame> = [];\n let currentNode = traceNode;\n let previousNode;\n const childStack: Protocol.Runtime.CallFrame[] = [];\n\n // Some profileCalls maybe constructed as its children in hierarchy tree\n while (currentNode.children.length > 0) {\n const childNode = currentNode.children[0];\n if (!previousNode) {\n previousNode = childNode;\n }\n const eventData = childNode.entry;\n if (Types.Events.isProfileCall(eventData)) {\n childStack.push(eventData.callFrame);\n }\n currentNode = childNode;\n }\n\n // In order to avoid too much information, we only contain 2 levels bottomUp data,\n while (childStack.length > 0 && bottomUpData.length < 2) {\n const traceData = childStack.pop();\n if (traceData) {\n bottomUpData.push(traceData);\n }\n }\n\n let node = traceNode.parent;\n let topLevelFunctionCall;\n let topLevelFunctionCallEvent: Types.Events.Event|undefined;\n while (node) {\n const eventData = node.entry;\n if (Types.Events.isProfileCall(eventData)) {\n if (bottomUpData.length < 2) {\n bottomUpData.push(eventData.callFrame);\n }\n } else {\n // We have finished searching bottom up data\n if (Types.Events.isFunctionCall(eventData) && eventData.args.data &&\n Types.Events.objectIsCallFrame(eventData.args.data)) {\n topLevelFunctionCall = eventData.args.data;\n topLevelFunctionCallEvent = eventData;\n if (bottomUpData.length === 0) {\n bottomUpData.push(topLevelFunctionCall);\n }\n } else {\n // Sometimes the top level task can be other JSInvocation event\n // then we use the top level profile call as topLevelFunctionCall's data\n const previousData = previousNode?.entry;\n if (previousData && Types.Events.isProfileCall(previousData)) {\n topLevelFunctionCall = previousData.callFrame;\n topLevelFunctionCallEvent = previousNode?.entry;\n }\n }\n break;\n }\n previousNode = node;\n node = node.parent;\n }\n\n if (!topLevelFunctionCall || !topLevelFunctionCallEvent || bottomUpData.length === 0) {\n return;\n }\n const bottomUpDataId =\n bottomUpData[0].scriptId + ':' + bottomUpData[0].lineNumber + ':' + bottomUpData[0].columnNumber + ':';\n\n const data = bottomUpDataMap.get(bottomUpDataId) ?? {\n bottomUpData: bottomUpData[0],\n totalTime: 0,\n relatedEvents: [],\n };\n data.totalTime += (e.dur ?? 0);\n data.relatedEvents.push(e);\n bottomUpDataMap.set(bottomUpDataId, data);\n\n const aggregatedDataId =\n topLevelFunctionCall.scriptId + ':' + topLevelFunctionCall.lineNumber + ':' + topLevelFunctionCall.columnNumber;\n if (!dataByTopLevelFunction.has(aggregatedDataId)) {\n dataByTopLevelFunction.set(aggregatedDataId, {\n topLevelFunctionCall,\n totalReflowTime: 0,\n bottomUpData: new Set<string>(),\n topLevelFunctionCallEvents: [],\n });\n }\n const aggregatedData = dataByTopLevelFunction.get(aggregatedDataId);\n if (aggregatedData) {\n aggregatedData.totalReflowTime += (e.dur ?? 0);\n aggregatedData.bottomUpData.add(bottomUpDataId);\n aggregatedData.topLevelFunctionCallEvents.push(topLevelFunctionCallEvent);\n }\n });\n\n let topTimeConsumingDataId = '';\n let maxTime = 0;\n dataByTopLevelFunction.forEach((value, key) => {\n if (value.totalReflowTime > maxTime) {\n maxTime = value.totalReflowTime;\n topTimeConsumingDataId = key;\n }\n });\n\n const aggregatedBottomUpData: BottomUpCallStack[] = [];\n const topLevelFunctionCallData = dataByTopLevelFunction.get(topTimeConsumingDataId);\n const dataSet = dataByTopLevelFunction.get(topTimeConsumingDataId)?.bottomUpData;\n if (dataSet) {\n dataSet.forEach(value => {\n const callStackData = bottomUpDataMap.get(value);\n if (callStackData && callStackData.totalTime > Helpers.Timing.milliToMicro(Types.Timing.Milli(1))) {\n aggregatedBottomUpData.push(callStackData);\n }\n });\n }\n\n return [topLevelFunctionCallData, aggregatedBottomUpData];\n}\n\nfunction finalize(partialModel: PartialInsightModel<ForcedReflowInsightModel>): ForcedReflowInsightModel {\n return {\n insightKey: InsightKeys.FORCED_REFLOW,\n strings: UIStrings,\n title: i18nString(UIStrings.title),\n description: i18nString(UIStrings.description),\n category: InsightCategory.ALL,\n state: partialModel.topLevelFunctionCallData !== undefined && partialModel.aggregatedBottomUpData.length !== 0 ?\n 'fail' :\n 'pass',\n ...partialModel,\n };\n}\n\nexport function generateInsight(traceParsedData: RequiredData<typeof deps>): ForcedReflowInsightModel {\n const warningsData = traceParsedData.Warnings;\n const entryToNodeMap = traceParsedData.Renderer.entryToNode;\n\n if (!warningsData) {\n throw new Error('no warnings data');\n }\n\n if (!entryToNodeMap) {\n throw new Error('no renderer data');\n }\n\n const [topLevelFunctionCallData, aggregatedBottomUpData] =\n aggregateForcedReflow(warningsData.perWarning, entryToNodeMap);\n\n return finalize({\n relatedEvents: topLevelFunctionCallData?.topLevelFunctionCallEvents,\n topLevelFunctionCallData,\n aggregatedBottomUpData,\n });\n}\n"]}
1
+ {"version":3,"file":"ForcedReflow.js","sourceRoot":"","sources":["../../../../../../../front_end/models/trace/insights/ForcedReflow.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 The Chromium Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\nimport * as i18n from '../../../core/i18n/i18n.js';\nimport type * as Protocol from '../../../generated/protocol.js';\nimport type * as Handlers from '../handlers/handlers.js';\nimport type {Warning} from '../handlers/WarningsHandler.js';\nimport * as Helpers from '../helpers/helpers.js';\nimport * as Types from '../types/types.js';\n\nimport {\n type BottomUpCallStack,\n type ForcedReflowAggregatedData,\n InsightCategory,\n InsightKeys,\n type InsightModel,\n type InsightSetContext,\n type PartialInsightModel,\n} from './types.js';\n\nexport const UIStrings = {\n /**\n *@description Title of an insight that provides details about Forced reflow.\n */\n title: 'Forced reflow',\n /**\n * @description Text to describe the forced reflow.\n */\n description:\n 'Many APIs, typically reading layout geometry, force the rendering engine to pause script execution in order to calculate the style and layout. Learn more about [forced reflow](https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing#avoid-forced-synchronous-layouts) and its mitigations.',\n /**\n *@description Title of a list to provide related stack trace data\n */\n relatedStackTrace: 'Stack trace',\n /**\n *@description Text to describe the top time-consuming function call\n */\n topTimeConsumingFunctionCall: 'Top function call',\n /**\n * @description Text to describe the total reflow time\n */\n totalReflowTime: 'Total reflow time',\n} as const;\n\nconst str_ = i18n.i18n.registerUIStrings('models/trace/insights/ForcedReflow.ts', UIStrings);\nexport const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);\n\nexport type ForcedReflowInsightModel = InsightModel<typeof UIStrings, {\n topLevelFunctionCallData: ForcedReflowAggregatedData | undefined,\n aggregatedBottomUpData: BottomUpCallStack[],\n}>;\n\nfunction aggregateForcedReflow(\n data: Map<Warning, Types.Events.Event[]>,\n entryToNodeMap: Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>):\n [ForcedReflowAggregatedData|undefined, BottomUpCallStack[]] {\n const dataByTopLevelFunction = new Map<string, ForcedReflowAggregatedData>();\n const bottomUpDataMap = new Map<string, BottomUpCallStack>();\n const forcedReflowEvents = data.get('FORCED_REFLOW');\n if (!forcedReflowEvents || forcedReflowEvents.length === 0) {\n return [undefined, []];\n }\n\n forcedReflowEvents.forEach(e => {\n // Gather the stack traces by searching in the tree\n const traceNode = entryToNodeMap.get(e);\n\n if (!traceNode) {\n return;\n }\n // Compute call stack fully\n const bottomUpData: Array<Types.Events.CallFrame|Protocol.Runtime.CallFrame> = [];\n let currentNode = traceNode;\n let previousNode;\n const childStack: Protocol.Runtime.CallFrame[] = [];\n\n // Some profileCalls maybe constructed as its children in hierarchy tree\n while (currentNode.children.length > 0) {\n const childNode = currentNode.children[0];\n if (!previousNode) {\n previousNode = childNode;\n }\n const eventData = childNode.entry;\n if (Types.Events.isProfileCall(eventData)) {\n childStack.push(eventData.callFrame);\n }\n currentNode = childNode;\n }\n\n // In order to avoid too much information, we only contain 2 levels bottomUp data,\n while (childStack.length > 0 && bottomUpData.length < 2) {\n const traceData = childStack.pop();\n if (traceData) {\n bottomUpData.push(traceData);\n }\n }\n\n let node = traceNode.parent;\n let topLevelFunctionCall;\n let topLevelFunctionCallEvent: Types.Events.Event|undefined;\n while (node) {\n const eventData = node.entry;\n if (Types.Events.isProfileCall(eventData)) {\n if (bottomUpData.length < 2) {\n bottomUpData.push(eventData.callFrame);\n }\n } else {\n // We have finished searching bottom up data\n if (Types.Events.isFunctionCall(eventData) && eventData.args.data &&\n Types.Events.objectIsCallFrame(eventData.args.data)) {\n topLevelFunctionCall = eventData.args.data;\n topLevelFunctionCallEvent = eventData;\n if (bottomUpData.length === 0) {\n bottomUpData.push(topLevelFunctionCall);\n }\n } else {\n // Sometimes the top level task can be other JSInvocation event\n // then we use the top level profile call as topLevelFunctionCall's data\n const previousData = previousNode?.entry;\n if (previousData && Types.Events.isProfileCall(previousData)) {\n topLevelFunctionCall = previousData.callFrame;\n topLevelFunctionCallEvent = previousNode?.entry;\n }\n }\n break;\n }\n previousNode = node;\n node = node.parent;\n }\n\n if (!topLevelFunctionCall || !topLevelFunctionCallEvent || bottomUpData.length === 0) {\n return;\n }\n const bottomUpDataId =\n bottomUpData[0].scriptId + ':' + bottomUpData[0].lineNumber + ':' + bottomUpData[0].columnNumber + ':';\n\n const data = bottomUpDataMap.get(bottomUpDataId) ?? {\n bottomUpData: bottomUpData[0],\n totalTime: 0,\n relatedEvents: [],\n };\n data.totalTime += (e.dur ?? 0);\n data.relatedEvents.push(e);\n bottomUpDataMap.set(bottomUpDataId, data);\n\n const aggregatedDataId =\n topLevelFunctionCall.scriptId + ':' + topLevelFunctionCall.lineNumber + ':' + topLevelFunctionCall.columnNumber;\n if (!dataByTopLevelFunction.has(aggregatedDataId)) {\n dataByTopLevelFunction.set(aggregatedDataId, {\n topLevelFunctionCall,\n totalReflowTime: 0,\n bottomUpData: new Set<string>(),\n topLevelFunctionCallEvents: [],\n });\n }\n const aggregatedData = dataByTopLevelFunction.get(aggregatedDataId);\n if (aggregatedData) {\n aggregatedData.totalReflowTime += (e.dur ?? 0);\n aggregatedData.bottomUpData.add(bottomUpDataId);\n aggregatedData.topLevelFunctionCallEvents.push(topLevelFunctionCallEvent);\n }\n });\n\n let topTimeConsumingDataId = '';\n let maxTime = 0;\n dataByTopLevelFunction.forEach((value, key) => {\n if (value.totalReflowTime > maxTime) {\n maxTime = value.totalReflowTime;\n topTimeConsumingDataId = key;\n }\n });\n\n const aggregatedBottomUpData: BottomUpCallStack[] = [];\n const topLevelFunctionCallData = dataByTopLevelFunction.get(topTimeConsumingDataId);\n const dataSet = dataByTopLevelFunction.get(topTimeConsumingDataId)?.bottomUpData;\n if (dataSet) {\n dataSet.forEach(value => {\n const callStackData = bottomUpDataMap.get(value);\n if (callStackData && callStackData.totalTime > Helpers.Timing.milliToMicro(Types.Timing.Milli(1))) {\n aggregatedBottomUpData.push(callStackData);\n }\n });\n }\n\n return [topLevelFunctionCallData, aggregatedBottomUpData];\n}\n\nfunction finalize(partialModel: PartialInsightModel<ForcedReflowInsightModel>): ForcedReflowInsightModel {\n return {\n insightKey: InsightKeys.FORCED_REFLOW,\n strings: UIStrings,\n title: i18nString(UIStrings.title),\n description: i18nString(UIStrings.description),\n category: InsightCategory.ALL,\n state: partialModel.topLevelFunctionCallData !== undefined && partialModel.aggregatedBottomUpData.length !== 0 ?\n 'fail' :\n 'pass',\n ...partialModel,\n };\n}\n\nexport function generateInsight(\n traceParsedData: Handlers.Types.ParsedTrace, context: InsightSetContext): ForcedReflowInsightModel {\n const warningsData = traceParsedData.Warnings;\n const entryToNodeMap = traceParsedData.Renderer.entryToNode;\n\n if (!warningsData) {\n throw new Error('no warnings data');\n }\n\n if (!entryToNodeMap) {\n throw new Error('no renderer data');\n }\n\n const [topLevelFunctionCallData, aggregatedBottomUpData] =\n aggregateForcedReflow(warningsData.perWarning, entryToNodeMap);\n\n return finalize({\n frameId: context.frameId,\n relatedEvents: topLevelFunctionCallData?.topLevelFunctionCallEvents,\n topLevelFunctionCallData,\n aggregatedBottomUpData,\n });\n}\n"]}
@@ -1,5 +1,7 @@
1
+ import type * as Platform from '../../../core/platform/platform.js';
2
+ import type * as Handlers from '../handlers/handlers.js';
1
3
  import type * as Types from '../types/types.js';
2
- import { type InsightModel, type InsightSetContext, type RequiredData } from './types.js';
4
+ import { type InsightModel, type InsightSetContext } from './types.js';
3
5
  export declare const UIStrings: {
4
6
  /**
5
7
  * @description Title of an insight that recommends ways to reduce the size of images downloaded and used on the page.
@@ -47,8 +49,7 @@ export declare const UIStrings: {
47
49
  */
48
50
  readonly estimatedSavings: "{PH1} (Est {PH2})";
49
51
  };
50
- export declare const i18nString: (id: string, values?: Record<string, string> | undefined) => Record<string, string>;
51
- export declare function deps(): ['NetworkRequests', 'Meta', 'ImagePainting'];
52
+ export declare const i18nString: (id: string, values?: Record<string, string> | undefined) => {i18nId: string, values: Record<string, string|number>, formattedDefault: string};
52
53
  export declare enum ImageOptimizationType {
53
54
  ADJUST_COMPRESSION = "ADJUST_COMPRESSION",
54
55
  MODERN_FORMAT_OR_COMPRESSION = "MODERN_FORMAT_OR_COMPRESSION",
@@ -87,6 +88,6 @@ export type ImageDeliveryInsightModel = InsightModel<typeof UIStrings, {
87
88
  optimizableImages: OptimizableImage[];
88
89
  totalByteSavings: number;
89
90
  }>;
90
- export declare function getOptimizationMessage(optimization: ImageOptimization): string;
91
- export declare function getOptimizationMessageWithBytes(optimization: ImageOptimization): string;
92
- export declare function generateInsight(parsedTrace: RequiredData<typeof deps>, context: InsightSetContext): ImageDeliveryInsightModel;
91
+ export declare function getOptimizationMessage(optimization: ImageOptimization): {i18nId: string, values: Record<string, string|number>, formattedDefault: string};
92
+ export declare function getOptimizationMessageWithBytes(optimization: ImageOptimization): {i18nId: string, values: Record<string, string|number>, formattedDefault: string};
93
+ export declare function generateInsight(parsedTrace: Handlers.Types.ParsedTrace, context: InsightSetContext): ImageDeliveryInsightModel;
@@ -76,9 +76,8 @@ const TARGET_BYTES_PER_PIXEL_AVIF = 2 * 1 / 12;
76
76
  */
77
77
  const GIF_SIZE_THRESHOLD = 100 * 1024;
78
78
  const BYTE_SAVINGS_THRESHOLD = 4096;
79
- export function deps() {
80
- return ['NetworkRequests', 'Meta', 'ImagePainting'];
81
- }
79
+ // Ignore up to 12KB of waste for responsive images if an effort was made with breakpoints.
80
+ const BYTE_SAVINGS_THRESHOLD_RESPONSIVE_BREAKPOINTS = 12288;
82
81
  export var ImageOptimizationType;
83
82
  (function (ImageOptimizationType) {
84
83
  ImageOptimizationType["ADJUST_COMPRESSION"] = "ADJUST_COMPRESSION";
@@ -142,7 +141,9 @@ export function generateInsight(parsedTrace, context) {
142
141
  if (request.args.data.mimeType === 'image/svg+xml') {
143
142
  continue;
144
143
  }
145
- const imagePaints = parsedTrace.ImagePainting.paintImageEventForUrl.get(request.args.data.url)?.filter(isWithinContext);
144
+ // If the request was redirected, the image paints will have the pre-redirect URL.
145
+ const url = request.args.data.redirects[0]?.url ?? request.args.data.url;
146
+ const imagePaints = parsedTrace.ImagePainting.paintImageEventForUrl.get(url)?.filter(isWithinContext);
146
147
  // This will filter out things like preloaded image requests where an image file is downloaded
147
148
  // but never rendered on the page.
148
149
  if (!imagePaints?.length) {
@@ -185,23 +186,28 @@ export function generateInsight(parsedTrace, context) {
185
186
  const imageByteSavingsFromFormat = Math.max(0, ...optimizations.map(o => o.byteSavings));
186
187
  let imageByteSavings = imageByteSavingsFromFormat;
187
188
  const wastedPixelRatio = 1 - (largestImageDisplayPixels / imageFilePixels);
188
- if (wastedPixelRatio > 0) {
189
+ // Ignore CSS images because it's difficult to determine what is a spritesheet,
190
+ // and the reward-to-effort ratio for responsive CSS images is quite low https://css-tricks.com/responsive-images-css/.
191
+ if (wastedPixelRatio > 0 && !largestImagePaint.args.data.isCSS) {
189
192
  const byteSavings = Math.round(wastedPixelRatio * imageBytes);
190
- // This will compound the byte savings from any potential format changes with the image size
191
- // optimization added here.
192
- imageByteSavings += Math.round(wastedPixelRatio * (imageBytes - imageByteSavingsFromFormat));
193
- optimizations.push({
194
- type: ImageOptimizationType.RESPONSIVE_SIZE,
195
- byteSavings,
196
- fileDimensions: {
197
- width: Math.round(largestImagePaint.args.data.srcWidth),
198
- height: Math.round(largestImagePaint.args.data.srcHeight),
199
- },
200
- displayDimensions: {
201
- width: Math.round(largestImagePaint.args.data.width),
202
- height: Math.round(largestImagePaint.args.data.height),
203
- },
204
- });
193
+ const hadBreakpoints = largestImagePaint.args.data.isPicture || largestImagePaint.args.data.srcsetAttribute;
194
+ if (!hadBreakpoints || byteSavings > BYTE_SAVINGS_THRESHOLD_RESPONSIVE_BREAKPOINTS) {
195
+ // This will compound the byte savings from any potential format changes with the image size
196
+ // optimization added here.
197
+ imageByteSavings += Math.round(wastedPixelRatio * (imageBytes - imageByteSavingsFromFormat));
198
+ optimizations.push({
199
+ type: ImageOptimizationType.RESPONSIVE_SIZE,
200
+ byteSavings,
201
+ fileDimensions: {
202
+ width: Math.round(largestImagePaint.args.data.srcWidth),
203
+ height: Math.round(largestImagePaint.args.data.srcHeight),
204
+ },
205
+ displayDimensions: {
206
+ width: Math.round(largestImagePaint.args.data.width),
207
+ height: Math.round(largestImagePaint.args.data.height),
208
+ },
209
+ });
210
+ }
205
211
  }
206
212
  optimizations = optimizations.filter(optimization => optimization.byteSavings > BYTE_SAVINGS_THRESHOLD);
207
213
  if (optimizations.length > 0) {
@@ -225,6 +231,7 @@ export function generateInsight(parsedTrace, context) {
225
231
  return b.request.args.data.decodedBodyLength - a.request.args.data.decodedBodyLength;
226
232
  });
227
233
  return finalize({
234
+ frameId: context.frameId,
228
235
  optimizableImages,
229
236
  totalByteSavings: optimizableImages.reduce((total, img) => total + img.byteSavings, 0),
230
237
  metricSavings: metricSavingsForWastedBytes(wastedBytesByRequestId, context),
@@ -1 +1 @@
1
- {"version":3,"file":"ImageDelivery.js","sourceRoot":"","sources":["../../../../../../../front_end/models/trace/insights/ImageDelivery.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 The Chromium Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\nimport * as i18n from '../../../core/i18n/i18n.js';\nimport * as Helpers from '../helpers/helpers.js';\nimport type * as Types from '../types/types.js';\n\nimport {metricSavingsForWastedBytes} from './Common.js';\nimport {\n InsightCategory,\n InsightKeys,\n type InsightModel,\n type InsightSetContext,\n type PartialInsightModel,\n type RequiredData,\n} from './types.js';\n\nexport const UIStrings = {\n /**\n * @description Title of an insight that recommends ways to reduce the size of images downloaded and used on the page.\n */\n title: 'Improve image delivery',\n /**\n * @description Description of an insight that recommends ways to reduce the size of images downloaded and used on the page.\n */\n description:\n 'Reducing the download time of images can improve the perceived load time of the page and LCP. [Learn more about optimizing image size](https://developer.chrome.com/docs/lighthouse/performance/uses-optimized-images/)',\n /**\n * @description Message displayed in a chip explaining that an image file size is large for the # of pixels it has and recommends possible adjustments to improve the image size.\n */\n useCompression: 'Increasing the image compression factor could improve this image\\'s download size.',\n /**\n * @description Message displayed in a chip explaining that an image file size is large for the # of pixels it has and recommends possible adjustments to improve the image size.\n */\n useModernFormat:\n 'Using a modern image format (WebP, AVIF) or increasing the image compression could improve this image\\'s download size.',\n /**\n * @description Message displayed in a chip advising the user to use video formats instead of GIFs because videos generally have smaller file sizes.\n */\n useVideoFormat: 'Using video formats instead of GIFs can improve the download size of animated content.',\n /**\n * @description Message displayed in a chip explaining that an image was displayed on the page with dimensions much smaller than the image file dimensions.\n * @example {1000x500} PH1\n * @example {100x50} PH2\n */\n useResponsiveSize:\n 'This image file is larger than it needs to be ({PH1}) for its displayed dimensions ({PH2}). Use responsive images to reduce the image download size.',\n /**\n * @description Column header for a table column containing network requests for images which can improve their file size (e.g. use a different format, increase compression, etc).\n */\n optimizeFile: 'Optimize file size',\n /**\n * @description Table row value representing the remaining items not shown in the table due to size constraints. This row will always represent at least 2 items.\n * @example {5} PH1\n */\n others: '{PH1} others',\n /**\n * @description Text status indicating that no potential optimizations were found for any image file\n */\n noOptimizableImages: 'No optimizable images',\n /**\n * @description Text describing the estimated number of bytes that an image file optimization can save. This text is appended to another block of text describing the image optimization in more detail. \"Est\" means \"Estimated\".\n * @example {Use the correct image dimensions to reduce the image file size.} PH1\n * @example {50 MB} PH2\n */\n estimatedSavings: '{PH1} (Est {PH2})',\n} as const;\n\nconst str_ = i18n.i18n.registerUIStrings('models/trace/insights/ImageDelivery.ts', UIStrings);\nexport const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);\n\n/**\n * Even JPEGs with lots of detail can usually be compressed down to <1 byte per pixel\n * Using 4:2:2 subsampling already gets an uncompressed bitmap to 2 bytes per pixel.\n * The compression ratio for JPEG is usually somewhere around 10:1 depending on content, so\n * 8:1 is a reasonable expectation for web content which is 1.5MB for a 6MP image.\n *\n * WebP usually gives ~20% additional savings on top of that, so we will assume 10:1 for WebP.\n * This is quite pessimistic as their study shows a photographic compression ratio of ~29:1.\n * https://developers.google.com/speed/webp/docs/webp_lossless_alpha_study#results\n *\n * AVIF usually gives ~20% additional savings on top of WebP, so we will use 12:1 for AVIF.\n * This is quite pessimistic as Netflix study shows a photographic compression ratio of ~40:1\n * (0.4 *bits* per pixel at SSIM 0.97).\n * https://netflixtechblog.com/avif-for-next-generation-image-coding-b1d75675fe4\n */\nconst TARGET_BYTES_PER_PIXEL_AVIF = 2 * 1 / 12;\n\n/**\n * If GIFs are above this size, we'll flag them\n * See https://github.com/GoogleChrome/lighthouse/pull/4885#discussion_r178406623 and https://github.com/GoogleChrome/lighthouse/issues/4696#issuecomment-370979920\n */\nconst GIF_SIZE_THRESHOLD = 100 * 1024;\n\nconst BYTE_SAVINGS_THRESHOLD = 4096;\n\nexport function deps(): ['NetworkRequests', 'Meta', 'ImagePainting'] {\n return ['NetworkRequests', 'Meta', 'ImagePainting'];\n}\n\nexport enum ImageOptimizationType {\n ADJUST_COMPRESSION = 'ADJUST_COMPRESSION',\n MODERN_FORMAT_OR_COMPRESSION = 'MODERN_FORMAT_OR_COMPRESSION',\n VIDEO_FORMAT = 'VIDEO_FORMAT',\n RESPONSIVE_SIZE = 'RESPONSIVE_SIZE',\n}\n\nexport type ImageOptimization = {\n type: Exclude<ImageOptimizationType, ImageOptimizationType.RESPONSIVE_SIZE>,\n byteSavings: number,\n}|{\n type: ImageOptimizationType.RESPONSIVE_SIZE,\n byteSavings: number,\n fileDimensions: {width: number, height: number},\n displayDimensions: {width: number, height: number},\n};\n\nexport interface OptimizableImage {\n request: Types.Events.SyntheticNetworkRequest;\n optimizations: ImageOptimization[];\n byteSavings: number;\n /**\n * If the an image resource has multiple `PaintImage`s, we compare its intrinsic size to the largest of the displayed sizes.\n *\n * It is theoretically possible for `PaintImage` events with the same URL to have different intrinsic sizes.\n * However, this should be rare because it requires serving different images from the same URL.\n */\n largestImagePaint: Types.Events.PaintImage;\n}\n\nexport type ImageDeliveryInsightModel = InsightModel<typeof UIStrings, {\n /** Sorted by potential byte savings, then by size of image. */\n optimizableImages: OptimizableImage[],\n totalByteSavings: number,\n}>;\n\nexport function getOptimizationMessage(optimization: ImageOptimization): string {\n switch (optimization.type) {\n case ImageOptimizationType.ADJUST_COMPRESSION:\n return i18nString(UIStrings.useCompression);\n case ImageOptimizationType.MODERN_FORMAT_OR_COMPRESSION:\n return i18nString(UIStrings.useModernFormat);\n case ImageOptimizationType.VIDEO_FORMAT:\n return i18nString(UIStrings.useVideoFormat);\n case ImageOptimizationType.RESPONSIVE_SIZE:\n return i18nString(UIStrings.useResponsiveSize, {\n PH1: `${optimization.fileDimensions.width}x${optimization.fileDimensions.height}`,\n PH2: `${optimization.displayDimensions.width}x${optimization.displayDimensions.height}`,\n });\n }\n}\n\nexport function getOptimizationMessageWithBytes(optimization: ImageOptimization): string {\n const byteSavingsText = i18n.ByteUtilities.bytesToString(optimization.byteSavings);\n const optimizationMessage = getOptimizationMessage(optimization);\n return i18nString(UIStrings.estimatedSavings, {PH1: optimizationMessage, PH2: byteSavingsText});\n}\n\nfunction finalize(partialModel: PartialInsightModel<ImageDeliveryInsightModel>): ImageDeliveryInsightModel {\n return {\n insightKey: InsightKeys.IMAGE_DELIVERY,\n strings: UIStrings,\n title: i18nString(UIStrings.title),\n description: i18nString(UIStrings.description),\n category: InsightCategory.LCP,\n state: partialModel.optimizableImages.length > 0 ? 'fail' : 'pass',\n ...partialModel,\n relatedEvents: new Map(partialModel.optimizableImages.map(\n image => [image.request, image.optimizations.map(getOptimizationMessageWithBytes)])),\n };\n}\n\n/**\n * Calculate rough savings percentage based on 1000 real gifs transcoded to video\n * https://github.com/GoogleChrome/lighthouse/issues/4696#issuecomment-380296510\n */\nfunction estimateGIFPercentSavings(request: Types.Events.SyntheticNetworkRequest): number {\n return Math.round((29.1 * Math.log10(request.args.data.decodedBodyLength) - 100.7)) / 100;\n}\n\nfunction getPixelCounts(paintImage: Types.Events.PaintImage): {displayedPixels: number, filePixels: number} {\n return {\n filePixels: paintImage.args.data.srcWidth * paintImage.args.data.srcHeight,\n displayedPixels: paintImage.args.data.width * paintImage.args.data.height,\n };\n}\n\nexport function generateInsight(\n parsedTrace: RequiredData<typeof deps>, context: InsightSetContext): ImageDeliveryInsightModel {\n const isWithinContext = (event: Types.Events.Event): boolean => Helpers.Timing.eventIsInBounds(event, context.bounds);\n\n const contextRequests = parsedTrace.NetworkRequests.byTime.filter(isWithinContext);\n\n const optimizableImages: OptimizableImage[] = [];\n for (const request of contextRequests) {\n if (request.args.data.resourceType !== 'Image') {\n continue;\n }\n\n if (request.args.data.mimeType === 'image/svg+xml') {\n continue;\n }\n\n const imagePaints =\n parsedTrace.ImagePainting.paintImageEventForUrl.get(request.args.data.url)?.filter(isWithinContext);\n\n // This will filter out things like preloaded image requests where an image file is downloaded\n // but never rendered on the page.\n if (!imagePaints?.length) {\n continue;\n }\n\n const largestImagePaint = imagePaints.reduce((prev, curr) => {\n const prevPixels = getPixelCounts(prev).displayedPixels;\n const currPixels = getPixelCounts(curr).displayedPixels;\n return prevPixels > currPixels ? prev : curr;\n });\n\n const {\n filePixels: imageFilePixels,\n displayedPixels: largestImageDisplayPixels,\n } = getPixelCounts(largestImagePaint);\n\n // Decoded body length is almost always the right one to be using because of the below:\n // `encodedDataLength = decodedBodyLength + headers`.\n // HOWEVER, there are some cases where an image is compressed again over the network and transfer size\n // is smaller (see https://github.com/GoogleChrome/lighthouse/pull/4968).\n // Use the min of the two numbers to be safe.\n const imageBytes = Math.min(request.args.data.decodedBodyLength, request.args.data.encodedDataLength);\n\n const bytesPerPixel = imageBytes / imageFilePixels;\n\n let optimizations: ImageOptimization[] = [];\n if (request.args.data.mimeType === 'image/gif') {\n if (imageBytes > GIF_SIZE_THRESHOLD) {\n const percentSavings = estimateGIFPercentSavings(request);\n const byteSavings = Math.round(imageBytes * percentSavings);\n optimizations.push({type: ImageOptimizationType.VIDEO_FORMAT, byteSavings});\n }\n } else if (bytesPerPixel > TARGET_BYTES_PER_PIXEL_AVIF) {\n const idealAvifImageSize = Math.round(TARGET_BYTES_PER_PIXEL_AVIF * imageFilePixels);\n const byteSavings = imageBytes - idealAvifImageSize;\n if (request.args.data.mimeType !== 'image/webp' && request.args.data.mimeType !== 'image/avif') {\n optimizations.push({type: ImageOptimizationType.MODERN_FORMAT_OR_COMPRESSION, byteSavings});\n } else {\n optimizations.push({type: ImageOptimizationType.ADJUST_COMPRESSION, byteSavings});\n }\n }\n\n // At this point (before looking at image size), the # of optimizations should only ever be 1 or 0\n // Math.max handles both cases correctly, and is defensive against future patches that would add\n // more than 1 format-specific optimization by this point.\n const imageByteSavingsFromFormat = Math.max(0, ...optimizations.map(o => o.byteSavings));\n let imageByteSavings = imageByteSavingsFromFormat;\n\n const wastedPixelRatio = 1 - (largestImageDisplayPixels / imageFilePixels);\n if (wastedPixelRatio > 0) {\n const byteSavings = Math.round(wastedPixelRatio * imageBytes);\n\n // This will compound the byte savings from any potential format changes with the image size\n // optimization added here.\n imageByteSavings += Math.round(wastedPixelRatio * (imageBytes - imageByteSavingsFromFormat));\n\n optimizations.push({\n type: ImageOptimizationType.RESPONSIVE_SIZE,\n byteSavings,\n fileDimensions: {\n width: Math.round(largestImagePaint.args.data.srcWidth),\n height: Math.round(largestImagePaint.args.data.srcHeight),\n },\n displayDimensions: {\n width: Math.round(largestImagePaint.args.data.width),\n height: Math.round(largestImagePaint.args.data.height),\n },\n });\n }\n\n optimizations = optimizations.filter(optimization => optimization.byteSavings > BYTE_SAVINGS_THRESHOLD);\n\n if (optimizations.length > 0) {\n optimizableImages.push({\n request,\n largestImagePaint,\n optimizations,\n byteSavings: imageByteSavings,\n });\n }\n }\n\n const wastedBytesByRequestId = new Map<string, number>();\n for (const image of optimizableImages) {\n wastedBytesByRequestId.set(image.request.args.data.requestId, image.byteSavings);\n }\n\n // Sort by savings, then by size of image.\n optimizableImages.sort((a, b) => {\n if (b.byteSavings !== a.byteSavings) {\n return b.byteSavings - a.byteSavings;\n }\n\n return b.request.args.data.decodedBodyLength - a.request.args.data.decodedBodyLength;\n });\n\n return finalize({\n optimizableImages,\n totalByteSavings: optimizableImages.reduce((total, img) => total + img.byteSavings, 0),\n metricSavings: metricSavingsForWastedBytes(wastedBytesByRequestId, context),\n });\n}\n"]}
1
+ {"version":3,"file":"ImageDelivery.js","sourceRoot":"","sources":["../../../../../../../front_end/models/trace/insights/ImageDelivery.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 The Chromium Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\nimport * as i18n from '../../../core/i18n/i18n.js';\nimport type * as Platform from '../../../core/platform/platform.js';\nimport type * as Handlers from '../handlers/handlers.js';\nimport * as Helpers from '../helpers/helpers.js';\nimport type * as Types from '../types/types.js';\n\nimport {metricSavingsForWastedBytes} from './Common.js';\nimport {\n InsightCategory,\n InsightKeys,\n type InsightModel,\n type InsightSetContext,\n type PartialInsightModel,\n} from './types.js';\n\nexport const UIStrings = {\n /**\n * @description Title of an insight that recommends ways to reduce the size of images downloaded and used on the page.\n */\n title: 'Improve image delivery',\n /**\n * @description Description of an insight that recommends ways to reduce the size of images downloaded and used on the page.\n */\n description:\n 'Reducing the download time of images can improve the perceived load time of the page and LCP. [Learn more about optimizing image size](https://developer.chrome.com/docs/lighthouse/performance/uses-optimized-images/)',\n /**\n * @description Message displayed in a chip explaining that an image file size is large for the # of pixels it has and recommends possible adjustments to improve the image size.\n */\n useCompression: 'Increasing the image compression factor could improve this image\\'s download size.',\n /**\n * @description Message displayed in a chip explaining that an image file size is large for the # of pixels it has and recommends possible adjustments to improve the image size.\n */\n useModernFormat:\n 'Using a modern image format (WebP, AVIF) or increasing the image compression could improve this image\\'s download size.',\n /**\n * @description Message displayed in a chip advising the user to use video formats instead of GIFs because videos generally have smaller file sizes.\n */\n useVideoFormat: 'Using video formats instead of GIFs can improve the download size of animated content.',\n /**\n * @description Message displayed in a chip explaining that an image was displayed on the page with dimensions much smaller than the image file dimensions.\n * @example {1000x500} PH1\n * @example {100x50} PH2\n */\n useResponsiveSize:\n 'This image file is larger than it needs to be ({PH1}) for its displayed dimensions ({PH2}). Use responsive images to reduce the image download size.',\n /**\n * @description Column header for a table column containing network requests for images which can improve their file size (e.g. use a different format, increase compression, etc).\n */\n optimizeFile: 'Optimize file size',\n /**\n * @description Table row value representing the remaining items not shown in the table due to size constraints. This row will always represent at least 2 items.\n * @example {5} PH1\n */\n others: '{PH1} others',\n /**\n * @description Text status indicating that no potential optimizations were found for any image file\n */\n noOptimizableImages: 'No optimizable images',\n /**\n * @description Text describing the estimated number of bytes that an image file optimization can save. This text is appended to another block of text describing the image optimization in more detail. \"Est\" means \"Estimated\".\n * @example {Use the correct image dimensions to reduce the image file size.} PH1\n * @example {50 MB} PH2\n */\n estimatedSavings: '{PH1} (Est {PH2})',\n} as const;\n\nconst str_ = i18n.i18n.registerUIStrings('models/trace/insights/ImageDelivery.ts', UIStrings);\nexport const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);\n\n/**\n * Even JPEGs with lots of detail can usually be compressed down to <1 byte per pixel\n * Using 4:2:2 subsampling already gets an uncompressed bitmap to 2 bytes per pixel.\n * The compression ratio for JPEG is usually somewhere around 10:1 depending on content, so\n * 8:1 is a reasonable expectation for web content which is 1.5MB for a 6MP image.\n *\n * WebP usually gives ~20% additional savings on top of that, so we will assume 10:1 for WebP.\n * This is quite pessimistic as their study shows a photographic compression ratio of ~29:1.\n * https://developers.google.com/speed/webp/docs/webp_lossless_alpha_study#results\n *\n * AVIF usually gives ~20% additional savings on top of WebP, so we will use 12:1 for AVIF.\n * This is quite pessimistic as Netflix study shows a photographic compression ratio of ~40:1\n * (0.4 *bits* per pixel at SSIM 0.97).\n * https://netflixtechblog.com/avif-for-next-generation-image-coding-b1d75675fe4\n */\nconst TARGET_BYTES_PER_PIXEL_AVIF = 2 * 1 / 12;\n\n/**\n * If GIFs are above this size, we'll flag them\n * See https://github.com/GoogleChrome/lighthouse/pull/4885#discussion_r178406623 and https://github.com/GoogleChrome/lighthouse/issues/4696#issuecomment-370979920\n */\nconst GIF_SIZE_THRESHOLD = 100 * 1024;\n\nconst BYTE_SAVINGS_THRESHOLD = 4096;\n\n// Ignore up to 12KB of waste for responsive images if an effort was made with breakpoints.\nconst BYTE_SAVINGS_THRESHOLD_RESPONSIVE_BREAKPOINTS = 12288;\n\nexport enum ImageOptimizationType {\n ADJUST_COMPRESSION = 'ADJUST_COMPRESSION',\n MODERN_FORMAT_OR_COMPRESSION = 'MODERN_FORMAT_OR_COMPRESSION',\n VIDEO_FORMAT = 'VIDEO_FORMAT',\n RESPONSIVE_SIZE = 'RESPONSIVE_SIZE',\n}\n\nexport type ImageOptimization = {\n type: Exclude<ImageOptimizationType, ImageOptimizationType.RESPONSIVE_SIZE>,\n byteSavings: number,\n}|{\n type: ImageOptimizationType.RESPONSIVE_SIZE,\n byteSavings: number,\n fileDimensions: {width: number, height: number},\n displayDimensions: {width: number, height: number},\n};\n\nexport interface OptimizableImage {\n request: Types.Events.SyntheticNetworkRequest;\n optimizations: ImageOptimization[];\n byteSavings: number;\n /**\n * If the an image resource has multiple `PaintImage`s, we compare its intrinsic size to the largest of the displayed sizes.\n *\n * It is theoretically possible for `PaintImage` events with the same URL to have different intrinsic sizes.\n * However, this should be rare because it requires serving different images from the same URL.\n */\n largestImagePaint: Types.Events.PaintImage;\n}\n\nexport type ImageDeliveryInsightModel = InsightModel<typeof UIStrings, {\n /** Sorted by potential byte savings, then by size of image. */\n optimizableImages: OptimizableImage[],\n totalByteSavings: number,\n}>;\n\nexport function getOptimizationMessage(optimization: ImageOptimization): Platform.UIString.LocalizedString {\n switch (optimization.type) {\n case ImageOptimizationType.ADJUST_COMPRESSION:\n return i18nString(UIStrings.useCompression);\n case ImageOptimizationType.MODERN_FORMAT_OR_COMPRESSION:\n return i18nString(UIStrings.useModernFormat);\n case ImageOptimizationType.VIDEO_FORMAT:\n return i18nString(UIStrings.useVideoFormat);\n case ImageOptimizationType.RESPONSIVE_SIZE:\n return i18nString(UIStrings.useResponsiveSize, {\n PH1: `${optimization.fileDimensions.width}x${optimization.fileDimensions.height}`,\n PH2: `${optimization.displayDimensions.width}x${optimization.displayDimensions.height}`,\n });\n }\n}\n\nexport function getOptimizationMessageWithBytes(optimization: ImageOptimization): Platform.UIString.LocalizedString {\n const byteSavingsText = i18n.ByteUtilities.bytesToString(optimization.byteSavings);\n const optimizationMessage = getOptimizationMessage(optimization);\n return i18nString(UIStrings.estimatedSavings, {PH1: optimizationMessage, PH2: byteSavingsText});\n}\n\nfunction finalize(partialModel: PartialInsightModel<ImageDeliveryInsightModel>): ImageDeliveryInsightModel {\n return {\n insightKey: InsightKeys.IMAGE_DELIVERY,\n strings: UIStrings,\n title: i18nString(UIStrings.title),\n description: i18nString(UIStrings.description),\n category: InsightCategory.LCP,\n state: partialModel.optimizableImages.length > 0 ? 'fail' : 'pass',\n ...partialModel,\n relatedEvents: new Map(partialModel.optimizableImages.map(\n image => [image.request, image.optimizations.map(getOptimizationMessageWithBytes)])),\n };\n}\n\n/**\n * Calculate rough savings percentage based on 1000 real gifs transcoded to video\n * https://github.com/GoogleChrome/lighthouse/issues/4696#issuecomment-380296510\n */\nfunction estimateGIFPercentSavings(request: Types.Events.SyntheticNetworkRequest): number {\n return Math.round((29.1 * Math.log10(request.args.data.decodedBodyLength) - 100.7)) / 100;\n}\n\nfunction getPixelCounts(paintImage: Types.Events.PaintImage): {displayedPixels: number, filePixels: number} {\n return {\n filePixels: paintImage.args.data.srcWidth * paintImage.args.data.srcHeight,\n displayedPixels: paintImage.args.data.width * paintImage.args.data.height,\n };\n}\n\nexport function generateInsight(\n parsedTrace: Handlers.Types.ParsedTrace, context: InsightSetContext): ImageDeliveryInsightModel {\n const isWithinContext = (event: Types.Events.Event): boolean => Helpers.Timing.eventIsInBounds(event, context.bounds);\n\n const contextRequests = parsedTrace.NetworkRequests.byTime.filter(isWithinContext);\n\n const optimizableImages: OptimizableImage[] = [];\n for (const request of contextRequests) {\n if (request.args.data.resourceType !== 'Image') {\n continue;\n }\n\n if (request.args.data.mimeType === 'image/svg+xml') {\n continue;\n }\n\n // If the request was redirected, the image paints will have the pre-redirect URL.\n const url = request.args.data.redirects[0]?.url ?? request.args.data.url;\n const imagePaints = parsedTrace.ImagePainting.paintImageEventForUrl.get(url)?.filter(isWithinContext);\n\n // This will filter out things like preloaded image requests where an image file is downloaded\n // but never rendered on the page.\n if (!imagePaints?.length) {\n continue;\n }\n\n const largestImagePaint = imagePaints.reduce((prev, curr) => {\n const prevPixels = getPixelCounts(prev).displayedPixels;\n const currPixels = getPixelCounts(curr).displayedPixels;\n return prevPixels > currPixels ? prev : curr;\n });\n\n const {\n filePixels: imageFilePixels,\n displayedPixels: largestImageDisplayPixels,\n } = getPixelCounts(largestImagePaint);\n\n // Decoded body length is almost always the right one to be using because of the below:\n // `encodedDataLength = decodedBodyLength + headers`.\n // HOWEVER, there are some cases where an image is compressed again over the network and transfer size\n // is smaller (see https://github.com/GoogleChrome/lighthouse/pull/4968).\n // Use the min of the two numbers to be safe.\n const imageBytes = Math.min(request.args.data.decodedBodyLength, request.args.data.encodedDataLength);\n\n const bytesPerPixel = imageBytes / imageFilePixels;\n\n let optimizations: ImageOptimization[] = [];\n if (request.args.data.mimeType === 'image/gif') {\n if (imageBytes > GIF_SIZE_THRESHOLD) {\n const percentSavings = estimateGIFPercentSavings(request);\n const byteSavings = Math.round(imageBytes * percentSavings);\n optimizations.push({type: ImageOptimizationType.VIDEO_FORMAT, byteSavings});\n }\n } else if (bytesPerPixel > TARGET_BYTES_PER_PIXEL_AVIF) {\n const idealAvifImageSize = Math.round(TARGET_BYTES_PER_PIXEL_AVIF * imageFilePixels);\n const byteSavings = imageBytes - idealAvifImageSize;\n if (request.args.data.mimeType !== 'image/webp' && request.args.data.mimeType !== 'image/avif') {\n optimizations.push({type: ImageOptimizationType.MODERN_FORMAT_OR_COMPRESSION, byteSavings});\n } else {\n optimizations.push({type: ImageOptimizationType.ADJUST_COMPRESSION, byteSavings});\n }\n }\n\n // At this point (before looking at image size), the # of optimizations should only ever be 1 or 0\n // Math.max handles both cases correctly, and is defensive against future patches that would add\n // more than 1 format-specific optimization by this point.\n const imageByteSavingsFromFormat = Math.max(0, ...optimizations.map(o => o.byteSavings));\n let imageByteSavings = imageByteSavingsFromFormat;\n\n const wastedPixelRatio = 1 - (largestImageDisplayPixels / imageFilePixels);\n\n // Ignore CSS images because it's difficult to determine what is a spritesheet,\n // and the reward-to-effort ratio for responsive CSS images is quite low https://css-tricks.com/responsive-images-css/.\n if (wastedPixelRatio > 0 && !largestImagePaint.args.data.isCSS) {\n const byteSavings = Math.round(wastedPixelRatio * imageBytes);\n\n const hadBreakpoints = largestImagePaint.args.data.isPicture || largestImagePaint.args.data.srcsetAttribute;\n if (!hadBreakpoints || byteSavings > BYTE_SAVINGS_THRESHOLD_RESPONSIVE_BREAKPOINTS) {\n // This will compound the byte savings from any potential format changes with the image size\n // optimization added here.\n imageByteSavings += Math.round(wastedPixelRatio * (imageBytes - imageByteSavingsFromFormat));\n\n optimizations.push({\n type: ImageOptimizationType.RESPONSIVE_SIZE,\n byteSavings,\n fileDimensions: {\n width: Math.round(largestImagePaint.args.data.srcWidth),\n height: Math.round(largestImagePaint.args.data.srcHeight),\n },\n displayDimensions: {\n width: Math.round(largestImagePaint.args.data.width),\n height: Math.round(largestImagePaint.args.data.height),\n },\n });\n }\n }\n\n optimizations = optimizations.filter(optimization => optimization.byteSavings > BYTE_SAVINGS_THRESHOLD);\n\n if (optimizations.length > 0) {\n optimizableImages.push({\n request,\n largestImagePaint,\n optimizations,\n byteSavings: imageByteSavings,\n });\n }\n }\n\n const wastedBytesByRequestId = new Map<string, number>();\n for (const image of optimizableImages) {\n wastedBytesByRequestId.set(image.request.args.data.requestId, image.byteSavings);\n }\n\n // Sort by savings, then by size of image.\n optimizableImages.sort((a, b) => {\n if (b.byteSavings !== a.byteSavings) {\n return b.byteSavings - a.byteSavings;\n }\n\n return b.request.args.data.decodedBodyLength - a.request.args.data.decodedBodyLength;\n });\n\n return finalize({\n frameId: context.frameId,\n optimizableImages,\n totalByteSavings: optimizableImages.reduce((total, img) => total + img.byteSavings, 0),\n metricSavings: metricSavingsForWastedBytes(wastedBytesByRequestId, context),\n });\n}\n"]}
@@ -1,5 +1,6 @@
1
+ import type * as Handlers from '../handlers/handlers.js';
1
2
  import type { SyntheticInteractionPair } from '../types/TraceEvents.js';
2
- import { type InsightModel, type InsightSetContext, type RequiredData } from './types.js';
3
+ import { type InsightModel, type InsightSetContext } from './types.js';
3
4
  export declare const UIStrings: {
4
5
  /**
5
6
  * @description Text to tell the user about the longest user interaction.
@@ -35,9 +36,8 @@ export declare const UIStrings: {
35
36
  readonly noInteractions: "No interactions detected";
36
37
  };
37
38
  export declare const i18nString: (id: string, values?: Record<string, string> | undefined) => Record<string, string>;
38
- export declare function deps(): ['UserInteractions'];
39
39
  export type INPInsightModel = InsightModel<typeof UIStrings, {
40
40
  longestInteractionEvent?: SyntheticInteractionPair;
41
41
  highPercentileInteractionEvent?: SyntheticInteractionPair;
42
42
  }>;
43
- export declare function generateInsight(parsedTrace: RequiredData<typeof deps>, context: InsightSetContext): INPInsightModel;
43
+ export declare function generateInsight(parsedTrace: Handlers.Types.ParsedTrace, context: InsightSetContext): INPInsightModel;
@@ -41,9 +41,6 @@ export const UIStrings = {
41
41
  };
42
42
  // const str_ = i18n.i18n.registerUIStrings('models/trace/insights/InteractionToNextPaint.ts', UIStrings);
43
43
  export const i18nString = (i18nId, values) => ({i18nId, values}); // i18n.i18n.getLocalizedString.bind(undefined, str_);
44
- export function deps() {
45
- return ['UserInteractions'];
46
- }
47
44
  function finalize(partialModel) {
48
45
  return {
49
46
  insightKey: "InteractionToNextPaint" /* InsightKeys.INTERACTION_TO_NEXT_PAINT */,
@@ -61,7 +58,9 @@ export function generateInsight(parsedTrace, context) {
61
58
  });
62
59
  if (!interactionEvents.length) {
63
60
  // A valid result, when there is no user interaction.
64
- return finalize({});
61
+ return finalize({
62
+ frameId: context.frameId,
63
+ });
65
64
  }
66
65
  const longestByInteractionId = new Map();
67
66
  for (const event of interactionEvents) {
@@ -79,6 +78,7 @@ export function generateInsight(parsedTrace, context) {
79
78
  // See https://source.chromium.org/chromium/chromium/src/+/main:components/page_load_metrics/browser/responsiveness_metrics_normalization.cc;l=45-59;drc=cb0f9c8b559d9c7c3cb4ca94fc1118cc015d38ad
80
79
  const highPercentileIndex = Math.min(9, Math.floor(normalizedInteractionEvents.length / 50));
81
80
  return finalize({
81
+ frameId: context.frameId,
82
82
  relatedEvents: [normalizedInteractionEvents[0]],
83
83
  longestInteractionEvent: normalizedInteractionEvents[0],
84
84
  highPercentileInteractionEvent: normalizedInteractionEvents[highPercentileIndex],
@@ -1 +1 @@
1
- {"version":3,"file":"InteractionToNextPaint.js","sourceRoot":"","sources":["../../../../../../../front_end/models/trace/insights/InteractionToNextPaint.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 The Chromium Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\nimport * as i18n from '../../../core/i18n/i18n.js';\nimport * as Helpers from '../helpers/helpers.js';\nimport type {SyntheticInteractionPair} from '../types/TraceEvents.js';\n\nimport {\n InsightCategory,\n InsightKeys,\n type InsightModel,\n type InsightSetContext,\n type PartialInsightModel,\n type RequiredData,\n} from './types.js';\n\nexport const UIStrings = {\n /**\n * @description Text to tell the user about the longest user interaction.\n */\n description:\n 'Start investigating with the longest phase. [Delays can be minimized](https://web.dev/articles/optimize-inp#optimize_interactions). To reduce processing duration, [optimize the main-thread costs](https://web.dev/articles/optimize-long-tasks), often JS.',\n /**\n * @description Title for the performance insight \"INP by phase\", which shows a breakdown of INP by phases / sections.\n */\n title: 'INP by phase',\n /**\n *@description Label used for the phase/component/stage/section of a larger duration.\n */\n phase: 'Phase',\n /**\n *@description Label used for a time duration.\n */\n duration: 'Duration',\n\n // TODO: these are repeated in InteractionBreakdown. Add a place for common strings?\n /**\n *@description Text shown next to the interaction event's input delay time in the detail view.\n */\n inputDelay: 'Input delay',\n /**\n *@description Text shown next to the interaction event's thread processing duration in the detail view.\n */\n processingDuration: 'Processing duration',\n /**\n *@description Text shown next to the interaction event's presentation delay time in the detail view.\n */\n presentationDelay: 'Presentation delay',\n /**\n * @description Text status indicating that no user interactions were detected.\n */\n noInteractions: 'No interactions detected',\n} as const;\n\nconst str_ = i18n.i18n.registerUIStrings('models/trace/insights/InteractionToNextPaint.ts', UIStrings);\nexport const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);\n\nexport function deps(): ['UserInteractions'] {\n return ['UserInteractions'];\n}\n\nexport type INPInsightModel = InsightModel<typeof UIStrings, {\n longestInteractionEvent?: SyntheticInteractionPair,\n highPercentileInteractionEvent?: SyntheticInteractionPair,\n}>;\n\nfunction finalize(partialModel: PartialInsightModel<INPInsightModel>): INPInsightModel {\n return {\n insightKey: InsightKeys.INTERACTION_TO_NEXT_PAINT,\n strings: UIStrings,\n title: i18nString(UIStrings.title),\n description: i18nString(UIStrings.description),\n category: InsightCategory.INP,\n state: partialModel.longestInteractionEvent ? 'informative' : 'pass',\n ...partialModel,\n };\n}\n\nexport function generateInsight(parsedTrace: RequiredData<typeof deps>, context: InsightSetContext): INPInsightModel {\n const interactionEvents = parsedTrace.UserInteractions.interactionEventsWithNoNesting.filter(event => {\n return Helpers.Timing.eventIsInBounds(event, context.bounds);\n });\n\n if (!interactionEvents.length) {\n // A valid result, when there is no user interaction.\n return finalize({});\n }\n\n const longestByInteractionId = new Map<number, SyntheticInteractionPair>();\n for (const event of interactionEvents) {\n const key = event.interactionId;\n const longest = longestByInteractionId.get(key);\n if (!longest || event.dur > longest.dur) {\n longestByInteractionId.set(key, event);\n }\n }\n const normalizedInteractionEvents = [...longestByInteractionId.values()];\n normalizedInteractionEvents.sort((a, b) => b.dur - a.dur);\n\n // INP is the \"nearest-rank\"/inverted_cdf 98th percentile, except Chrome only\n // keeps the 10 worst events around, so it can never be more than the 10th from\n // last array element. To keep things simpler, sort desc and pick from front.\n // See https://source.chromium.org/chromium/chromium/src/+/main:components/page_load_metrics/browser/responsiveness_metrics_normalization.cc;l=45-59;drc=cb0f9c8b559d9c7c3cb4ca94fc1118cc015d38ad\n const highPercentileIndex = Math.min(9, Math.floor(normalizedInteractionEvents.length / 50));\n\n return finalize({\n relatedEvents: [normalizedInteractionEvents[0]],\n longestInteractionEvent: normalizedInteractionEvents[0],\n highPercentileInteractionEvent: normalizedInteractionEvents[highPercentileIndex],\n });\n}\n"]}
1
+ {"version":3,"file":"InteractionToNextPaint.js","sourceRoot":"","sources":["../../../../../../../front_end/models/trace/insights/InteractionToNextPaint.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 The Chromium Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\nimport * as i18n from '../../../core/i18n/i18n.js';\nimport type * as Handlers from '../handlers/handlers.js';\nimport * as Helpers from '../helpers/helpers.js';\nimport type {SyntheticInteractionPair} from '../types/TraceEvents.js';\n\nimport {\n InsightCategory,\n InsightKeys,\n type InsightModel,\n type InsightSetContext,\n type PartialInsightModel,\n} from './types.js';\n\nexport const UIStrings = {\n /**\n * @description Text to tell the user about the longest user interaction.\n */\n description:\n 'Start investigating with the longest phase. [Delays can be minimized](https://web.dev/articles/optimize-inp#optimize_interactions). To reduce processing duration, [optimize the main-thread costs](https://web.dev/articles/optimize-long-tasks), often JS.',\n /**\n * @description Title for the performance insight \"INP by phase\", which shows a breakdown of INP by phases / sections.\n */\n title: 'INP by phase',\n /**\n *@description Label used for the phase/component/stage/section of a larger duration.\n */\n phase: 'Phase',\n /**\n *@description Label used for a time duration.\n */\n duration: 'Duration',\n\n // TODO: these are repeated in InteractionBreakdown. Add a place for common strings?\n /**\n *@description Text shown next to the interaction event's input delay time in the detail view.\n */\n inputDelay: 'Input delay',\n /**\n *@description Text shown next to the interaction event's thread processing duration in the detail view.\n */\n processingDuration: 'Processing duration',\n /**\n *@description Text shown next to the interaction event's presentation delay time in the detail view.\n */\n presentationDelay: 'Presentation delay',\n /**\n * @description Text status indicating that no user interactions were detected.\n */\n noInteractions: 'No interactions detected',\n} as const;\n\nconst str_ = i18n.i18n.registerUIStrings('models/trace/insights/InteractionToNextPaint.ts', UIStrings);\nexport const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);\n\nexport type INPInsightModel = InsightModel<typeof UIStrings, {\n longestInteractionEvent?: SyntheticInteractionPair,\n highPercentileInteractionEvent?: SyntheticInteractionPair,\n}>;\n\nfunction finalize(partialModel: PartialInsightModel<INPInsightModel>): INPInsightModel {\n return {\n insightKey: InsightKeys.INTERACTION_TO_NEXT_PAINT,\n strings: UIStrings,\n title: i18nString(UIStrings.title),\n description: i18nString(UIStrings.description),\n category: InsightCategory.INP,\n state: partialModel.longestInteractionEvent ? 'informative' : 'pass',\n ...partialModel,\n };\n}\n\nexport function generateInsight(parsedTrace: Handlers.Types.ParsedTrace, context: InsightSetContext): INPInsightModel {\n const interactionEvents = parsedTrace.UserInteractions.interactionEventsWithNoNesting.filter(event => {\n return Helpers.Timing.eventIsInBounds(event, context.bounds);\n });\n\n if (!interactionEvents.length) {\n // A valid result, when there is no user interaction.\n return finalize({\n\n frameId: context.frameId,\n });\n }\n\n const longestByInteractionId = new Map<number, SyntheticInteractionPair>();\n for (const event of interactionEvents) {\n const key = event.interactionId;\n const longest = longestByInteractionId.get(key);\n if (!longest || event.dur > longest.dur) {\n longestByInteractionId.set(key, event);\n }\n }\n const normalizedInteractionEvents = [...longestByInteractionId.values()];\n normalizedInteractionEvents.sort((a, b) => b.dur - a.dur);\n\n // INP is the \"nearest-rank\"/inverted_cdf 98th percentile, except Chrome only\n // keeps the 10 worst events around, so it can never be more than the 10th from\n // last array element. To keep things simpler, sort desc and pick from front.\n // See https://source.chromium.org/chromium/chromium/src/+/main:components/page_load_metrics/browser/responsiveness_metrics_normalization.cc;l=45-59;drc=cb0f9c8b559d9c7c3cb4ca94fc1118cc015d38ad\n const highPercentileIndex = Math.min(9, Math.floor(normalizedInteractionEvents.length / 50));\n\n return finalize({\n frameId: context.frameId,\n relatedEvents: [normalizedInteractionEvents[0]],\n longestInteractionEvent: normalizedInteractionEvents[0],\n highPercentileInteractionEvent: normalizedInteractionEvents[highPercentileIndex],\n });\n}\n"]}
@@ -1,5 +1,6 @@
1
+ import * as Handlers from '../handlers/handlers.js';
1
2
  import * as Types from '../types/types.js';
2
- import { type Checklist, type InsightModel, type InsightSetContext, type RequiredData } from './types.js';
3
+ import { type Checklist, type InsightModel, type InsightSetContext } from './types.js';
3
4
  export declare const UIStrings: {
4
5
  /**
5
6
  *@description Title of an insight that provides details about the LCP metric, and the network requests necessary to load it. Details how the LCP request was discoverable - in other words, the path necessary to load it (ex: network requests, JavaScript)
@@ -40,7 +41,7 @@ export declare const UIStrings: {
40
41
  readonly noLcpResource: "No LCP resource detected because the LCP is not an image";
41
42
  };
42
43
  export declare const i18nString: (id: string, values?: Record<string, string> | undefined) => Record<string, string>;
43
- export declare function deps(): ['NetworkRequests', 'PageLoadMetrics', 'LargestImagePaint', 'Meta'];
44
+ export declare function isLCPDiscovery(model: InsightModel): model is LCPDiscoveryInsightModel;
44
45
  export type LCPDiscoveryInsightModel = InsightModel<typeof UIStrings, {
45
46
  lcpEvent?: Types.Events.LargestContentfulPaintCandidate;
46
47
  /** The network request for the LCP image, if there was one. */
@@ -48,4 +49,4 @@ export type LCPDiscoveryInsightModel = InsightModel<typeof UIStrings, {
48
49
  earliestDiscoveryTimeTs?: Types.Timing.Micro;
49
50
  checklist?: Checklist<'priorityHinted' | 'requestDiscoverable' | 'eagerlyLoaded'>;
50
51
  }>;
51
- export declare function generateInsight(parsedTrace: RequiredData<typeof deps>, context: InsightSetContext): LCPDiscoveryInsightModel;
52
+ export declare function generateInsight(parsedTrace: Handlers.Types.ParsedTrace, context: InsightSetContext): LCPDiscoveryInsightModel;