Skip to content

Commit 682fc9b

Browse files
committed
Startup profiling
1 parent b9805d7 commit 682fc9b

File tree

14 files changed

+687
-145
lines changed

14 files changed

+687
-145
lines changed

.changeset/rich-pots-mate.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Add `--outfile` to `wrangler deploy` for generating a worker bundle.
6+
7+
This is an advanced feature that most users won't need to use. When set, Wrangler will output your built Worker bundle in a Cloudflare specific format that captures all information needed to deploy a Worker using the [Worker Upload API](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/)

.changeset/startup-profiling.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Add a `wrangler check startup` command to generate a CPU profile of your Worker's startup phase.
6+
7+
This can be imported into Chrome DevTools or opened directly in VSCode to view a flamegraph of your Worker's startup phase. Additionally, when a Worker deployment fails with a startup time error Wrangler will automatically generate a CPU profile for easy investigation.
8+
9+
Advanced usage:
10+
11+
- `--deploy-args`: to customise the way `wrangler check startup` builds your Worker for analysis, provide the exact arguments you use when deploying your Worker with `wrangler deploy`. For instance, if you deploy your Worker with `wrangler deploy --no-bundle`, you should use `wrangler check startup --deploy-args="--no-bundle"` to profile the startup phase.
12+
- `--worker-bundle`: if you don't use Wrangler to deploy your Worker, you can use this argument to provide a Worker bundle to analyse. This should be a file path to a serialised multipart upload, with the exact same format as the API expects: https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/

packages/wrangler/src/__tests__/deploy.test.ts

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10301,6 +10301,223 @@ export default{
1030110301
});
1030210302
});
1030310303

10304+
describe("--outfile", () => {
10305+
it("should generate worker bundle at --outfile if specified", async () => {
10306+
writeWranglerConfig();
10307+
writeWorkerSource();
10308+
mockSubDomainRequest();
10309+
mockUploadWorkerRequest();
10310+
await runWrangler("deploy index.js --outfile some-dir/worker.bundle");
10311+
expect(fs.existsSync("some-dir/worker.bundle")).toBe(true);
10312+
expect(std).toMatchInlineSnapshot(`
10313+
Object {
10314+
"debug": "",
10315+
"err": "",
10316+
"info": "",
10317+
"out": "Total Upload: xx KiB / gzip: xx KiB
10318+
Uploaded test-name (TIMINGS)
10319+
Published test-name (TIMINGS)
10320+
https://test-name.test-sub-domain.workers.dev
10321+
Current Deployment ID: Galaxy-Class
10322+
Current Version ID: Galaxy-Class
10323+
10324+
10325+
Note: Deployment ID has been renamed to Version ID. Deployment ID is present to maintain compatibility with the previous behavior of this command. This output will change in a future version of Wrangler. To learn more visit: https://developers.cloudflare.com/workers/configuration/versions-and-deployments",
10326+
"warn": "",
10327+
}
10328+
`);
10329+
});
10330+
10331+
it("should include any module imports related assets in the worker bundle", async () => {
10332+
writeWranglerConfig();
10333+
fs.writeFileSync(
10334+
"./index.js",
10335+
`
10336+
import txt from './textfile.txt';
10337+
import hello from './hello.wasm';
10338+
export default{
10339+
async fetch(){
10340+
const module = await WebAssembly.instantiate(hello);
10341+
return new Response(txt + module.exports.hello);
10342+
}
10343+
}
10344+
`
10345+
);
10346+
fs.writeFileSync("./textfile.txt", "Hello, World!");
10347+
fs.writeFileSync("./hello.wasm", "Hello wasm World!");
10348+
mockSubDomainRequest();
10349+
mockUploadWorkerRequest({
10350+
expectedModules: {
10351+
"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt":
10352+
"Hello, World!",
10353+
"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm":
10354+
"Hello wasm World!",
10355+
},
10356+
});
10357+
await runWrangler("deploy index.js --outfile some-dir/worker.bundle");
10358+
10359+
expect(fs.existsSync("some-dir/worker.bundle")).toBe(true);
10360+
expect(
10361+
fs
10362+
.readFileSync("some-dir/worker.bundle", "utf8")
10363+
.replace(
10364+
/------formdata-undici-0.[0-9]*/g,
10365+
"------formdata-undici-0.test"
10366+
)
10367+
.replace(/wrangler_(.+?)_default/g, "wrangler_default")
10368+
).toMatchInlineSnapshot(`
10369+
"------formdata-undici-0.test
10370+
Content-Disposition: form-data; name=\\"metadata\\"
10371+
10372+
{\\"main_module\\":\\"index.js\\",\\"bindings\\":[],\\"compatibility_date\\":\\"2022-01-12\\",\\"compatibility_flags\\":[]}
10373+
------formdata-undici-0.test
10374+
Content-Disposition: form-data; name=\\"index.js\\"; filename=\\"index.js\\"
10375+
Content-Type: application/javascript+module
10376+
10377+
// index.js
10378+
import txt from \\"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt\\";
10379+
import hello from \\"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm\\";
10380+
var wrangler_default = {
10381+
async fetch() {
10382+
const module = await WebAssembly.instantiate(hello);
10383+
return new Response(txt + module.exports.hello);
10384+
}
10385+
};
10386+
export {
10387+
wrangler_default as default
10388+
};
10389+
//# sourceMappingURL=index.js.map
10390+
10391+
------formdata-undici-0.test
10392+
Content-Disposition: form-data; name=\\"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt\\"; filename=\\"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt\\"
10393+
Content-Type: text/plain
10394+
10395+
Hello, World!
10396+
------formdata-undici-0.test
10397+
Content-Disposition: form-data; name=\\"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm\\"; filename=\\"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm\\"
10398+
Content-Type: application/wasm
10399+
10400+
Hello wasm World!
10401+
------formdata-undici-0.test--"
10402+
`);
10403+
10404+
expect(std).toMatchInlineSnapshot(`
10405+
Object {
10406+
"debug": "",
10407+
"err": "",
10408+
"info": "",
10409+
"out": "Total Upload: xx KiB / gzip: xx KiB
10410+
Uploaded test-name (TIMINGS)
10411+
Published test-name (TIMINGS)
10412+
https://test-name.test-sub-domain.workers.dev
10413+
Current Deployment ID: Galaxy-Class
10414+
Current Version ID: Galaxy-Class
10415+
10416+
10417+
Note: Deployment ID has been renamed to Version ID. Deployment ID is present to maintain compatibility with the previous behavior of this command. This output will change in a future version of Wrangler. To learn more visit: https://developers.cloudflare.com/workers/configuration/versions-and-deployments",
10418+
"warn": "",
10419+
}
10420+
`);
10421+
});
10422+
10423+
it("should include bindings in the worker bundle", async () => {
10424+
writeWranglerConfig({
10425+
kv_namespaces: [{ binding: "KV", id: "kv-namespace-id" }],
10426+
});
10427+
fs.writeFileSync(
10428+
"./index.js",
10429+
`
10430+
import txt from './textfile.txt';
10431+
import hello from './hello.wasm';
10432+
export default{
10433+
async fetch(){
10434+
const module = await WebAssembly.instantiate(hello);
10435+
return new Response(txt + module.exports.hello);
10436+
}
10437+
}
10438+
`
10439+
);
10440+
fs.writeFileSync("./textfile.txt", "Hello, World!");
10441+
fs.writeFileSync("./hello.wasm", "Hello wasm World!");
10442+
mockSubDomainRequest();
10443+
mockUploadWorkerRequest({
10444+
expectedModules: {
10445+
"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt":
10446+
"Hello, World!",
10447+
"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm":
10448+
"Hello wasm World!",
10449+
},
10450+
});
10451+
await runWrangler("deploy index.js --outfile some-dir/worker.bundle");
10452+
10453+
expect(fs.existsSync("some-dir/worker.bundle")).toBe(true);
10454+
expect(
10455+
fs
10456+
.readFileSync("some-dir/worker.bundle", "utf8")
10457+
.replace(
10458+
/------formdata-undici-0.[0-9]*/g,
10459+
"------formdata-undici-0.test"
10460+
)
10461+
.replace(/wrangler_(.+?)_default/g, "wrangler_default")
10462+
).toMatchInlineSnapshot(`
10463+
"------formdata-undici-0.test
10464+
Content-Disposition: form-data; name=\\"metadata\\"
10465+
10466+
{\\"main_module\\":\\"index.js\\",\\"bindings\\":[{\\"name\\":\\"KV\\",\\"type\\":\\"kv_namespace\\",\\"namespace_id\\":\\"kv-namespace-id\\"}],\\"compatibility_date\\":\\"2022-01-12\\",\\"compatibility_flags\\":[]}
10467+
------formdata-undici-0.test
10468+
Content-Disposition: form-data; name=\\"index.js\\"; filename=\\"index.js\\"
10469+
Content-Type: application/javascript+module
10470+
10471+
// index.js
10472+
import txt from \\"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt\\";
10473+
import hello from \\"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm\\";
10474+
var wrangler_default = {
10475+
async fetch() {
10476+
const module = await WebAssembly.instantiate(hello);
10477+
return new Response(txt + module.exports.hello);
10478+
}
10479+
};
10480+
export {
10481+
wrangler_default as default
10482+
};
10483+
//# sourceMappingURL=index.js.map
10484+
10485+
------formdata-undici-0.test
10486+
Content-Disposition: form-data; name=\\"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt\\"; filename=\\"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt\\"
10487+
Content-Type: text/plain
10488+
10489+
Hello, World!
10490+
------formdata-undici-0.test
10491+
Content-Disposition: form-data; name=\\"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm\\"; filename=\\"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm\\"
10492+
Content-Type: application/wasm
10493+
10494+
Hello wasm World!
10495+
------formdata-undici-0.test--"
10496+
`);
10497+
10498+
expect(std).toMatchInlineSnapshot(`
10499+
Object {
10500+
"debug": "",
10501+
"err": "",
10502+
"info": "",
10503+
"out": "Total Upload: xx KiB / gzip: xx KiB
10504+
Your worker has access to the following bindings:
10505+
- KV Namespaces:
10506+
- KV: kv-namespace-id
10507+
Uploaded test-name (TIMINGS)
10508+
Published test-name (TIMINGS)
10509+
https://test-name.test-sub-domain.workers.dev
10510+
Current Deployment ID: Galaxy-Class
10511+
Current Version ID: Galaxy-Class
10512+
10513+
10514+
Note: Deployment ID has been renamed to Version ID. Deployment ID is present to maintain compatibility with the previous behavior of this command. This output will change in a future version of Wrangler. To learn more visit: https://developers.cloudflare.com/workers/configuration/versions-and-deployments",
10515+
"warn": "",
10516+
}
10517+
`);
10518+
});
10519+
});
10520+
1030410521
describe("--dry-run", () => {
1030510522
it("should not deploy the worker if --dry-run is specified", async () => {
1030610523
writeWranglerConfig({
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { readFile } from "node:fs/promises";
2+
import { describe, expect, test } from "vitest";
3+
import { collectCLIOutput } from "./helpers/collect-cli-output";
4+
import { mockConsoleMethods } from "./helpers/mock-console";
5+
import { useMockIsTTY } from "./helpers/mock-istty";
6+
import { runInTempDir } from "./helpers/run-in-tmp";
7+
import { runWrangler } from "./helpers/run-wrangler";
8+
import { writeWorkerSource } from "./helpers/write-worker-source";
9+
import { writeWranglerConfig } from "./helpers/write-wrangler-config";
10+
11+
describe("wrangler check startup", () => {
12+
mockConsoleMethods();
13+
const std = collectCLIOutput();
14+
runInTempDir();
15+
const { setIsTTY } = useMockIsTTY();
16+
setIsTTY(false);
17+
18+
test("generates profile for basic worker", async () => {
19+
writeWranglerConfig({ main: "index.js" });
20+
writeWorkerSource();
21+
22+
await runWrangler("check startup");
23+
24+
expect(std.out).toContain(
25+
`CPU Profile written to worker-startup.cpuprofile`
26+
);
27+
28+
await expect(
29+
readFile("worker-startup.cpuprofile", "utf8")
30+
).resolves.toContain("callFrame");
31+
});
32+
test("--outfile works", async () => {
33+
writeWranglerConfig({ main: "index.js" });
34+
writeWorkerSource();
35+
36+
await runWrangler("check startup --outfile worker.cpuprofile");
37+
38+
expect(std.out).toContain(`CPU Profile written to worker.cpuprofile`);
39+
});
40+
test("--deploy-args passed through to deploy", async () => {
41+
writeWranglerConfig({ main: "index.js" });
42+
writeWorkerSource();
43+
44+
await expect(
45+
runWrangler("check startup --deploy-args 'abc'")
46+
).rejects.toThrowErrorMatchingInlineSnapshot(
47+
`[Error: The entry-point file at "abc" was not found.]`
48+
);
49+
});
50+
51+
test("--worker-bundle is used instead of building", async () => {
52+
writeWranglerConfig({ main: "index.js" });
53+
writeWorkerSource();
54+
55+
await runWrangler("deploy --dry-run --outfile worker.bundle");
56+
57+
await expect(readFile("worker.bundle", "utf8")).resolves.toContain(
58+
"main_module"
59+
);
60+
await runWrangler("check startup --worker-bundle worker.bundle");
61+
expect(std.out).not.toContain(`Building your Worker`);
62+
63+
await expect(
64+
readFile("worker-startup.cpuprofile", "utf8")
65+
).resolves.toContain("callFrame");
66+
});
67+
});

0 commit comments

Comments
 (0)