Skip to content

Commit

Permalink
EW-8447 Add regression test for CPU profiling
Browse files Browse the repository at this point in the history
This commit adds a regression test for #1754, based on @mrbbot's original reproduction.
  • Loading branch information
harrishancock committed Aug 20, 2024
1 parent 3374dd6 commit b972f43
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 0 deletions.
19 changes: 19 additions & 0 deletions src/workerd/server/tests/inspector/BUILD.bazel
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"],
)

19 changes: 19 additions & 0 deletions src/workerd/server/tests/inspector/config.capnp
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"],
);
89 changes: 89 additions & 0 deletions src/workerd/server/tests/inspector/driver.mjs
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.equal(code, 0);
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(max.count > 0);
});
23 changes: 23 additions & 0 deletions src/workerd/server/tests/inspector/index.mjs
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!"));
}
}

0 comments on commit b972f43

Please sign in to comment.