-
Notifications
You must be signed in to change notification settings - Fork 347
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
EW-8447 Add regression test for CPU profiling
This commit adds a regression test for the CPU profiling bug reported in #1754 and fixed in #2497. It is based on @mrbbot's original reproduction. I backported the test to the commit just prior to #2497, and confirmed that the test caught the original breakage. I wrote the test in JavaScript, because it seemed to have the richest ecosystem of tools for working with the Chrome Devtool Protocol. I originally intended to use Playwright to initiate a CDP connection to workerd, but it seemed to make too many assumptions that it was connecting to a browser. I tried the [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface) library next, and it seemed to work well. Fixes #1754.
- Loading branch information
1 parent
5295e08
commit c6223da
Showing
4 changed files
with
150 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
load("@aspect_rules_js//js:defs.bzl", "js_test") | ||
|
||
js_test( | ||
name = "inspector-test", | ||
entry_point = "driver.mjs", | ||
env = { | ||
"WORKERD_BINARY": "$(rootpath //src/workerd/server:workerd)", | ||
"WORKERD_CONFIG": "$(rootpath :config.capnp)", | ||
}, | ||
data = [ | ||
"//:node_modules/chrome-remote-interface", | ||
"//:node_modules/@workerd/test", | ||
"//src/workerd/server:workerd", | ||
":config.capnp", | ||
":index.mjs", | ||
], | ||
tags = ["js-test"], | ||
) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# config.capnp | ||
using Workerd = import "/workerd/workerd.capnp"; | ||
|
||
const config :Workerd.Config = ( | ||
services = [ | ||
( name = "main", worker = .worker ), | ||
], | ||
sockets = [ | ||
( name = "http", address = "*:0", http = (), service = "main" ), | ||
] | ||
); | ||
|
||
const worker :Workerd.Worker = ( | ||
modules = [ | ||
( name = "./index.mjs", esModule = embed "index.mjs" ) | ||
], | ||
compatibilityDate = "2024-01-01", | ||
compatibilityFlags = ["nodejs_compat"], | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { env } from "node:process"; | ||
import { beforeEach, afterEach, test } from "node:test"; | ||
import assert from "node:assert"; | ||
import CDP from "chrome-remote-interface"; | ||
import { WorkerdServerHarness } from "@workerd/test/server-harness.mjs"; | ||
|
||
// Globals that are reset for each test. | ||
let workerd; | ||
let inspectorClient; | ||
|
||
assert(env.WORKERD_BINARY !== undefined, "You must set the WORKERD_BINARY environment variable."); | ||
assert(env.WORKERD_CONFIG !== undefined, "You must set the WORKERD_CONFIG environment variable."); | ||
|
||
// Start workerd and connect to its inspector port with our CDP library. | ||
beforeEach(async () => { | ||
workerd = new WorkerdServerHarness({ | ||
workerdBinary: env.WORKERD_BINARY, | ||
workerdConfig: env.WORKERD_CONFIG, | ||
|
||
// Hard-coded to match a socket name expected in the `workerdConfig` file. | ||
listenPortNames: [ "http" ], | ||
}); | ||
|
||
await workerd.start(); | ||
|
||
inspectorClient = await CDP({ | ||
port: await workerd.getListenInspectorPort(), | ||
|
||
// Hard-coded to match a service name expected in the `workerdConfig` file. | ||
target: "/main", | ||
|
||
// Required to avoid trying to load the Protocol (schema, I guess?) from workerd, which doesn't | ||
// implement the inspector protocol message in question. | ||
local: true, | ||
}); | ||
}); | ||
|
||
// Stop both our CDP client and workerd. | ||
afterEach(async () => { | ||
await inspectorClient.close(); | ||
inspectorClient = null; | ||
|
||
const [code, signal] = await workerd.stop(); | ||
assert(code === 0 || signal === "SIGTERM"); | ||
workerd = null; | ||
}); | ||
|
||
test("Profiler mostly sees deriveBits() frames", async () => { | ||
// Enable and start profiling. | ||
await inspectorClient.Profiler.enable(); | ||
await inspectorClient.Profiler.start(); | ||
|
||
// Drive the worker with a test request. A single one is sufficient. | ||
let httpPort = await workerd.getListenPort("http"); | ||
const response = await fetch(`http://localhost:${httpPort}`); | ||
await response.arrayBuffer(); | ||
|
||
// Stop and disable profiling. | ||
const profile = await inspectorClient.Profiler.stop(); | ||
await inspectorClient.Profiler.disable(); | ||
|
||
// Figure out which function name was most frequently sampled. | ||
let hitCountMap = new Map(); | ||
|
||
for (let node of profile.profile.nodes) { | ||
if (hitCountMap.get(node.callFrame.functionName) === undefined) { | ||
hitCountMap.set(node.callFrame.functionName, 0); | ||
} | ||
hitCountMap.set(node.callFrame.functionName, | ||
hitCountMap.get(node.callFrame.functionName) + node.hitCount); | ||
} | ||
|
||
let max = { | ||
name: null, | ||
count: 0, | ||
}; | ||
|
||
for (let [name, count] of hitCountMap) { | ||
if (count > max.count) { | ||
max.name = name; | ||
max.count = count; | ||
} | ||
} | ||
|
||
// The most CPU-intensive function our test script runs is `deriveBits()`, so we expect that to be | ||
// the most frequently sampled function. | ||
assert.equal(max.name, "deriveBits"); | ||
assert.notEqual(max.count, 0); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// index.mjs | ||
import { Buffer } from "node:buffer"; | ||
|
||
const encoder = new TextEncoder(); | ||
|
||
async function pbkdf2Derive(password) { | ||
const passwordArray = encoder.encode(password); | ||
const passwordKey = await crypto.subtle.importKey( | ||
"raw", passwordArray, "PBKDF2", false, ["deriveBits"] | ||
); | ||
const saltArray = crypto.getRandomValues(new Uint8Array(16)); | ||
const keyBuffer = await crypto.subtle.deriveBits( | ||
{ name: "PBKDF2", hash: "SHA-256", salt: saltArray, iterations: 1_000_000 }, | ||
passwordKey, 256 | ||
); | ||
return Buffer.from(keyBuffer).toString("base64"); | ||
} | ||
|
||
export default { | ||
async fetch(request, env, ctx) { | ||
return new Response(await pbkdf2Derive("hello!")); | ||
} | ||
} |