Skip to content
File

Blob: src/workerd/server/tests/inspector/driver.mjs

javascript121 lines
1// Copyright (c) 2024 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4import { env } from 'node:process';
5import { beforeEach, afterEach, test } from 'node:test';
6import assert from 'node:assert';
7import CDP from 'chrome-remote-interface';
8import { WorkerdServerHarness } from '../server-harness.mjs';
9 
10// Global that is reset for each test.
11let workerd;
12 
13assert(
14 env.WORKERD_BINARY !== undefined,
15 'You must set the WORKERD_BINARY environment variable.'
16);
17assert(
18 env.WORKERD_CONFIG !== undefined,
19 'You must set the WORKERD_CONFIG environment variable.'
20);
21 
22// Start workerd.
23beforeEach(async () => {
24 workerd = new WorkerdServerHarness({
25 workerdBinary: env.WORKERD_BINARY,
26 workerdConfig: env.WORKERD_CONFIG,
27 
28 // Hard-coded to match a socket name expected in the `workerdConfig` file.
29 listenPortNames: ['http'],
30 });
31 
32 await workerd.start();
33 
34 // We wait for the worker's HTTP port to come online before starting the test case. If we don't,
35 // and the inspector port comes online first, there's a chance the inspector connection will fail
36 // with 404 because the isolate doesn't exist yet.
37 await workerd.getListenPort('http');
38});
39 
40// Stop workerd.
41afterEach(async () => {
42 const [code, signal] = await workerd.stop();
43 assert(code === 0 || signal === 'SIGTERM');
44 workerd = null;
45});
46 
47async function connectInspector(port) {
48 return await CDP({
49 port,
50 
51 // Hard-coded to match a service name expected in the `workerdConfig` file.
52 target: '/main',
53 
54 // Required to avoid trying to load the Protocol (schema, I guess?) from workerd, which doesn't
55 // implement the inspector protocol message in question.
56 local: true,
57 });
58}
59 
60async function profileAndExpectDeriveBitsFrames(inspectorClient) {
61 // Enable and start profiling.
62 await inspectorClient.Profiler.enable();
63 await inspectorClient.Profiler.start();
64 
65 // Drive the worker with a test request. A single one is sufficient.
66 let httpPort = await workerd.getListenPort('http');
67 const response = await fetch(`http://localhost:${httpPort}`);
68 await response.arrayBuffer();
69 
70 // Stop and disable profiling.
71 const profile = await inspectorClient.Profiler.stop();
72 await inspectorClient.Profiler.disable();
73 
74 // Figure out which function name was most frequently sampled.
75 let hitCountMap = new Map();
76 
77 for (let node of profile.profile.nodes) {
78 if (hitCountMap.get(node.callFrame.functionName) === undefined) {
79 hitCountMap.set(node.callFrame.functionName, 0);
80 }
81 hitCountMap.set(
82 node.callFrame.functionName,
83 hitCountMap.get(node.callFrame.functionName) + node.hitCount
84 );
85 }
86 
87 let max = {
88 name: null,
89 count: 0,
90 };
91 
92 for (let [name, count] of hitCountMap) {
93 if (count > max.count) {
94 max.name = name;
95 max.count = count;
96 }
97 }
98 
99 // The most CPU-intensive function our test script runs is `deriveBits()`, so we expect that to be
100 // the most frequently sampled function.
101 assert.equal(max.name, 'deriveBits');
102 assert.notEqual(max.count, 0);
103}
104 
105// Regression test for:
106// - https://github.com/cloudflare/workerd/issues/1754
107// - https://github.com/cloudflare/workerd/issues/2564
108//
109// At one time, workerd profiling broke, and started producing only "(program)" frames. My original
110// attempt at a fix subsequently caused workerd to segfault on the second inspector connection. This
111// rather expensive test case exercises both regressions.
112test('Profiler mostly sees deriveBits() frames, and can safely reconnect', async () => {
113 for (let i = 0; i < 2; ++i) {
114 let inspectorClient = await connectInspector(
115 await workerd.getListenInspectorPort()
116 );
117 await profileAndExpectDeriveBitsFrames(inspectorClient);
118 await inspectorClient.close();
119 }
120});