chrome-devtools-frontend 1.0.1537860 → 1.0.1538310

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 (31) hide show
  1. package/.env.template +3 -2
  2. package/eslint.config.mjs +151 -149
  3. package/front_end/core/host/AidaClient.ts +1 -0
  4. package/front_end/core/host/UserMetrics.ts +3 -1
  5. package/front_end/core/root/Runtime.ts +8 -0
  6. package/front_end/models/ai_code_generation/AiCodeGeneration.ts +151 -0
  7. package/front_end/models/ai_code_generation/ai_code_generation.ts +6 -0
  8. package/front_end/models/ai_code_generation/debug.ts +30 -0
  9. package/front_end/panels/application/PreloadingTreeElement.ts +10 -2
  10. package/front_end/panels/application/components/OriginTrialTreeView.ts +97 -129
  11. package/front_end/panels/application/components/originTrialTreeView.css +37 -7
  12. package/front_end/panels/application/preloading/components/PreloadingString.ts +13 -11
  13. package/front_end/panels/emulation/components/DeviceSizeInputElement.ts +1 -0
  14. package/front_end/panels/network/NetworkItemView.ts +1 -1
  15. package/front_end/panels/network/NetworkWaterfallColumn.ts +5 -6
  16. package/front_end/panels/network/RequestTimingView.ts +404 -348
  17. package/front_end/panels/network/networkTimingTable.css +22 -2
  18. package/front_end/panels/timeline/components/NetworkRequestTooltip.ts +42 -3
  19. package/front_end/panels/timeline/components/networkRequestTooltip.css +19 -0
  20. package/front_end/ui/components/adorners/Adorner.ts +1 -0
  21. package/front_end/ui/components/icon_button/IconButton.ts +1 -0
  22. package/front_end/ui/components/settings/SettingCheckbox.ts +1 -0
  23. package/front_end/ui/legacy/Treeoutline.ts +15 -0
  24. package/front_end/ui/legacy/UIUtils.ts +3 -0
  25. package/front_end/ui/legacy/Widget.ts +6 -4
  26. package/front_end/ui/legacy/XLink.ts +1 -0
  27. package/front_end/ui/legacy/components/inline_editor/Swatches.ts +1 -0
  28. package/front_end/ui/legacy/components/perf_ui/BrickBreaker.ts +1 -0
  29. package/front_end/ui/legacy/popover.css +12 -11
  30. package/package.json +1 -1
  31. package/front_end/panels/application/components/badge.css +0 -33
@@ -7,6 +7,7 @@
7
7
  import * as Common from '../../core/common/common.js';
8
8
  import * as Host from '../../core/host/host.js';
9
9
  import * as i18n from '../../core/i18n/i18n.js';
10
+ import * as Platform from '../../core/platform/platform.js';
10
11
  import * as SDK from '../../core/sdk/sdk.js';
11
12
  import * as Protocol from '../../generated/protocol.js';
12
13
  import * as Logs from '../../models/logs/logs.js';
@@ -14,10 +15,12 @@ import * as NetworkTimeCalculator from '../../models/network_time_calculator/net
14
15
  import * as uiI18n from '../../ui/i18n/i18n.js';
15
16
  import * as ObjectUI from '../../ui/legacy/components/object_ui/object_ui.js';
16
17
  import * as UI from '../../ui/legacy/legacy.js';
18
+ import {Directives, html, type LitTemplate, nothing, render} from '../../ui/lit/lit.js';
17
19
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
18
20
 
19
21
  import networkingTimingTableStyles from './networkTimingTable.css.js';
20
22
 
23
+ const {repeat, classMap, ifDefined} = Directives;
21
24
  const UIStrings = {
22
25
  /**
23
26
  * @description Text used to label the time taken to receive an HTTP/2 Push message.
@@ -124,7 +127,7 @@ const UIStrings = {
124
127
  /**
125
128
  * @description Text in Request Timing View of the Network panel
126
129
  */
127
- requestresponse: 'Request/Response',
130
+ requestResponse: 'Request/Response',
128
131
  /**
129
132
  * @description Text of a DOM element in Request Timing View of the Network panel
130
133
  */
@@ -212,286 +215,397 @@ const UIStrings = {
212
215
  * @example {network} PH1
213
216
  */
214
217
  routerActualSource: 'Actual source: {PH1}',
218
+ /**
219
+ * @description Cell title in Network Data Grid Node of the Network panel
220
+ * @example {Fast 4G} PH1
221
+ */
222
+ wasThrottled: 'Request was throttled ({PH1})',
215
223
  } as const;
216
224
  const str_ = i18n.i18n.registerUIStrings('panels/network/RequestTimingView.ts', UIStrings);
217
225
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
218
- export class RequestTimingView extends UI.Widget.VBox {
219
- private request: SDK.NetworkRequest.NetworkRequest;
220
- private calculator: NetworkTimeCalculator.NetworkTimeCalculator;
221
- private lastMinimumBoundary: number;
222
- private tableElement?: Element;
223
- constructor(request: SDK.NetworkRequest.NetworkRequest, calculator: NetworkTimeCalculator.NetworkTimeCalculator) {
224
- super();
225
- this.registerRequiredCSS(networkingTimingTableStyles);
226
- this.element.classList.add('resource-timing-view');
227
-
228
- this.request = request;
229
- this.calculator = calculator;
230
- this.lastMinimumBoundary = -1;
226
+
227
+ function timeRangeTitle(name: NetworkTimeCalculator.RequestTimeRangeNames): string {
228
+ switch (name) {
229
+ case NetworkTimeCalculator.RequestTimeRangeNames.PUSH:
230
+ return i18nString(UIStrings.receivingPush);
231
+ case NetworkTimeCalculator.RequestTimeRangeNames.QUEUEING:
232
+ return i18nString(UIStrings.queueing);
233
+ case NetworkTimeCalculator.RequestTimeRangeNames.BLOCKING:
234
+ return i18nString(UIStrings.stalled);
235
+ case NetworkTimeCalculator.RequestTimeRangeNames.CONNECTING:
236
+ return i18nString(UIStrings.initialConnection);
237
+ case NetworkTimeCalculator.RequestTimeRangeNames.DNS:
238
+ return i18nString(UIStrings.dnsLookup);
239
+ case NetworkTimeCalculator.RequestTimeRangeNames.PROXY:
240
+ return i18nString(UIStrings.proxyNegotiation);
241
+ case NetworkTimeCalculator.RequestTimeRangeNames.RECEIVING_PUSH:
242
+ return i18nString(UIStrings.readingPush);
243
+ case NetworkTimeCalculator.RequestTimeRangeNames.RECEIVING:
244
+ return i18nString(UIStrings.contentDownload);
245
+ case NetworkTimeCalculator.RequestTimeRangeNames.SENDING:
246
+ return i18nString(UIStrings.requestSent);
247
+ case NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER:
248
+ return i18nString(UIStrings.requestToServiceworker);
249
+ case NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_PREPARATION:
250
+ return i18nString(UIStrings.startup);
251
+ case NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_ROUTER_EVALUATION:
252
+ return i18nString(UIStrings.routerEvaluation);
253
+ case NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_CACHE_LOOKUP:
254
+ return i18nString(UIStrings.routerCacheLookup);
255
+ case NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_RESPOND_WITH:
256
+ return i18nString(UIStrings.respondwith);
257
+ case NetworkTimeCalculator.RequestTimeRangeNames.SSL:
258
+ return i18nString(UIStrings.ssl);
259
+ case NetworkTimeCalculator.RequestTimeRangeNames.TOTAL:
260
+ return i18nString(UIStrings.total);
261
+ case NetworkTimeCalculator.RequestTimeRangeNames.WAITING:
262
+ return i18nString(UIStrings.waitingTtfb);
263
+ default:
264
+ return name;
231
265
  }
266
+ }
232
267
 
233
- private static timeRangeTitle(name: NetworkTimeCalculator.RequestTimeRangeNames): string {
234
- switch (name) {
235
- case NetworkTimeCalculator.RequestTimeRangeNames.PUSH:
236
- return i18nString(UIStrings.receivingPush);
237
- case NetworkTimeCalculator.RequestTimeRangeNames.QUEUEING:
238
- return i18nString(UIStrings.queueing);
239
- case NetworkTimeCalculator.RequestTimeRangeNames.BLOCKING:
240
- return i18nString(UIStrings.stalled);
241
- case NetworkTimeCalculator.RequestTimeRangeNames.CONNECTING:
242
- return i18nString(UIStrings.initialConnection);
243
- case NetworkTimeCalculator.RequestTimeRangeNames.DNS:
244
- return i18nString(UIStrings.dnsLookup);
245
- case NetworkTimeCalculator.RequestTimeRangeNames.PROXY:
246
- return i18nString(UIStrings.proxyNegotiation);
247
- case NetworkTimeCalculator.RequestTimeRangeNames.RECEIVING_PUSH:
248
- return i18nString(UIStrings.readingPush);
249
- case NetworkTimeCalculator.RequestTimeRangeNames.RECEIVING:
250
- return i18nString(UIStrings.contentDownload);
251
- case NetworkTimeCalculator.RequestTimeRangeNames.SENDING:
252
- return i18nString(UIStrings.requestSent);
253
- case NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER:
254
- return i18nString(UIStrings.requestToServiceworker);
255
- case NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_PREPARATION:
256
- return i18nString(UIStrings.startup);
257
- case NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_ROUTER_EVALUATION:
258
- return i18nString(UIStrings.routerEvaluation);
259
- case NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_CACHE_LOOKUP:
260
- return i18nString(UIStrings.routerCacheLookup);
261
- case NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_RESPOND_WITH:
262
- return i18nString(UIStrings.respondwith);
263
- case NetworkTimeCalculator.RequestTimeRangeNames.SSL:
264
- return i18nString(UIStrings.ssl);
265
- case NetworkTimeCalculator.RequestTimeRangeNames.TOTAL:
266
- return i18nString(UIStrings.total);
267
- case NetworkTimeCalculator.RequestTimeRangeNames.WAITING:
268
- return i18nString(UIStrings.waitingTtfb);
269
- default:
270
- return name;
271
- }
268
+ function groupHeader(name: NetworkTimeCalculator.RequestTimeRangeNames): string {
269
+ if (name === NetworkTimeCalculator.RequestTimeRangeNames.PUSH) {
270
+ return i18nString(UIStrings.serverPush);
272
271
  }
272
+ if (name === NetworkTimeCalculator.RequestTimeRangeNames.QUEUEING) {
273
+ return i18nString(UIStrings.resourceScheduling);
274
+ }
275
+ if (NetworkTimeCalculator.ConnectionSetupRangeNames.has(name)) {
276
+ return i18nString(UIStrings.connectionStart);
277
+ }
278
+ if (NetworkTimeCalculator.ServiceWorkerRangeNames.has(name)) {
279
+ return 'Service Worker';
280
+ }
281
+ return i18nString(UIStrings.requestResponse);
282
+ }
273
283
 
274
- static createTimingTable(
275
- request: SDK.NetworkRequest.NetworkRequest, calculator: NetworkTimeCalculator.NetworkTimeCalculator): Element {
276
- const tableElement = document.createElement('table');
277
- tableElement.classList.add('network-timing-table');
278
- tableElement.setAttribute('jslog', `${VisualLogging.pane('timing').track({resize: true})}`);
279
- const colgroup = tableElement.createChild('colgroup');
280
- colgroup.createChild('col', 'labels');
281
- colgroup.createChild('col', 'bars');
282
- colgroup.createChild('col', 'duration');
283
-
284
- const timeRanges = NetworkTimeCalculator.calculateRequestTimeRanges(request, calculator.minimumBoundary());
285
- const startTime = timeRanges.map(r => r.start).reduce((a, b) => Math.min(a, b));
286
- const endTime = timeRanges.map(r => r.end).reduce((a, b) => Math.max(a, b));
287
- const scale = 100 / (endTime - startTime);
288
-
289
- let connectionHeader;
290
- let serviceworkerHeader;
291
- let dataHeader;
292
- let queueingHeader;
293
- let totalDuration = 0;
294
-
295
- const startTimeHeader = tableElement.createChild('thead', 'network-timing-start');
296
- const tableHeaderRow = startTimeHeader.createChild('tr');
297
- const activityHeaderCell = tableHeaderRow.createChild('th');
298
- activityHeaderCell.createChild('span', 'network-timing-hidden-header').textContent = i18nString(UIStrings.label);
299
- activityHeaderCell.scope = 'col';
300
- const waterfallHeaderCell = tableHeaderRow.createChild('th');
301
- waterfallHeaderCell.createChild('span', 'network-timing-hidden-header').textContent =
302
- i18nString(UIStrings.waterfall);
303
- waterfallHeaderCell.scope = 'col';
304
- const durationHeaderCell = tableHeaderRow.createChild('th');
305
- durationHeaderCell.createChild('span', 'network-timing-hidden-header').textContent = i18nString(UIStrings.duration);
306
- durationHeaderCell.scope = 'col';
307
-
308
- const queuedCell = startTimeHeader.createChild('tr').createChild('td');
309
- const startedCell = startTimeHeader.createChild('tr').createChild('td');
310
- queuedCell.colSpan = startedCell.colSpan = 3;
311
- UI.UIUtils.createTextChild(
312
- queuedCell, i18nString(UIStrings.queuedAtS, {PH1: calculator.formatValue(request.issueTime(), 2)}));
313
- UI.UIUtils.createTextChild(
314
- startedCell, i18nString(UIStrings.startedAtS, {PH1: calculator.formatValue(request.startTime, 2)}));
315
-
316
- let right;
317
- for (let i = 0; i < timeRanges.length; ++i) {
318
- const range = timeRanges[i];
319
- const rangeName = range.name;
320
- if (rangeName === NetworkTimeCalculator.RequestTimeRangeNames.TOTAL) {
321
- totalDuration = range.end - range.start;
322
- continue;
323
- }
324
- if (rangeName === NetworkTimeCalculator.RequestTimeRangeNames.PUSH) {
325
- createHeader(i18nString(UIStrings.serverPush));
326
- } else if (rangeName === NetworkTimeCalculator.RequestTimeRangeNames.QUEUEING) {
327
- if (!queueingHeader) {
328
- queueingHeader = createHeader(i18nString(UIStrings.resourceScheduling));
329
- }
330
- } else if (NetworkTimeCalculator.ConnectionSetupRangeNames.has(rangeName)) {
331
- if (!connectionHeader) {
332
- connectionHeader = createHeader(i18nString(UIStrings.connectionStart));
333
- }
334
- } else if (NetworkTimeCalculator.ServiceWorkerRangeNames.has(rangeName)) {
335
- if (!serviceworkerHeader) {
336
- serviceworkerHeader = createHeader('Service Worker');
337
- }
338
- } else if (!dataHeader) {
339
- dataHeader = createHeader(i18nString(UIStrings.requestresponse));
340
- }
341
-
342
- const left = (scale * (range.start - startTime));
343
- right = (scale * (endTime - range.end));
344
- const duration = range.end - range.start;
345
-
346
- const tr = tableElement.createChild('tr');
347
- const timingBarTitleElement = tr.createChild('td');
348
- UI.UIUtils.createTextChild(timingBarTitleElement, RequestTimingView.timeRangeTitle(rangeName));
349
-
350
- const row = tr.createChild('td').createChild('div', 'network-timing-row');
351
- const bar = row.createChild('span', 'network-timing-bar ' + rangeName);
352
- bar.style.left = left + '%';
353
- bar.style.right = right + '%';
354
- bar.textContent = '\u200B'; // Important for 0-time items to have 0 width.
355
- UI.ARIAUtils.setLabel(row, i18nString(UIStrings.startedAtS, {PH1: calculator.formatValue(range.start, 2)}));
356
- const label = tr.createChild('td').createChild('div', 'network-timing-bar-title');
357
- label.textContent = i18n.TimeUtilities.secondsToString(duration, true);
358
-
359
- if (range.name === 'serviceworker-respondwith') {
360
- timingBarTitleElement.classList.add('network-fetch-timing-bar-clickable');
361
- tableElement.createChild('tr', 'network-fetch-timing-bar-details');
362
-
363
- timingBarTitleElement.setAttribute('tabindex', '0');
364
- timingBarTitleElement.setAttribute('role', 'switch');
365
- UI.ARIAUtils.setChecked(timingBarTitleElement, false);
366
- }
367
-
368
- if (range.name === 'serviceworker-routerevaluation') {
369
- timingBarTitleElement.classList.add('network-fetch-timing-bar-clickable');
370
- tableElement.createChild('tr', 'router-evaluation-timing-bar-details');
371
-
372
- timingBarTitleElement.setAttribute('tabindex', '0');
373
- timingBarTitleElement.setAttribute('role', 'switch');
374
- UI.ARIAUtils.setChecked(timingBarTitleElement, false);
375
- }
376
- }
284
+ function getLocalizedResponseSourceForCode(swResponseSource: Protocol.Network.ServiceWorkerResponseSource):
285
+ Common.UIString.LocalizedString {
286
+ switch (swResponseSource) {
287
+ case Protocol.Network.ServiceWorkerResponseSource.CacheStorage:
288
+ return i18nString(UIStrings.serviceworkerCacheStorage);
289
+ case Protocol.Network.ServiceWorkerResponseSource.HttpCache:
290
+ return i18nString(UIStrings.fromHttpCache);
291
+ case Protocol.Network.ServiceWorkerResponseSource.Network:
292
+ return i18nString(UIStrings.networkFetch);
293
+ default:
294
+ return i18nString(UIStrings.fallbackCode);
295
+ }
296
+ }
377
297
 
378
- if (!request.finished && !request.preserved) {
379
- const cell = tableElement.createChild('tr').createChild('td', 'caution');
380
- cell.colSpan = 3;
381
- UI.UIUtils.createTextChild(cell, i18nString(UIStrings.cautionRequestIsNotFinishedYet));
382
- }
298
+ interface ViewInput {
299
+ requestUnfinished: boolean;
300
+ requestStartTime: number;
301
+ requestIssueTime: number;
302
+ totalDuration: number;
303
+ startTime: number;
304
+ endTime: number;
305
+ timeRanges: NetworkTimeCalculator.RequestTimeRange[];
306
+ calculator: NetworkTimeCalculator.NetworkTimeCalculator;
307
+ serverTimings: SDK.ServerTiming.ServerTiming[];
308
+ fetchDetails?: UI.TreeOutline.TreeOutlineInShadow;
309
+ routerDetails?: UI.TreeOutline.TreeOutlineInShadow;
310
+ wasThrottled?: SDK.NetworkManager.Conditions;
311
+ }
383
312
 
384
- const footer = tableElement.createChild('tr', 'network-timing-footer');
385
- const note = footer.createChild('td');
386
- note.colSpan = 1;
387
- const explanationLink = UI.XLink.XLink.create(
388
- 'https://developer.chrome.com/docs/devtools/network/reference/#timing-explanation',
389
- i18nString(UIStrings.explanation), undefined, undefined, 'explanation');
390
- note.appendChild(explanationLink);
391
- footer.createChild('td');
392
- UI.UIUtils.createTextChild(footer.createChild('td'), i18n.TimeUtilities.secondsToString(totalDuration, true));
393
-
394
- const serverTimings = request.serverTimings;
395
-
396
- const lastTimingRightEdge = right === undefined ? 100 : right;
397
-
398
- const breakElement = tableElement.createChild('tr', 'network-timing-table-header').createChild('td');
399
- breakElement.colSpan = 3;
400
- breakElement.createChild('hr', 'break');
401
-
402
- const serverHeader = tableElement.createChild('tr', 'network-timing-table-header');
403
- UI.UIUtils.createTextChild(serverHeader.createChild('td'), i18nString(UIStrings.serverTiming));
404
- serverHeader.createChild('td');
405
- UI.UIUtils.createTextChild(serverHeader.createChild('td'), i18nString(UIStrings.time));
406
-
407
- if (!serverTimings) {
408
- const informationRow = tableElement.createChild('tr');
409
- const information = informationRow.createChild('td');
410
- information.colSpan = 3;
411
-
412
- const link = UI.XLink.XLink.create(
413
- 'https://web.dev/custom-metrics/#server-timing-api', i18nString(UIStrings.theServerTimingApi), undefined,
414
- undefined, 'server-timing-api');
415
- information.appendChild(
416
- uiI18n.getFormatLocalizedString(str_, UIStrings.duringDevelopmentYouCanUseSToAdd, {PH1: link}));
417
-
418
- return tableElement;
313
+ type View = (input: ViewInput, output: object, target: HTMLElement) => void;
314
+ export const DEFAULT_VIEW: View = (input, output, target) => {
315
+ const scale = 100 / (input.endTime - input.startTime);
316
+ const isClickable = (range: NetworkTimeCalculator.RequestTimeRange): boolean =>
317
+ range.name === 'serviceworker-respondwith' || range.name === 'serviceworker-routerevaluation';
318
+ const addServerTiming = (serverTiming: SDK.ServerTiming.ServerTiming): LitTemplate => {
319
+ const colorGenerator =
320
+ new Common.Color.Generator({min: 0, max: 360, count: 36}, {min: 50, max: 80, count: undefined}, 80);
321
+ const isTotal = serverTiming.metric.toLowerCase() === 'total';
322
+ const metricDesc = [serverTiming.metric, serverTiming.description].filter(Boolean).join(' — ');
323
+ const left =
324
+ serverTiming.value === null ? -1 : scale * (input.endTime - input.startTime - (serverTiming.value / 1000));
325
+ const lastRange =
326
+ input.timeRanges.findLast(range => range.name !== NetworkTimeCalculator.RequestTimeRangeNames.TOTAL);
327
+ const lastTimingRightEdge = lastRange ? (scale * (input.endTime - lastRange.end)) : 100;
328
+
329
+ const classes = classMap({
330
+ ['network-timing-footer']: isTotal,
331
+ ['server-timing-row']: !isTotal,
332
+ // Mark entries from a bespoke format
333
+ ['synthetic']: serverTiming.metric.startsWith('(c'),
334
+ });
335
+
336
+ // clang-format off
337
+ return html`
338
+ <tr class=${classes}>
339
+ <td title=${metricDesc} class=network-timing-metric>
340
+ ${metricDesc}
341
+ </td>
342
+ ${serverTiming.value === null ? nothing : html`
343
+ <td class=server-timing-cell--value-bar>
344
+ <div class=network-timing-row>
345
+ ${left < 0 // don't chart values too big or too small
346
+ ? nothing
347
+ : html`<span
348
+ class="network-timing-bar server-timing"
349
+ data-background=${ifDefined(isTotal ? undefined : colorGenerator.colorForID(serverTiming.metric))}
350
+ data-left=${left}
351
+ data-right=${lastTimingRightEdge}>${'\u200B'}</span>`}
352
+ </div>
353
+ </td>
354
+ <td class=server-timing-cell--value-text>
355
+ <div class=network-timing-bar-title>
356
+ ${i18n.TimeUtilities.millisToString(serverTiming.value, true)}
357
+ </div>
358
+ </td>
359
+ `}
360
+ </tr>`;
361
+ // clang-format on
362
+ };
363
+
364
+ const onActivate = (e: KeyboardEvent|MouseEvent): void => {
365
+ if ('key' in e && !Platform.KeyboardUtilities.isEnterOrSpaceKey(e)) {
366
+ return;
419
367
  }
368
+ const target = e.target as Element | null;
369
+ if (!target?.classList.contains('network-fetch-timing-bar-clickable')) {
370
+ return;
371
+ }
372
+ const isChecked = target.ariaChecked === 'false';
373
+ target.ariaChecked = isChecked ? 'true' : 'false';
374
+ if (!isChecked) {
375
+ Host.userMetrics.actionTaken(Host.UserMetrics.Action.NetworkPanelServiceWorkerRespondWith);
376
+ }
377
+ };
378
+
379
+ const throttledRequestTitle = input.wasThrottled ?
380
+ i18nString(
381
+ UIStrings.wasThrottled,
382
+ {PH1: typeof input.wasThrottled.title === 'string' ? input.wasThrottled.title : input.wasThrottled.title()}) :
383
+ undefined;
384
+
385
+ const classes = classMap({
386
+ ['network-timing-table']: true,
387
+ ['resource-timing-table']: true,
388
+ });
389
+
390
+ const timeRangeGroups: Array<{name: string, ranges: NetworkTimeCalculator.RequestTimeRange[]}> = [];
391
+ for (const range of input.timeRanges) {
392
+ if (range.name === NetworkTimeCalculator.RequestTimeRangeNames.TOTAL) {
393
+ continue;
394
+ }
395
+ const groupName = groupHeader(range.name);
396
+ const tail = timeRangeGroups.at(-1);
397
+ if (tail?.name !== groupName) {
398
+ timeRangeGroups.push({name: groupName, ranges: [range]});
399
+ } else {
400
+ tail.ranges.push(range);
401
+ }
402
+ }
420
403
 
421
- serverTimings.filter(item => item.metric.toLowerCase() !== 'total')
422
- .forEach(item => addServerTiming(item, lastTimingRightEdge));
423
- serverTimings.filter(item => item.metric.toLowerCase() === 'total')
424
- .forEach(item => addServerTiming(item, lastTimingRightEdge));
425
-
426
- return tableElement;
427
-
428
- function addServerTiming(serverTiming: SDK.ServerTiming.ServerTiming, right: number): void {
429
- const colorGenerator =
430
- new Common.Color.Generator({min: 0, max: 360, count: 36}, {min: 50, max: 80, count: undefined}, 80);
431
- const isTotal = serverTiming.metric.toLowerCase() === 'total';
432
- const tr = tableElement.createChild('tr', isTotal ? 'network-timing-footer' : 'server-timing-row');
433
- const metricEl = tr.createChild('td', 'network-timing-metric');
434
- const metricDesc = [serverTiming.metric, serverTiming.description].filter(Boolean).join(' — ');
404
+ render(
405
+ // clang-format off
406
+ html`<style>${networkingTimingTableStyles}</style>
407
+ <table
408
+ class=${classes}
409
+ jslog=${VisualLogging.pane('timing').track({
410
+ resize: true
411
+ })}>
412
+ <colgroup>
413
+ <col class=labels></col>
414
+ <col class=bars> </col>
415
+ <col class=duration></col>
416
+ </colgroup>
417
+ <thead class=network-timing-start>
418
+ <tr>
419
+ <th scope=col>
420
+ <span class=network-timing-hidden-header>${i18nString(UIStrings.label)}</span>
421
+ </th>
422
+ <th scope=col>
423
+ <span class=network-timing-hidden-header>${i18nString(UIStrings.waterfall)}</span>
424
+ </th>
425
+ <th scope=col>
426
+ <span class=network-timing-hidden-header>${i18nString(UIStrings.duration)}</span>
427
+ </th>
428
+ </tr>
429
+ <tr>
430
+ <td colspan = 3>
431
+ ${i18nString(UIStrings.queuedAtS, {PH1: input.calculator.formatValue(input.requestIssueTime, 2)})}
432
+ </td>
433
+ </tr>
434
+ <tr>
435
+ <td colspan=3>
436
+ ${i18nString(UIStrings.startedAtS, {PH1: input.calculator.formatValue(input.requestStartTime, 2)})}
437
+ </td>
438
+ </tr>
439
+ </thead>
440
+ ${timeRangeGroups.map(group => html`
441
+ <tr class=network-timing-table-header>
442
+ <td role=heading aria-level=2>
443
+ ${group.name}
444
+ </td>
445
+ <td></td>
446
+ <td>${i18nString(UIStrings.durationC)}</td>
447
+ </tr>
448
+ ${repeat(group.ranges, range => html`
449
+ <tr>
450
+ ${isClickable(range) ? html`<td
451
+ tabindex=0
452
+ role=switch
453
+ aria-checked=false
454
+ @click=${onActivate}
455
+ @keydown=${onActivate}
456
+ class=network-fetch-timing-bar-clickable>
457
+ ${timeRangeTitle(range.name)}
458
+ </td>`
459
+ : html`<td>
460
+ ${timeRangeTitle(range.name)}
461
+ </td>`}
462
+ <td>
463
+ <div
464
+ class=network-timing-row
465
+ aria-label=${i18nString(UIStrings.startedAtS, {PH1: input.calculator.formatValue(range.start, 2)})}>
466
+ <span
467
+ class="network-timing-bar ${range.name}"
468
+ data-left=${scale * (range.start - input.startTime)}
469
+ data-right=${scale * (input.endTime - range.end)}>${'\u200B'}</span>
470
+ </div>
471
+ </td>
472
+ <td>
473
+ <div class=network-timing-bar-title>
474
+ ${i18n.TimeUtilities.secondsToString(range.end - range.start, true)}
475
+ </div>
476
+ </td>
477
+ </tr>
478
+ ${range.name === 'serviceworker-respondwith' && input.fetchDetails ? html`
479
+ <tr class="network-fetch-timing-bar-details network-fetch-timing-bar-details-collapsed">
480
+ ${input.fetchDetails.element}
481
+ </tr>`
482
+ : nothing}
483
+ ${range.name === 'serviceworker-routerevaluation' && input.routerDetails ? html`
484
+ <tr class="router-evaluation-timing-bar-details network-fetch-timing-bar-details-collapsed">
485
+ ${input.routerDetails.element}
486
+ </tr>`
487
+ : nothing}
488
+ `)}
489
+ `)}
490
+ ${input.requestUnfinished ? html`
491
+ <tr>
492
+ <td class=caution colspan=3>
493
+ ${i18nString(UIStrings.cautionRequestIsNotFinishedYet)}
494
+ </td>
495
+ </tr>` : nothing}
496
+ <tr class=network-timing-footer>
497
+ <td colspan=1>
498
+ <x-link
499
+ href="https://developer.chrome.com/docs/devtools/network/reference/#timing-explanation"
500
+ class=devtools-link
501
+ jslog=${VisualLogging.link().track({click: true, keydown:'Enter|Space'}).context('explanation')}>
502
+ ${i18nString(UIStrings.explanation)}
503
+ </x-link>
504
+ <td></td>
505
+ <td class=${input.wasThrottled ? 'throttled' : ''} title=${ifDefined(throttledRequestTitle)}>
506
+ ${input.wasThrottled ? html` <devtools-icon name=watch ></devtools-icon>` : nothing}
507
+ ${i18n.TimeUtilities.secondsToString(input.totalDuration, true)}
508
+ </td>
509
+ </tr>
510
+ <tr class=network-timing-table-header>
511
+ <td colspan=3>
512
+ <hr class=break />
513
+ </td>
514
+ </tr>
515
+ <tr class=network-timing-table-header>
516
+ <td>${i18nString(UIStrings.serverTiming)}</td>
517
+ <td></td>
518
+ <td>${i18nString(UIStrings.time)}</td>
519
+ </tr>
520
+ ${repeat(input.serverTimings.filter(item => item.metric.toLowerCase() !== 'total'), addServerTiming)}
521
+ ${repeat(input.serverTimings.filter(item => item.metric.toLowerCase() === 'total'), addServerTiming)}
522
+ ${input.serverTimings.length === 0 ? html`
523
+ <tr>
524
+ <td colspan=3>
525
+ ${uiI18n.getFormatLocalizedString(str_, UIStrings.duringDevelopmentYouCanUseSToAdd, {PH1:
526
+ UI.XLink.XLink.create(
527
+ 'https://web.dev/custom-metrics/#server-timing-api',
528
+ i18nString(UIStrings.theServerTimingApi),
529
+ undefined,
530
+ undefined,
531
+ 'server-timing-api')})}
532
+ </td>
533
+ </tr>` : nothing}
534
+ </table>`,
535
+ // clang-format on
536
+ target);
537
+ };
435
538
 
436
- // Mark entries from a bespoke format
437
- if (serverTiming.metric.startsWith('(c')) {
438
- tr.classList.add('synthetic');
439
- }
440
-
441
- UI.UIUtils.createTextChild(metricEl, metricDesc);
442
- UI.Tooltip.Tooltip.install(metricEl, metricDesc);
443
-
444
- const row = tr.createChild('td', 'server-timing-cell--value-bar').createChild('div', 'network-timing-row');
445
-
446
- if (serverTiming.value === null) {
447
- return;
448
- }
449
- const left = scale * (endTime - startTime - (serverTiming.value / 1000));
450
- if (left >= 0) { // don't chart values too big or too small
451
- const bar = row.createChild('span', 'network-timing-bar server-timing');
452
- bar.style.left = left + '%';
453
- bar.style.right = right + '%';
454
- bar.textContent = '\u200B'; // Important for 0-time items to have 0 width.
455
- if (!isTotal) {
456
- bar.style.backgroundColor = colorGenerator.colorForID(serverTiming.metric);
457
- }
458
- }
459
- const label =
460
- tr.createChild('td', 'server-timing-cell--value-text').createChild('div', 'network-timing-bar-title');
461
- label.textContent = i18n.TimeUtilities.millisToString(serverTiming.value, true);
462
- }
539
+ export class RequestTimingView extends UI.Widget.VBox {
540
+ #request?: SDK.NetworkRequest.NetworkRequest;
541
+ #calculator?: NetworkTimeCalculator.NetworkTimeCalculator;
542
+ #lastMinimumBoundary = -1;
543
+ readonly #view: View;
544
+ constructor(target?: HTMLElement, view = DEFAULT_VIEW) {
545
+ super(target, {classes: ['resource-timing-view']});
546
+ this.#view = view;
547
+ }
463
548
 
464
- function createHeader(title: string): Element {
465
- const dataHeader = tableElement.createChild('tr', 'network-timing-table-header');
466
- const headerCell = dataHeader.createChild('td');
467
- UI.UIUtils.createTextChild(headerCell, title);
468
- UI.ARIAUtils.markAsHeading(headerCell, 2);
469
- UI.UIUtils.createTextChild(dataHeader.createChild('td'), '');
470
- UI.UIUtils.createTextChild(dataHeader.createChild('td'), i18nString(UIStrings.durationC));
471
- return dataHeader;
472
- }
549
+ static create(request: SDK.NetworkRequest.NetworkRequest, calculator: NetworkTimeCalculator.NetworkTimeCalculator):
550
+ RequestTimingView {
551
+ const view = new RequestTimingView();
552
+ view.request = request;
553
+ view.calculator = calculator;
554
+ view.requestUpdate();
555
+ return view;
473
556
  }
474
557
 
475
- private constructFetchDetailsView(): void {
476
- if (!this.tableElement) {
558
+ override performUpdate(): void {
559
+ if (!this.#request || !this.#calculator) {
477
560
  return;
478
561
  }
562
+ const timeRanges =
563
+ NetworkTimeCalculator.calculateRequestTimeRanges(this.#request, this.#calculator.minimumBoundary());
564
+ const startTime = timeRanges.map(r => r.start).reduce((a, b) => Math.min(a, b));
565
+ const endTime = timeRanges.map(r => r.end).reduce((a, b) => Math.max(a, b));
566
+ const total = timeRanges.findLast(range => range.name === NetworkTimeCalculator.RequestTimeRangeNames.TOTAL);
567
+ const totalDuration = total ? total?.end - total?.start : 0;
568
+ const conditions = SDK.NetworkManager.MultitargetNetworkManager.instance().appliedRequestConditions(this.#request);
569
+
570
+ const input: ViewInput = {
571
+ startTime,
572
+ endTime,
573
+ totalDuration,
574
+ serverTimings: this.#request.serverTimings ?? [],
575
+ calculator: this.#calculator,
576
+ requestStartTime: this.#request.startTime,
577
+ requestIssueTime: this.#request.issueTime(),
578
+ requestUnfinished: false,
579
+ fetchDetails: this.#fetchDetailsTree(),
580
+ routerDetails: this.#routerDetailsTree(),
581
+ wasThrottled: conditions?.urlPattern ? conditions.conditions : undefined,
582
+ timeRanges,
583
+ };
584
+ this.#view(input, {}, this.contentElement);
585
+ }
479
586
 
480
- const document = this.tableElement.ownerDocument;
481
- const fetchDetailsElement = document.querySelector('.network-fetch-timing-bar-details');
482
-
483
- if (!fetchDetailsElement) {
587
+ private onToggleFetchDetails(fetchDetailsElement: Element, event: Event): void {
588
+ if (!event.target) {
484
589
  return;
485
590
  }
486
591
 
487
- fetchDetailsElement.classList.add('network-fetch-timing-bar-details-collapsed');
592
+ const target = (event.target as Element);
593
+ if (target.classList.contains('network-fetch-timing-bar-clickable')) {
594
+ const expanded = target.getAttribute('aria-checked') === 'true';
595
+ target.setAttribute('aria-checked', String(!expanded));
488
596
 
489
- self.onInvokeElement(this.tableElement, this.onToggleFetchDetails.bind(this, fetchDetailsElement));
597
+ fetchDetailsElement.classList.toggle('network-fetch-timing-bar-details-collapsed');
598
+ fetchDetailsElement.classList.toggle('network-fetch-timing-bar-details-expanded');
599
+ }
600
+ }
490
601
 
602
+ #fetchDetailsTree(): UI.TreeOutline.TreeOutlineInShadow|undefined {
603
+ if (!this.#request?.fetchedViaServiceWorker) {
604
+ return undefined;
605
+ }
491
606
  const detailsView = new UI.TreeOutline.TreeOutlineInShadow();
492
- fetchDetailsElement.appendChild(detailsView.element);
493
607
 
494
- const origRequest = Logs.NetworkLog.NetworkLog.instance().originalRequestForURL(this.request.url());
608
+ const origRequest = Logs.NetworkLog.NetworkLog.instance().originalRequestForURL(this.#request.url());
495
609
  if (origRequest) {
496
610
  const requestObject = SDK.RemoteObject.RemoteObject.fromLocalObject(origRequest);
497
611
  const requestTreeElement = new ObjectUI.ObjectPropertiesSection.RootElement(requestObject);
@@ -499,7 +613,7 @@ export class RequestTimingView extends UI.Widget.VBox {
499
613
  detailsView.appendChild(requestTreeElement);
500
614
  }
501
615
 
502
- const response = Logs.NetworkLog.NetworkLog.instance().originalResponseForURL(this.request.url());
616
+ const response = Logs.NetworkLog.NetworkLog.instance().originalResponseForURL(this.#request.url());
503
617
  if (response) {
504
618
  const responseObject = SDK.RemoteObject.RemoteObject.fromLocalObject(response);
505
619
  const responseTreeElement = new ObjectUI.ObjectPropertiesSection.RootElement(responseObject);
@@ -510,9 +624,9 @@ export class RequestTimingView extends UI.Widget.VBox {
510
624
  const serviceWorkerResponseSource = document.createElement('div');
511
625
  serviceWorkerResponseSource.classList.add('network-fetch-details-treeitem');
512
626
  let swResponseSourceString = i18nString(UIStrings.unknown);
513
- const swResponseSource = this.request.serviceWorkerResponseSource();
627
+ const swResponseSource = this.#request.serviceWorkerResponseSource();
514
628
  if (swResponseSource) {
515
- swResponseSourceString = this.getLocalizedResponseSourceForCode(swResponseSource);
629
+ swResponseSourceString = getLocalizedResponseSourceForCode(swResponseSource);
516
630
  }
517
631
  serviceWorkerResponseSource.textContent = i18nString(UIStrings.sourceOfResponseS, {PH1: swResponseSourceString});
518
632
 
@@ -521,7 +635,7 @@ export class RequestTimingView extends UI.Widget.VBox {
521
635
 
522
636
  const cacheNameElement = document.createElement('div');
523
637
  cacheNameElement.classList.add('network-fetch-details-treeitem');
524
- const responseCacheStorageName = this.request.getResponseCacheStorageCacheName();
638
+ const responseCacheStorageName = this.#request.getResponseCacheStorageCacheName();
525
639
  if (responseCacheStorageName) {
526
640
  cacheNameElement.textContent = i18nString(UIStrings.cacheStorageCacheNameS, {PH1: responseCacheStorageName});
527
641
  } else {
@@ -531,7 +645,7 @@ export class RequestTimingView extends UI.Widget.VBox {
531
645
  const cacheNameTreeElement = new UI.TreeOutline.TreeElement(cacheNameElement);
532
646
  detailsView.appendChild(cacheNameTreeElement);
533
647
 
534
- const retrievalTime = this.request.getResponseRetrievalTime();
648
+ const retrievalTime = this.#request.getResponseRetrievalTime();
535
649
  if (retrievalTime) {
536
650
  const responseTimeElement = document.createElement('div');
537
651
  responseTimeElement.classList.add('network-fetch-details-treeitem');
@@ -539,65 +653,21 @@ export class RequestTimingView extends UI.Widget.VBox {
539
653
  const responseTimeTreeElement = new UI.TreeOutline.TreeElement(responseTimeElement);
540
654
  detailsView.appendChild(responseTimeTreeElement);
541
655
  }
656
+ return detailsView;
542
657
  }
543
658
 
544
- private getLocalizedResponseSourceForCode(swResponseSource: Protocol.Network.ServiceWorkerResponseSource):
545
- Common.UIString.LocalizedString {
546
- switch (swResponseSource) {
547
- case Protocol.Network.ServiceWorkerResponseSource.CacheStorage:
548
- return i18nString(UIStrings.serviceworkerCacheStorage);
549
- case Protocol.Network.ServiceWorkerResponseSource.HttpCache:
550
- return i18nString(UIStrings.fromHttpCache);
551
- case Protocol.Network.ServiceWorkerResponseSource.Network:
552
- return i18nString(UIStrings.networkFetch);
553
- default:
554
- return i18nString(UIStrings.fallbackCode);
555
- }
556
- }
557
-
558
- private onToggleFetchDetails(fetchDetailsElement: Element, event: Event): void {
559
- if (!event.target) {
560
- return;
659
+ #routerDetailsTree(): UI.TreeOutline.TreeOutlineInShadow|undefined {
660
+ if (!this.#request?.serviceWorkerRouterInfo) {
661
+ return undefined;
561
662
  }
562
663
 
563
- const target = (event.target as Element);
564
- if (target.classList.contains('network-fetch-timing-bar-clickable')) {
565
- if (fetchDetailsElement.classList.contains('network-fetch-timing-bar-details-collapsed')) {
566
- Host.userMetrics.actionTaken(Host.UserMetrics.Action.NetworkPanelServiceWorkerRespondWith);
567
- }
568
- const expanded = target.getAttribute('aria-checked') === 'true';
569
- target.setAttribute('aria-checked', String(!expanded));
570
-
571
- fetchDetailsElement.classList.toggle('network-fetch-timing-bar-details-collapsed');
572
- fetchDetailsElement.classList.toggle('network-fetch-timing-bar-details-expanded');
573
- }
574
- }
575
-
576
- private constructRouterEvaluationView(): void {
577
- if (!this.tableElement) {
578
- return;
579
- }
580
-
581
- const routerEvaluationDetailsElement = this.tableElement.querySelector('.router-evaluation-timing-bar-details');
582
- if (!routerEvaluationDetailsElement) {
583
- return;
584
- }
585
-
586
- routerEvaluationDetailsElement.classList.add('network-fetch-timing-bar-details-collapsed');
587
-
588
- self.onInvokeElement(
589
- this.tableElement, this.onToggleRouterEvaluationDetails.bind(this, routerEvaluationDetailsElement));
590
-
591
664
  const detailsView = new UI.TreeOutline.TreeOutlineInShadow();
592
- routerEvaluationDetailsElement.appendChild(detailsView.element);
593
665
 
594
- const {serviceWorkerRouterInfo} = this.request;
666
+ const {serviceWorkerRouterInfo} = this.#request;
595
667
  if (!serviceWorkerRouterInfo) {
596
668
  return;
597
669
  }
598
670
 
599
- const document = this.tableElement.ownerDocument;
600
-
601
671
  // Add matched source type element
602
672
  const matchedSourceTypeElement = document.createElement('div');
603
673
  matchedSourceTypeElement.classList.add('network-fetch-details-treeitem');
@@ -617,61 +687,47 @@ export class RequestTimingView extends UI.Widget.VBox {
617
687
 
618
688
  const actualSourceTypeTreeElement = new UI.TreeOutline.TreeElement(actualSourceTypeElement);
619
689
  detailsView.appendChild(actualSourceTypeTreeElement);
690
+
691
+ return detailsView;
620
692
  }
621
693
 
622
- private onToggleRouterEvaluationDetails(routerEvaluationDetailsElement: Element, event: Event): void {
623
- if (!event.target) {
624
- return;
694
+ set request(request: SDK.NetworkRequest.NetworkRequest) {
695
+ this.#request = request;
696
+ if (this.isShowing()) {
697
+ this.#request.addEventListener(SDK.NetworkRequest.Events.TIMING_CHANGED, this.requestUpdate, this);
698
+ this.#request.addEventListener(SDK.NetworkRequest.Events.FINISHED_LOADING, this.requestUpdate, this);
699
+ this.requestUpdate();
625
700
  }
701
+ }
626
702
 
627
- const target = (event.target as Element);
628
- if (target.classList.contains('network-fetch-timing-bar-clickable')) {
629
- const expanded = target.getAttribute('aria-checked') === 'true';
630
- target.setAttribute('aria-checked', String(!expanded));
631
-
632
- routerEvaluationDetailsElement.classList.toggle('network-fetch-timing-bar-details-collapsed');
633
- routerEvaluationDetailsElement.classList.toggle('network-fetch-timing-bar-details-expanded');
703
+ set calculator(calculator: NetworkTimeCalculator.NetworkTimeCalculator) {
704
+ this.#calculator = calculator;
705
+ if (this.isShowing()) {
706
+ this.#calculator.addEventListener(NetworkTimeCalculator.Events.BOUNDARIES_CHANGED, this.boundaryChanged, this);
707
+ this.requestUpdate();
634
708
  }
635
709
  }
636
710
 
637
711
  override wasShown(): void {
638
712
  super.wasShown();
639
- this.request.addEventListener(SDK.NetworkRequest.Events.TIMING_CHANGED, this.refresh, this);
640
- this.request.addEventListener(SDK.NetworkRequest.Events.FINISHED_LOADING, this.refresh, this);
641
- this.calculator.addEventListener(NetworkTimeCalculator.Events.BOUNDARIES_CHANGED, this.boundaryChanged, this);
642
- this.refresh();
713
+ this.#request?.addEventListener(SDK.NetworkRequest.Events.TIMING_CHANGED, this.requestUpdate, this);
714
+ this.#request?.addEventListener(SDK.NetworkRequest.Events.FINISHED_LOADING, this.requestUpdate, this);
715
+ this.#calculator?.addEventListener(NetworkTimeCalculator.Events.BOUNDARIES_CHANGED, this.boundaryChanged, this);
716
+ this.requestUpdate();
643
717
  }
644
718
 
645
719
  override willHide(): void {
646
720
  super.willHide();
647
- this.request.removeEventListener(SDK.NetworkRequest.Events.TIMING_CHANGED, this.refresh, this);
648
- this.request.removeEventListener(SDK.NetworkRequest.Events.FINISHED_LOADING, this.refresh, this);
649
- this.calculator.removeEventListener(NetworkTimeCalculator.Events.BOUNDARIES_CHANGED, this.boundaryChanged, this);
650
- }
651
-
652
- private refresh(): void {
653
- if (this.tableElement) {
654
- this.tableElement.remove();
655
- }
656
-
657
- this.tableElement = RequestTimingView.createTimingTable(this.request, this.calculator);
658
- this.tableElement.classList.add('resource-timing-table');
659
- this.element.appendChild(this.tableElement);
660
-
661
- if (this.request.fetchedViaServiceWorker) {
662
- this.constructFetchDetailsView();
663
- }
664
-
665
- if (this.request.serviceWorkerRouterInfo) {
666
- this.constructRouterEvaluationView();
667
- }
721
+ this.#request?.removeEventListener(SDK.NetworkRequest.Events.TIMING_CHANGED, this.requestUpdate, this);
722
+ this.#request?.removeEventListener(SDK.NetworkRequest.Events.FINISHED_LOADING, this.requestUpdate, this);
723
+ this.#calculator?.removeEventListener(NetworkTimeCalculator.Events.BOUNDARIES_CHANGED, this.boundaryChanged, this);
668
724
  }
669
725
 
670
726
  private boundaryChanged(): void {
671
727
  const minimumBoundary = this.calculator.minimumBoundary();
672
- if (minimumBoundary !== this.lastMinimumBoundary) {
673
- this.lastMinimumBoundary = minimumBoundary;
674
- this.refresh();
728
+ if (minimumBoundary !== this.#lastMinimumBoundary) {
729
+ this.#lastMinimumBoundary = minimumBoundary;
730
+ this.requestUpdate();
675
731
  }
676
732
  }
677
733
  }