Skip to content

Commit 5706c90

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Display network recommendation in Perf panel toolbar
This required significant refactors to CrUXManager/LiveMetricView, moving all the state to the CrUXManager so that it can also be used from TimelinePanel. Bug: 311438203 Change-Id: Ic67b4bf0362955dbbddf3df7a2cb2800daa3c12d Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6044331 Auto-Submit: Connor Clark <[email protected]> Reviewed-by: Adam Raine <[email protected]> Commit-Queue: Connor Clark <[email protected]>
1 parent b750d0e commit 5706c90

File tree

15 files changed

+366
-276
lines changed

15 files changed

+366
-276
lines changed

front_end/models/crux-manager/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ devtools_module("crux-manager") {
1212
deps = [
1313
"../../core/common:bundle",
1414
"../../core/sdk:bundle",
15+
"../emulation:bundle",
1516
]
1617
}
1718

front_end/models/crux-manager/CrUXManager.test.ts

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import * as Common from '../../core/common/common.js';
66
import * as Platform from '../../core/platform/platform.js';
77
import type * as Root from '../../core/root/root.js';
88
import * as SDK from '../../core/sdk/sdk.js';
9+
import * as EmulationModel from '../../models/emulation/emulation.js';
910
import {createTarget} from '../../testing/EnvironmentHelpers.js';
1011
import {describeWithMockConnection} from '../../testing/MockConnection.js';
1112

1213
import * as CrUXManager from './crux-manager.js';
1314

14-
function mockResponse(): CrUXManager.CrUXResponse {
15+
function mockResponse(scopes: {pageScope: CrUXManager.PageScope, deviceScope: CrUXManager.DeviceScope}|null = null):
16+
CrUXManager.CrUXResponse {
1517
return {
1618
record: {
1719
key: {},
@@ -23,6 +25,8 @@ function mockResponse(): CrUXManager.CrUXResponse {
2325
{start: 4000, density: 0.2},
2426
],
2527
percentiles: {p75: 1000},
28+
// @ts-expect-error
29+
testScopes: scopes,
2630
},
2731
cumulative_layout_shift: {
2832
histogram: [
@@ -31,6 +35,8 @@ function mockResponse(): CrUXManager.CrUXResponse {
3135
{start: 0.25, density: 0.8},
3236
],
3337
percentiles: {p75: 0.25},
38+
// @ts-expect-error
39+
testScopes: scopes,
3440
},
3541
},
3642
collectionPeriod: {
@@ -60,6 +66,7 @@ describeWithMockConnection('CrUXManager', () => {
6066
cruxManager = CrUXManager.CrUXManager.instance({forceNew: true});
6167
mockFetch = sinon.stub(window, 'fetch');
6268
mockConsoleError = sinon.stub(console, 'error');
69+
EmulationModel.DeviceModeModel.DeviceModeModel.instance({forceNew: true});
6370
});
6471

6572
afterEach(() => {
@@ -307,13 +314,13 @@ describeWithMockConnection('CrUXManager', () => {
307314
beforeEach(() => {
308315
getFieldDataMock = sinon.stub(cruxManager, 'getFieldDataForPage');
309316
getFieldDataMock.resolves({
310-
'origin-ALL': mockResponse(),
311-
'origin-DESKTOP': mockResponse(),
312-
'origin-PHONE': mockResponse(),
317+
'origin-ALL': mockResponse({pageScope: 'origin', deviceScope: 'ALL'}),
318+
'origin-DESKTOP': mockResponse({pageScope: 'origin', deviceScope: 'DESKTOP'}),
319+
'origin-PHONE': mockResponse({pageScope: 'origin', deviceScope: 'PHONE'}),
313320
'origin-TABLET': null,
314-
'url-ALL': mockResponse(),
315-
'url-DESKTOP': mockResponse(),
316-
'url-PHONE': mockResponse(),
321+
'url-ALL': mockResponse({pageScope: 'url', deviceScope: 'ALL'}),
322+
'url-DESKTOP': mockResponse({pageScope: 'url', deviceScope: 'DESKTOP'}),
323+
'url-PHONE': mockResponse({pageScope: 'url', deviceScope: 'PHONE'}),
317324
'url-TABLET': null,
318325
warnings: [],
319326
});
@@ -413,6 +420,60 @@ describeWithMockConnection('CrUXManager', () => {
413420
assert.strictEqual(getFieldDataMock.callCount, 1);
414421
assert.strictEqual(getFieldDataMock.firstCall.args[0], 'https://example.com/awaitInspected');
415422
});
423+
424+
it('getSelectedFieldMetricData - should take from selected page scope', async () => {
425+
await cruxManager.getFieldDataForCurrentPage();
426+
427+
let data: CrUXManager.MetricResponse|undefined;
428+
429+
cruxManager.fieldPageScope = 'origin';
430+
data = cruxManager.getSelectedFieldMetricData('largest_contentful_paint');
431+
assert.strictEqual(data?.percentiles?.p75, 1000);
432+
// @ts-expect-error
433+
assert.strictEqual(data?.testScopes.pageScope, 'origin');
434+
435+
cruxManager.fieldPageScope = 'url';
436+
data = cruxManager.getSelectedFieldMetricData('largest_contentful_paint');
437+
assert.strictEqual(data?.percentiles?.p75, 1000);
438+
// @ts-expect-error
439+
assert.strictEqual(data?.testScopes.pageScope, 'url');
440+
});
441+
442+
it('should take from selected device scope', async () => {
443+
await cruxManager.getFieldDataForCurrentPage();
444+
cruxManager.fieldPageScope = 'url';
445+
446+
let data: CrUXManager.MetricResponse|undefined;
447+
448+
cruxManager.fieldDeviceOption = 'ALL';
449+
data = cruxManager.getSelectedFieldMetricData('largest_contentful_paint');
450+
assert.strictEqual(data?.percentiles?.p75, 1000);
451+
// @ts-expect-error
452+
assert.strictEqual(data?.testScopes?.deviceScope, 'ALL');
453+
454+
cruxManager.fieldDeviceOption = 'PHONE';
455+
data = cruxManager.getSelectedFieldMetricData('largest_contentful_paint');
456+
assert.strictEqual(data?.percentiles?.p75, 1000);
457+
// @ts-expect-error
458+
assert.strictEqual(data?.testScopes?.deviceScope, 'PHONE');
459+
});
460+
461+
it('auto device option should chose based on emulation', async () => {
462+
cruxManager.fieldDeviceOption = 'AUTO';
463+
assert.strictEqual(cruxManager.getSelectedDeviceScope(), 'ALL');
464+
465+
await cruxManager.getFieldDataForCurrentPage();
466+
assert.strictEqual(cruxManager.getSelectedDeviceScope(), 'DESKTOP');
467+
468+
for (const device of EmulationModel.EmulatedDevices.EmulatedDevicesList.instance().standard()) {
469+
if (device.title === 'Moto G Power') {
470+
EmulationModel.DeviceModeModel.DeviceModeModel.instance().emulate(
471+
EmulationModel.DeviceModeModel.Type.Device, device, device.modes[0], 1);
472+
}
473+
}
474+
475+
assert.strictEqual(cruxManager.getSelectedDeviceScope(), 'PHONE');
476+
});
416477
});
417478

418479
describe('automatic refresh', () => {

front_end/models/crux-manager/CrUXManager.ts

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import * as Common from '../../core/common/common.js';
66
import * as i18n from '../../core/i18n/i18n.js';
77
import * as SDK from '../../core/sdk/sdk.js';
8+
import * as EmulationModel from '../../models/emulation/emulation.js';
89

910
const UIStrings = {
1011
/**
@@ -26,6 +27,7 @@ export type StandardMetricNames = 'cumulative_layout_shift'|'first_contentful_pa
2627
export type MetricNames = StandardMetricNames|'form_factors';
2728
export type FormFactor = 'DESKTOP'|'PHONE'|'TABLET';
2829
export type DeviceScope = FormFactor|'ALL';
30+
export type DeviceOption = DeviceScope|'AUTO';
2931
export type PageScope = 'url'|'origin';
3032
export type ConnectionType = 'offline'|'slow-2G'|'2G'|'3G'|'4G';
3133

@@ -114,6 +116,9 @@ export class CrUXManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
114116
#mainDocumentUrl?: string;
115117
#configSetting: Common.Settings.Setting<ConfigSetting>;
116118
#endpoint = DEFAULT_ENDPOINT;
119+
#pageResult?: PageResult;
120+
fieldDeviceOption: DeviceOption = 'AUTO';
121+
fieldPageScope: PageScope = 'url';
117122

118123
private constructor() {
119124
super();
@@ -139,7 +144,7 @@ export class CrUXManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
139144
storageTypeForConsent);
140145

141146
this.#configSetting.addChangeListener(() => {
142-
void this.#automaticRefresh();
147+
void this.refresh();
143148
});
144149

145150
SDK.TargetManager.TargetManager.instance().addModelListener(
@@ -156,6 +161,11 @@ export class CrUXManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
156161
return cruxManagerInstance;
157162
}
158163

164+
/** The most recent page result from the CrUX service. */
165+
get pageResult(): PageResult|undefined {
166+
return this.#pageResult;
167+
}
168+
159169
getConfigSetting(): Common.Settings.Setting<ConfigSetting> {
160170
return this.#configSetting;
161171
}
@@ -191,6 +201,7 @@ export class CrUXManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
191201
}
192202

193203
await Promise.all(promises);
204+
this.#pageResult = pageResult;
194205
} catch (err) {
195206
console.error(err);
196207
} finally {
@@ -231,6 +242,7 @@ export class CrUXManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
231242
this.#getMappedUrl(currentUrl);
232243

233244
const result = await this.getFieldDataForPage(urlForCrux);
245+
this.#pageResult = result;
234246
if (currentUrl !== urlForCrux) {
235247
result.warnings.push(i18nString(UIStrings.fieldOverrideWarning));
236248
}
@@ -262,22 +274,22 @@ export class CrUXManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
262274

263275
this.#mainDocumentUrl = event.data.url;
264276

265-
await this.#automaticRefresh();
277+
await this.refresh();
266278
}
267279

268-
async #automaticRefresh(): Promise<void> {
280+
async refresh(): Promise<void> {
269281
// This does 2 things:
270282
// - Tells listeners to clear old data so it isn't shown during a URL transition
271283
// - Tells listeners to clear old data when field data is disabled.
284+
this.#pageResult = undefined;
272285
this.dispatchEventToListeners(Events.FIELD_DATA_CHANGED, undefined);
273286

274287
if (!this.#configSetting.get().enabled) {
275288
return;
276289
}
277290

278-
const pageResult = await this.getFieldDataForCurrentPage();
279-
280-
this.dispatchEventToListeners(Events.FIELD_DATA_CHANGED, pageResult);
291+
this.#pageResult = await this.getFieldDataForCurrentPage();
292+
this.dispatchEventToListeners(Events.FIELD_DATA_CHANGED, this.#pageResult);
281293
}
282294

283295
#normalizeUrl(inputUrl: string): URL {
@@ -344,6 +356,45 @@ export class CrUXManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
344356
return responseData;
345357
}
346358

359+
#getAutoDeviceScope(): DeviceScope {
360+
const emulationModel = EmulationModel.DeviceModeModel.DeviceModeModel.tryInstance();
361+
if (emulationModel === null) {
362+
return 'ALL';
363+
}
364+
365+
if (emulationModel.isMobile()) {
366+
if (this.#pageResult?.[`${this.fieldPageScope}-PHONE`]) {
367+
return 'PHONE';
368+
}
369+
370+
return 'ALL';
371+
}
372+
373+
if (this.#pageResult?.[`${this.fieldPageScope}-DESKTOP`]) {
374+
return 'DESKTOP';
375+
}
376+
377+
return 'ALL';
378+
}
379+
380+
getSelectedDeviceScope(): DeviceScope {
381+
return this.fieldDeviceOption === 'AUTO' ? this.#getAutoDeviceScope() : this.fieldDeviceOption;
382+
}
383+
384+
getSelectedFieldResponse(): CrUXResponse|null|undefined {
385+
const pageScope = this.fieldPageScope;
386+
const deviceScope = this.getSelectedDeviceScope();
387+
return this.getFieldResponse(pageScope, deviceScope);
388+
}
389+
390+
getSelectedFieldMetricData(fieldMetric: StandardMetricNames): MetricResponse|undefined {
391+
return this.getSelectedFieldResponse()?.record.metrics[fieldMetric];
392+
}
393+
394+
getFieldResponse(pageScope: PageScope, deviceScope: DeviceScope): CrUXResponse|null|undefined {
395+
return this.#pageResult?.[`${pageScope}-${deviceScope}`];
396+
}
397+
347398
setEndpointForTesting(endpoint: string): void {
348399
this.#endpoint = endpoint;
349400
}

front_end/models/emulation/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ devtools_entrypoint("bundle") {
3434
":*",
3535
"../*",
3636
"../../panels/emulation/*",
37+
"../crux-manager/*",
3738
]
3839

3940
visibility += devtools_models_visibility

front_end/panels/mobile_throttling/ThrottlingManager.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ describeWithEnvironment('ThrottlingManager', () => {
6565
it('listens to changes in cpu throttling setting', () => {
6666
const cpuThrottlingPresets = MobileThrottling.ThrottlingPresets.ThrottlingPresets.cpuThrottlingPresets;
6767
const throttlingManager = MobileThrottling.ThrottlingManager.throttlingManager();
68-
const selector = throttlingManager.createCPUThrottlingSelector();
68+
const selector = throttlingManager.createCPUThrottlingSelector().control;
6969
assert.strictEqual(
7070
cpuThrottlingPresets[selector.selectedIndex()], SDK.CPUThrottlingManager.CPUThrottlingRates.NO_THROTTLING);
7171

0 commit comments

Comments
 (0)