Skip to content

Commit 2e6530e

Browse files
szuendDevtools-frontend LUCI CQ
authored andcommitted
[lighthouse] Use CDPConnection instead of parallel connection
Both on the frontend-side and the worker-side, this CL updates Lighthouse to use the new CDPConnection. On the frontend-side, we create a new CDP session and then use the standard CDPConnection to send/receive messages, which are forwarded nearly 1:1 to the worker. On the worker-side, we create a custom transport with a CDPConnection, and then wrap it with the new puppeteer adapter. Bug: 453469270 Change-Id: I737816f4ce657210079413ba888e9c26897dac82 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7106261 Reviewed-by: Connor Clark <[email protected]> Reviewed-by: Paul Irish <[email protected]> Commit-Queue: Simon Zünd <[email protected]>
1 parent da824b8 commit 2e6530e

File tree

5 files changed

+86
-77
lines changed

5 files changed

+86
-77
lines changed

front_end/entrypoints/lighthouse_worker/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ devtools_module("lighthouse_worker") {
1010
sources = [ "LighthouseWorkerService.ts" ]
1111

1212
deps = [
13+
"../../core/protocol_client:bundle",
1314
"../../core/root:bundle",
1415
"../../core/sdk:bundle",
1516
"../../services/puppeteer:bundle",

front_end/entrypoints/lighthouse_worker/LighthouseWorkerService.ts

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import * as ProtocolClient from '../../core/protocol_client/protocol_client.js';
56
import * as Root from '../../core/root/root.js';
6-
import type * as SDK from '../../core/sdk/sdk.js';
77
import * as PuppeteerService from '../../services/puppeteer/puppeteer.js';
88
import * as ThirdPartyWeb from '../../third_party/third-party-web/third-party-web.js';
99

@@ -12,19 +12,12 @@ function disableLoggingForTest(): void {
1212
}
1313

1414
/**
15-
* ConnectionProxy is a SDK interface, but the implementation has no knowledge it's a parallelConnection.
16-
* The CDP traffic is smuggled back and forth by the system described in LighthouseProtocolService
15+
* WorkerConnectionTransport is a DevTools `ConnectionTransport` implementation that talks
16+
* CDP via web worker postMessage. The system is described in LighthouseProtocolService.
1717
*/
18-
class ConnectionProxy implements SDK.Connections.ParallelConnectionInterface {
19-
sessionId: string;
20-
onMessage: ((arg0: Object) => void)|null;
21-
onDisconnect: ((arg0: string) => void)|null;
22-
23-
constructor(sessionId: string) {
24-
this.sessionId = sessionId;
25-
this.onMessage = null;
26-
this.onDisconnect = null;
27-
}
18+
class WorkerConnectionTransport implements ProtocolClient.ConnectionTransport.ConnectionTransport {
19+
onMessage: ((arg0: Object) => void)|null = null;
20+
onDisconnect: ((arg0: string) => void)|null = null;
2821

2922
setOnMessage(onMessage: (arg0: Object|string) => void): void {
3023
this.onMessage = onMessage;
@@ -34,14 +27,6 @@ class ConnectionProxy implements SDK.Connections.ParallelConnectionInterface {
3427
this.onDisconnect = onDisconnect;
3528
}
3629

37-
getOnDisconnect(): (((arg0: string) => void)|null) {
38-
return this.onDisconnect;
39-
}
40-
41-
getSessionId(): string {
42-
return this.sessionId;
43-
}
44-
4530
sendRawMessage(message: string): void {
4631
notifyFrontendViaWorkerMessage('sendProtocolMessage', {message});
4732
}
@@ -53,7 +38,7 @@ class ConnectionProxy implements SDK.Connections.ParallelConnectionInterface {
5338
}
5439
}
5540

56-
let cdpConnection: ConnectionProxy|undefined;
41+
let cdpTransport: WorkerConnectionTransport|undefined;
5742
let endTimespan: (() => unknown)|undefined;
5843

5944
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -68,8 +53,8 @@ async function invokeLH(action: string, args: any): Promise<unknown> {
6853
notifyFrontendViaWorkerMessage('statusUpdate', {message: message[1]});
6954
});
7055

71-
let puppeteerHandle: Awaited<ReturnType<typeof PuppeteerService.PuppeteerConnection
72-
.PuppeteerConnectionHelper['connectPuppeteerToConnectionViaTabLegacy']>>|
56+
let puppeteerHandle: Awaited<ReturnType<
57+
typeof PuppeteerService.PuppeteerConnection.PuppeteerConnectionHelper['connectPuppeteerToConnectionViaTab']>>|
7358
undefined;
7459

7560
try {
@@ -98,11 +83,15 @@ async function invokeLH(action: string, args: any): Promise<unknown> {
9883
self.thirdPartyWeb.provideThirdPartyWeb(ThirdPartyWeb.ThirdPartyWeb);
9984

10085
const {rootTargetId, mainSessionId} = args;
101-
cdpConnection = new ConnectionProxy(mainSessionId);
86+
cdpTransport = new WorkerConnectionTransport();
87+
// TODO(crbug.com/453469270): Use "DevToolsCDPConnection" once we split SessionRouter into
88+
// a connection handling part and a session handling part.
89+
const connection = new ProtocolClient.InspectorBackend.SessionRouter(cdpTransport);
10290
puppeteerHandle =
103-
await PuppeteerService.PuppeteerConnection.PuppeteerConnectionHelper.connectPuppeteerToConnectionViaTabLegacy({
104-
connection: cdpConnection,
105-
rootTargetId,
91+
await PuppeteerService.PuppeteerConnection.PuppeteerConnectionHelper.connectPuppeteerToConnectionViaTab({
92+
connection,
93+
targetId: rootTargetId,
94+
sessionId: mainSessionId,
10695
// Lighthouse can only audit normal pages.
10796
isPageTargetCallback: targetInfo => targetInfo.type === 'page',
10897
});
@@ -212,7 +201,7 @@ async function onFrontendMessage(event: MessageEvent): Promise<void> {
212201
break;
213202
}
214203
case 'dispatchProtocolMessage': {
215-
cdpConnection?.onMessage?.(messageFromFrontend.args.message);
204+
cdpTransport?.onMessage?.(messageFromFrontend.args.message);
216205
break;
217206
}
218207
default: {

front_end/panels/lighthouse/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ devtools_ui_module("lighthouse") {
3838
"../../core/protocol_client:bundle",
3939
"../../core/root:bundle",
4040
"../../core/sdk:bundle",
41+
"../../generated:protocol",
4142
"../../models/geometry:bundle",
4243
"../../models/text_utils:bundle",
4344
"../../models/trace:bundle",

front_end/panels/lighthouse/LighthouseProtocolService.test.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import type * as ProtocolClient from '../../core/protocol_client/protocol_client.js';
65
import * as SDK from '../../core/sdk/sdk.js';
76
import {createTarget} from '../../testing/EnvironmentHelpers.js';
8-
import {describeWithMockConnection} from '../../testing/MockConnection.js';
7+
import {describeWithMockConnection, setMockConnectionResponseHandler} from '../../testing/MockConnection.js';
98

109
import type * as LighthouseModule from './lighthouse.js';
1110

@@ -16,7 +15,6 @@ describeWithMockConnection('LighthouseProtocolService', () => {
1615
let rootTarget: SDK.Target.Target;
1716
let suspendAllTargets: sinon.SinonStub;
1817
let resumeAllTargets: sinon.SinonStub;
19-
let createParallelConnection: sinon.SinonStub;
2018

2119
beforeEach(async () => {
2220
Lighthouse = await import('./lighthouse.js');
@@ -33,19 +31,10 @@ describeWithMockConnection('LighthouseProtocolService', () => {
3331
assert.exists(childTargetManager);
3432

3533
sinon.stub(childTargetManager, 'getParentTargetId').resolves(primaryTarget.targetInfo()?.targetId);
36-
if (rootTarget === primaryTarget) {
37-
createParallelConnection = sinon.stub(childTargetManager, 'createParallelConnection').resolves({
38-
connection: {disconnect: () => {}} as ProtocolClient.ConnectionTransport.ConnectionTransport,
39-
sessionId: 'foo',
40-
});
41-
} else {
34+
if (rootTarget !== primaryTarget) {
4235
const rootChildTargetManager = rootTarget.model(SDK.ChildTargetManager.ChildTargetManager);
4336
assert.exists(rootChildTargetManager);
4437
sinon.stub(rootChildTargetManager, 'getParentTargetId').resolves(rootTarget.targetInfo()?.targetId);
45-
createParallelConnection = sinon.stub(rootChildTargetManager, 'createParallelConnection').resolves({
46-
connection: {disconnect: () => {}} as ProtocolClient.ConnectionTransport.ConnectionTransport,
47-
sessionId: 'foo',
48-
});
4938
}
5039
});
5140

@@ -55,10 +44,15 @@ describeWithMockConnection('LighthouseProtocolService', () => {
5544
sinon.assert.calledOnce(suspendAllTargets);
5645
});
5746

58-
it('creates a parallel connection', async () => {
47+
it('attaches to to the root target', async () => {
48+
const attachedToTargetStub = sinon.stub();
49+
setMockConnectionResponseHandler('Target.attachToTarget', attachedToTargetStub);
5950
const service = new Lighthouse.LighthouseProtocolService.ProtocolService();
51+
6052
await service.attach();
61-
sinon.assert.calledOnce(createParallelConnection);
53+
54+
sinon.assert.calledOnceWithExactly(
55+
attachedToTargetStub, {targetId: rootTarget.targetInfo()?.targetId, flatten: true});
6256
});
6357

6458
it('resumes all targets', async () => {

front_end/panels/lighthouse/LighthouseProtocolService.ts

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@ import * as i18n from '../../core/i18n/i18n.js';
66
import type * as Platform from '../../core/platform/platform.js';
77
import type * as ProtocolClient from '../../core/protocol_client/protocol_client.js';
88
import * as SDK from '../../core/sdk/sdk.js';
9+
import type * as Protocol from '../../generated/protocol.js';
910

1011
import type * as ReportRenderer from './LighthouseReporterTypes.js';
1112

1213
/**
1314
* @file
14-
* ────────────┐
15-
* │CDP Backend
16-
* ────────────┘
15+
* ┌───────────────┐
16+
* │ CDPConnection
17+
* └───────────────┘
1718
* │ ▲
18-
* │ │ parallelConnection
19+
* │ │
1920
* ┌┐ ▼ │ ┌┐
2021
* ││ dispatchProtocolMessage sendProtocolMessage ││
2122
* ││ │ ▲ ││
@@ -28,7 +29,10 @@ import type * as ReportRenderer from './LighthouseReporterTypes.js';
2829
* ││ onFrontendMessage notifyFrontendViaWorkerMessage ││
2930
* ││ │ ▲ ││
3031
* ││ ▼ │ ││
31-
* LighthouseWorkerService ││ Either ConnectionProxy or LegacyPort ││
32+
* LighthouseWorkerService ││ WorkerConnectionTransport ││
33+
* ││ │ ▲ ││
34+
* ││ ▼ │ ││
35+
* ││ CDPConnection ││
3236
* ││ │ ▲ ││
3337
* ││ ┌─────────────────────┼─┼───────────────────────┐ ││
3438
* ││ │ Lighthouse ┌────▼──────┐ │ ││
@@ -37,10 +41,10 @@ import type * as ReportRenderer from './LighthouseReporterTypes.js';
3741
* └┘ └───────────────────────────────────────────────┘ └┘
3842
*
3943
* - All messages traversing the worker boundary are action-wrapped.
40-
* - All messages over the parallelConnection speak pure CDP.
41-
* - All messages within ConnectionProxy/LegacyPort speak pure CDP.
42-
* - The foundational CDP connection is `parallelConnection`.
43-
* - All connections within the worker are not actual ParallelConnection's.
44+
* - All messages over the CDPConnection speak pure CDP.
45+
* - Within the worker we also use a 'CDPConnection' but with a custom
46+
* transport called WorkerConnectionTransport.
47+
* - All messages within WorkerConnectionTransport/LegacyPort speak pure CDP.
4448
*/
4549

4650
let lastId = 1;
@@ -57,14 +61,15 @@ export interface LighthouseRun {
5761
/**
5862
* ProtocolService manages a connection between the frontend (Lighthouse panel) and the Lighthouse worker.
5963
*/
60-
export class ProtocolService {
61-
private mainSessionId?: string;
64+
export class ProtocolService implements ProtocolClient.CDPConnection.CDPConnectionObserver {
65+
private mainSessionId?: Protocol.Target.SessionID;
6266
private rootTargetId?: string;
63-
private parallelConnection?: ProtocolClient.ConnectionTransport.ConnectionTransport;
67+
private rootTarget?: SDK.Target.Target;
6468
private lighthouseWorkerPromise?: Promise<Worker>;
6569
private lighthouseMessageUpdateCallback?: ((arg0: string) => void);
6670
private removeDialogHandler?: () => void;
6771
private configForTesting?: object;
72+
private connection?: ProtocolClient.CDPConnection.CDPConnection;
6873

6974
async attach(): Promise<void> {
7075
await SDK.TargetManager.TargetManager.instance().suspendAllTargets();
@@ -90,12 +95,15 @@ export class ProtocolService {
9095
throw new Error('Could not find the child target manager class for the root target');
9196
}
9297

93-
const {connection, sessionId} = await rootChildTargetManager.createParallelConnection(message => {
94-
if (typeof message === 'string') {
95-
message = JSON.parse(message);
96-
}
97-
this.dispatchProtocolMessage(message);
98-
});
98+
const router = rootTarget.router();
99+
if (!router) {
100+
throw new Error('Expected root target to have a session router');
101+
}
102+
103+
const rootTargetId = await rootChildTargetManager.getParentTargetId();
104+
const {sessionId} = await rootTarget.targetAgent().invoke_attachToTarget({targetId: rootTargetId, flatten: true});
105+
this.connection = router;
106+
this.connection.observe(this);
99107

100108
// Lighthouse implements its own dialog handler like this, however its lifecycle ends when
101109
// the internal Lighthouse session is disposed.
@@ -114,8 +122,8 @@ export class ProtocolService {
114122
this.removeDialogHandler = () =>
115123
resourceTreeModel.removeEventListener(SDK.ResourceTreeModel.Events.JavaScriptDialogOpening, dialogHandler);
116124

117-
this.parallelConnection = connection;
118-
this.rootTargetId = await rootChildTargetManager.getParentTargetId();
125+
this.rootTargetId = rootTargetId;
126+
this.rootTarget = rootTarget;
119127
this.mainSessionId = sessionId;
120128
}
121129

@@ -166,20 +174,22 @@ export class ProtocolService {
166174

167175
async detach(): Promise<void> {
168176
const oldLighthouseWorker = this.lighthouseWorkerPromise;
169-
const oldParallelConnection = this.parallelConnection;
177+
const oldRootTarget = this.rootTarget;
170178

171179
// When detaching, make sure that we remove the old promises, before we
172180
// perform any async cleanups. That way, if there is a message coming from
173181
// lighthouse while we are in the process of cleaning up, we shouldn't deliver
174182
// them to the backend.
175183
this.lighthouseWorkerPromise = undefined;
176-
this.parallelConnection = undefined;
184+
this.rootTarget = undefined;
185+
this.connection?.unobserve(this);
186+
this.connection = undefined;
177187

178188
if (oldLighthouseWorker) {
179189
(await oldLighthouseWorker).terminate();
180190
}
181-
if (oldParallelConnection) {
182-
await oldParallelConnection.disconnect();
191+
if (oldRootTarget && this.mainSessionId) {
192+
await oldRootTarget.targetAgent().invoke_detachFromTarget({sessionId: this.mainSessionId});
183193
}
184194
await SDK.TargetManager.TargetManager.instance().resumeAllTargets();
185195
this.removeDialogHandler?.();
@@ -189,7 +199,11 @@ export class ProtocolService {
189199
this.lighthouseMessageUpdateCallback = callback;
190200
}
191201

192-
private dispatchProtocolMessage(message: string|object): void {
202+
onEvent<T extends ProtocolClient.CDPConnection.Event>(event: ProtocolClient.CDPConnection.CDPEvent<T>): void {
203+
this.dispatchProtocolMessage(event);
204+
}
205+
206+
private dispatchProtocolMessage(message: ProtocolClient.CDPConnection.CDPReceivableMessage): void {
193207
// A message without a sessionId is the main session of the main target (call it "Main session").
194208
// A parallel connection and session was made that connects to the same main target (call it "Lighthouse session").
195209
// Messages from the "Lighthouse session" have a sessionId.
@@ -199,15 +213,15 @@ export class ProtocolService {
199213
// * the message has a sessionId (is not for the "Main session")
200214
// * the message does not have a sessionId (is for the "Main session"), but only for the Target domain
201215
// (to kickstart autoAttach in LH).
202-
const protocolMessage = message as {
203-
sessionId?: string,
204-
method?: string,
205-
};
206-
if (protocolMessage.sessionId || (protocolMessage.method?.startsWith('Target'))) {
216+
if (message.sessionId || ('method' in message && message.method?.startsWith('Target'))) {
207217
void this.send('dispatchProtocolMessage', {message});
208218
}
209219
}
210220

221+
onDisconnect(): void {
222+
// Do nothing.
223+
}
224+
211225
private initWorker(): Promise<Worker> {
212226
this.lighthouseWorkerPromise = new Promise<Worker>(resolve => {
213227
const workerUrl = new URL('../../entrypoints/lighthouse_worker/lighthouse_worker.js', import.meta.url);
@@ -255,9 +269,19 @@ export class ProtocolService {
255269
}
256270

257271
private sendProtocolMessage(message: string): void {
258-
if (this.parallelConnection) {
259-
this.parallelConnection.sendRawMessage(message);
260-
}
272+
const {id, method, params, sessionId} = JSON.parse(message);
273+
// CDPConnection manages it's own message IDs and it's important, otherwise we'd clash
274+
// with the rest of the DevTools traffic.
275+
// Instead, we ignore the ID coming from the worker when sending the command, but
276+
// patch it back in when sending the response back to the worker.
277+
void this.connection?.send(method, params, sessionId).then(response => {
278+
this.dispatchProtocolMessage({
279+
id,
280+
sessionId,
281+
result: 'result' in response ? response.result : undefined,
282+
error: 'error' in response ? response.error : undefined,
283+
});
284+
});
261285
}
262286

263287
private async send(action: string, args: Record<string, string|string[]|object> = {}): Promise<void> {

0 commit comments

Comments
 (0)