Skip to content

Commit 07d1f70

Browse files
committed
Startup profiling
1 parent b9805d7 commit 07d1f70

File tree

15 files changed

+705
-185
lines changed

15 files changed

+705
-185
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/miniflare/src/index.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -767,17 +767,17 @@ export class Miniflare {
767767
});
768768
// Add custom headers included in response to WebSocket upgrade requests
769769
this.#webSocketExtraHeaders = new WeakMap();
770-
this.#webSocketServer.on("headers", (headers, req) => {
771-
const extra = this.#webSocketExtraHeaders.get(req);
772-
this.#webSocketExtraHeaders.delete(req);
773-
if (extra) {
774-
for (const [key, value] of extra) {
775-
if (!restrictedWebSocketUpgradeHeaders.includes(key.toLowerCase())) {
776-
headers.push(`${key}: ${value}`);
777-
}
778-
}
779-
}
780-
});
770+
// this.#webSocketServer.on("headers", (headers, req) => {
771+
// const extra = this.#webSocketExtraHeaders.get(req);
772+
// this.#webSocketExtraHeaders.delete(req);
773+
// if (extra) {
774+
// for (const [key, value] of extra) {
775+
// if (!restrictedWebSocketUpgradeHeaders.includes(key.toLowerCase())) {
776+
// headers.push(`${key}: ${value}`);
777+
// }
778+
// }
779+
// }
780+
// });
781781

782782
// Build path for temporary directory. We don't actually want to create this
783783
// unless it's needed (i.e. we have Durable Objects enabled). This means we
@@ -1046,7 +1046,7 @@ export class Miniflare {
10461046
http.createServer(this.#handleLoopback),
10471047
/* grace */ 0
10481048
);
1049-
server.on("upgrade", this.#handleLoopbackUpgrade);
1049+
// server.on("upgrade", this.#handleLoopbackUpgrade);
10501050
server.listen(0, hostname, () => resolve(server));
10511051
});
10521052
}

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

Lines changed: 219 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { http, HttpResponse } from "msw";
1010
import dedent from "ts-dedent";
1111
import { File } from "undici";
1212
import { vi } from "vitest";
13+
import * as checkCommand from "../check/commands";
1314
import {
1415
printBundleSize,
1516
printOffendingDependencies,
@@ -64,6 +65,14 @@ import type { FormData } from "undici";
6465
import type { Mock } from "vitest";
6566

6667
vi.mock("command-exists");
68+
vi.mock("../check/commands", async (importOriginal) => {
69+
return {
70+
...(await importOriginal()),
71+
analyseBundle() {
72+
return `{}`;
73+
},
74+
};
75+
});
6776

6877
describe("deploy", () => {
6978
mockAccountId();
@@ -10301,6 +10310,214 @@ export default{
1030110310
});
1030210311
});
1030310312

10313+
describe("--outfile", () => {
10314+
it("should generate worker bundle at --outfile if specified", async () => {
10315+
writeWranglerConfig();
10316+
writeWorkerSource();
10317+
mockSubDomainRequest();
10318+
mockUploadWorkerRequest();
10319+
await runWrangler("deploy index.js --outfile some-dir/worker.bundle");
10320+
expect(fs.existsSync("some-dir/worker.bundle")).toBe(true);
10321+
expect(std).toMatchInlineSnapshot(`
10322+
Object {
10323+
"debug": "",
10324+
"err": "",
10325+
"info": "",
10326+
"out": "Total Upload: xx KiB / gzip: xx KiB
10327+
Worker Startup Time: 100 ms
10328+
Uploaded test-name (TIMINGS)
10329+
Deployed test-name triggers (TIMINGS)
10330+
https://test-name.test-sub-domain.workers.dev
10331+
Current Version ID: Galaxy-Class",
10332+
"warn": "",
10333+
}
10334+
`);
10335+
});
10336+
10337+
it("should include any module imports related assets in the worker bundle", async () => {
10338+
writeWranglerConfig();
10339+
fs.writeFileSync(
10340+
"./index.js",
10341+
`
10342+
import txt from './textfile.txt';
10343+
import hello from './hello.wasm';
10344+
export default{
10345+
async fetch(){
10346+
const module = await WebAssembly.instantiate(hello);
10347+
return new Response(txt + module.exports.hello);
10348+
}
10349+
}
10350+
`
10351+
);
10352+
fs.writeFileSync("./textfile.txt", "Hello, World!");
10353+
fs.writeFileSync("./hello.wasm", "Hello wasm World!");
10354+
mockSubDomainRequest();
10355+
mockUploadWorkerRequest({
10356+
expectedModules: {
10357+
"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt":
10358+
"Hello, World!",
10359+
"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm":
10360+
"Hello wasm World!",
10361+
},
10362+
});
10363+
await runWrangler("deploy index.js --outfile some-dir/worker.bundle");
10364+
10365+
expect(fs.existsSync("some-dir/worker.bundle")).toBe(true);
10366+
expect(
10367+
fs
10368+
.readFileSync("some-dir/worker.bundle", "utf8")
10369+
.replace(
10370+
/------formdata-undici-0.[0-9]*/g,
10371+
"------formdata-undici-0.test"
10372+
)
10373+
.replace(/wrangler_(.+?)_default/g, "wrangler_default")
10374+
).toMatchInlineSnapshot(`
10375+
"------formdata-undici-0.test
10376+
Content-Disposition: form-data; name=\\"metadata\\"
10377+
10378+
{\\"main_module\\":\\"index.js\\",\\"bindings\\":[],\\"compatibility_date\\":\\"2022-01-12\\",\\"compatibility_flags\\":[]}
10379+
------formdata-undici-0.test
10380+
Content-Disposition: form-data; name=\\"index.js\\"; filename=\\"index.js\\"
10381+
Content-Type: application/javascript+module
10382+
10383+
// index.js
10384+
import txt from \\"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt\\";
10385+
import hello from \\"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm\\";
10386+
var wrangler_default = {
10387+
async fetch() {
10388+
const module = await WebAssembly.instantiate(hello);
10389+
return new Response(txt + module.exports.hello);
10390+
}
10391+
};
10392+
export {
10393+
wrangler_default as default
10394+
};
10395+
//# sourceMappingURL=index.js.map
10396+
10397+
------formdata-undici-0.test
10398+
Content-Disposition: form-data; name=\\"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt\\"; filename=\\"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt\\"
10399+
Content-Type: text/plain
10400+
10401+
Hello, World!
10402+
------formdata-undici-0.test
10403+
Content-Disposition: form-data; name=\\"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm\\"; filename=\\"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm\\"
10404+
Content-Type: application/wasm
10405+
10406+
Hello wasm World!
10407+
------formdata-undici-0.test--"
10408+
`);
10409+
10410+
expect(std).toMatchInlineSnapshot(`
10411+
Object {
10412+
"debug": "",
10413+
"err": "",
10414+
"info": "",
10415+
"out": "Total Upload: xx KiB / gzip: xx KiB
10416+
Worker Startup Time: 100 ms
10417+
Uploaded test-name (TIMINGS)
10418+
Deployed test-name triggers (TIMINGS)
10419+
https://test-name.test-sub-domain.workers.dev
10420+
Current Version ID: Galaxy-Class",
10421+
"warn": "",
10422+
}
10423+
`);
10424+
});
10425+
10426+
it("should include bindings in the worker bundle", async () => {
10427+
writeWranglerConfig({
10428+
kv_namespaces: [{ binding: "KV", id: "kv-namespace-id" }],
10429+
});
10430+
fs.writeFileSync(
10431+
"./index.js",
10432+
`
10433+
import txt from './textfile.txt';
10434+
import hello from './hello.wasm';
10435+
export default{
10436+
async fetch(){
10437+
const module = await WebAssembly.instantiate(hello);
10438+
return new Response(txt + module.exports.hello);
10439+
}
10440+
}
10441+
`
10442+
);
10443+
fs.writeFileSync("./textfile.txt", "Hello, World!");
10444+
fs.writeFileSync("./hello.wasm", "Hello wasm World!");
10445+
mockSubDomainRequest();
10446+
mockUploadWorkerRequest({
10447+
expectedModules: {
10448+
"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt":
10449+
"Hello, World!",
10450+
"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm":
10451+
"Hello wasm World!",
10452+
},
10453+
});
10454+
await runWrangler("deploy index.js --outfile some-dir/worker.bundle");
10455+
10456+
expect(fs.existsSync("some-dir/worker.bundle")).toBe(true);
10457+
expect(
10458+
fs
10459+
.readFileSync("some-dir/worker.bundle", "utf8")
10460+
.replace(
10461+
/------formdata-undici-0.[0-9]*/g,
10462+
"------formdata-undici-0.test"
10463+
)
10464+
.replace(/wrangler_(.+?)_default/g, "wrangler_default")
10465+
).toMatchInlineSnapshot(`
10466+
"------formdata-undici-0.test
10467+
Content-Disposition: form-data; name=\\"metadata\\"
10468+
10469+
{\\"main_module\\":\\"index.js\\",\\"bindings\\":[{\\"name\\":\\"KV\\",\\"type\\":\\"kv_namespace\\",\\"namespace_id\\":\\"kv-namespace-id\\"}],\\"compatibility_date\\":\\"2022-01-12\\",\\"compatibility_flags\\":[]}
10470+
------formdata-undici-0.test
10471+
Content-Disposition: form-data; name=\\"index.js\\"; filename=\\"index.js\\"
10472+
Content-Type: application/javascript+module
10473+
10474+
// index.js
10475+
import txt from \\"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt\\";
10476+
import hello from \\"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm\\";
10477+
var wrangler_default = {
10478+
async fetch() {
10479+
const module = await WebAssembly.instantiate(hello);
10480+
return new Response(txt + module.exports.hello);
10481+
}
10482+
};
10483+
export {
10484+
wrangler_default as default
10485+
};
10486+
//# sourceMappingURL=index.js.map
10487+
10488+
------formdata-undici-0.test
10489+
Content-Disposition: form-data; name=\\"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt\\"; filename=\\"./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt\\"
10490+
Content-Type: text/plain
10491+
10492+
Hello, World!
10493+
------formdata-undici-0.test
10494+
Content-Disposition: form-data; name=\\"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm\\"; filename=\\"./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm\\"
10495+
Content-Type: application/wasm
10496+
10497+
Hello wasm World!
10498+
------formdata-undici-0.test--"
10499+
`);
10500+
10501+
expect(std).toMatchInlineSnapshot(`
10502+
Object {
10503+
"debug": "",
10504+
"err": "",
10505+
"info": "",
10506+
"out": "Total Upload: xx KiB / gzip: xx KiB
10507+
Worker Startup Time: 100 ms
10508+
Your worker has access to the following bindings:
10509+
- KV Namespaces:
10510+
- KV: kv-namespace-id
10511+
Uploaded test-name (TIMINGS)
10512+
Deployed test-name triggers (TIMINGS)
10513+
https://test-name.test-sub-domain.workers.dev
10514+
Current Version ID: Galaxy-Class",
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({
@@ -10864,35 +11081,9 @@ export default{
1086411081
main: "index.js",
1086511082
});
1086611083

10867-
await expect(runWrangler("deploy")).rejects.toMatchInlineSnapshot(
10868-
`[APIError: A request to the Cloudflare API (/accounts/some-account-id/workers/scripts/test-name/versions) failed.]`
11084+
await expect(runWrangler("deploy")).rejects.toThrowError(
11085+
`Your Worker failed validation because it exceeded startup limits.`
1086911086
);
10870-
expect(std).toMatchInlineSnapshot(`
10871-
Object {
10872-
"debug": "",
10873-
"err": "",
10874-
"info": "",
10875-
"out": "Total Upload: xx KiB / gzip: xx KiB
10876-
10877-
X [ERROR] A request to the Cloudflare API (/accounts/some-account-id/workers/scripts/test-name/versions) failed.
10878-
10879-
Error: Script startup exceeded CPU time limit. [code: 10021]
10880-
10881-
If you think this is a bug, please open an issue at:
10882-
https://github.com/cloudflare/workers-sdk/issues/new/choose
10883-
10884-
",
10885-
"warn": "▲ [WARNING] Your Worker failed validation because it exceeded startup limits.
10886-
10887-
To ensure fast responses, we place constraints on Worker startup -- like how much CPU it can use,
10888-
or how long it can take.
10889-
Your Worker failed validation, which means it hit one of these startup limits.
10890-
Try reducing the amount of work done during startup (outside the event handler), either by
10891-
removing code or relocating it inside the event handler.
10892-
10893-
",
10894-
}
10895-
`);
1089611087
});
1089711088

1089811089
describe("unit tests", () => {

0 commit comments

Comments
 (0)