-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Allow JIT fast-path on Cloudflare Workers when new Function is available (microtask approach) #5565
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
+92
−15
Closed
Changes from 1 commit
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
2e5dde0
Allow JIT fast-path on Cloudflare Workers when Function is available …
southpolesteve 0b1f513
Scope to just cloudflare workers
southpolesteve 26cd659
Respond to github comments
southpolesteve 8a5d0c4
More simplifications
southpolesteve 65758bb
Simplify further
southpolesteve 1bd4ee7
Cleanup
southpolesteve 5039b1b
use microtasks for speedup
mattzcarey 7b6a31f
return object for non cf
mattzcarey 882e29c
inline condition
mattzcarey 9bad279
more tests
mattzcarey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
use microtasks for speedup
- Loading branch information
commit 5039b1b5de37e0d27f6235827abcac882a6405d0
There are no files selected for viewing
This file contains hidden or 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,139 @@ | ||
| import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; | ||
|
|
||
| const state = vi.hoisted(() => { | ||
| Object.defineProperty(globalThis, "navigator", { | ||
| value: { userAgent: "Cloudflare-Workers" }, | ||
| configurable: true, | ||
| writable: true, | ||
| }); | ||
|
|
||
| const userAgent = typeof navigator === "undefined" ? undefined : navigator?.userAgent; | ||
| const isCloudflare = typeof userAgent === "string" && userAgent.includes("Cloudflare"); | ||
|
|
||
| let _allowsEval: boolean; | ||
| try { | ||
| const F = Function; | ||
| new F(""); | ||
| _allowsEval = true; | ||
| } catch (_) { | ||
| _allowsEval = false; | ||
| } | ||
|
|
||
| const valueBeforeMicrotask = _allowsEval; | ||
|
|
||
| if (isCloudflare && _allowsEval) { | ||
| Promise.resolve().then(() => { | ||
| _allowsEval = false; | ||
| }); | ||
| } | ||
|
|
||
| return { | ||
| isCloudflare, | ||
| valueBeforeMicrotask, | ||
| getAllowsEval: () => _allowsEval, | ||
| }; | ||
| }); | ||
|
|
||
| describe("Cloudflare JIT microtask timing", () => { | ||
| test("allowsEval is true during sync, false after microtask", () => { | ||
| expect(state.isCloudflare).toBe(true); | ||
| expect(state.valueBeforeMicrotask).toBe(true); | ||
| expect(state.getAllowsEval()).toBe(false); | ||
| }); | ||
|
|
||
| test("util.allowsEval is false after module load", async () => { | ||
| vi.resetModules(); | ||
| const { allowsEval } = await import("../../core/util.js"); | ||
| expect(allowsEval.value).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Cloudflare allowsEval behavior", () => { | ||
| let navigatorDescriptor: PropertyDescriptor | undefined; | ||
|
|
||
| beforeEach(() => { | ||
| vi.resetModules(); | ||
| navigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, "navigator"); | ||
| Object.defineProperty(globalThis, "navigator", { | ||
| value: { userAgent: "Cloudflare-Workers" }, | ||
| configurable: true, | ||
| writable: true, | ||
| }); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| if (navigatorDescriptor) { | ||
| Object.defineProperty(globalThis, "navigator", navigatorDescriptor); | ||
| } else { | ||
| delete (globalThis as any).navigator; | ||
| } | ||
| vi.resetModules(); | ||
| }); | ||
|
|
||
| const loadAllowsEval = async () => { | ||
| const mod = await import("../../core/util.js"); | ||
| return mod.allowsEval.value; | ||
| }; | ||
|
|
||
| test("disables fast path after startup", async () => { | ||
| expect(await loadAllowsEval()).toBe(false); | ||
| }); | ||
|
|
||
| test("caches value instead of re-evaluating", async () => { | ||
| const first = await loadAllowsEval(); | ||
| expect(first).toBe(false); | ||
|
|
||
| vi.stubGlobal("Function", function ThrowingFunction() { | ||
| throw new Error("Function constructor disabled"); | ||
| }); | ||
|
|
||
| const second = await loadAllowsEval(); | ||
| expect(second).toBe(first); | ||
| }); | ||
|
|
||
| test("stays disabled when eval is disabled from start", async () => { | ||
| vi.stubGlobal("Function", function ThrowingFunction() { | ||
| throw new Error("Function constructor disabled"); | ||
| }); | ||
|
|
||
| vi.resetModules(); | ||
| expect(await loadAllowsEval()).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("non-Cloudflare allowsEval behavior", () => { | ||
| beforeEach(() => { | ||
| vi.resetModules(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| vi.resetModules(); | ||
| }); | ||
|
|
||
| const loadAllowsEval = async () => { | ||
| const mod = await import("../../core/util.js"); | ||
| return mod.allowsEval.value; | ||
| }; | ||
|
|
||
| test("falls back when eval is disabled", async () => { | ||
| vi.stubGlobal("Function", function ThrowingFunction() { | ||
| throw new Error("Function constructor disabled"); | ||
| }); | ||
|
|
||
| vi.resetModules(); | ||
| expect(await loadAllowsEval()).toBe(false); | ||
| }); | ||
|
|
||
| test("caches value", async () => { | ||
| const first = await loadAllowsEval(); | ||
|
|
||
| vi.stubGlobal("Function", function ThrowingFunction() { | ||
| throw new Error("Function constructor disabled"); | ||
| }); | ||
|
|
||
| const second = await loadAllowsEval(); | ||
| expect(second).toBe(first); | ||
| }); | ||
| }); |
This file contains hidden or 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
This file contains hidden or 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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's problematic that this is eagerly attempted. It'll throw a warning/error in various environments. Zod currently avoids ever using the function constructor if
z.config({ jitless: true })has been set to avoid spamming users with warnings in these environments. You can still attempt this during module initialization but only when you have high confidence (based onnavigatorthat the code is executing in Cloudflare.Though more broadly—as I understand it
new Function()is only allowed during module initialization, so this doesn't actually help. Zod usesnew Function()insidegetFastPasswhen an object schema is parsing for the first tiem. At that point we're out of module initialization so Zod won't be able to properly JIT] this anyway. Your benchmark should include an object parse operation - it would reveal this.Thinking about this more—there's no guarantee that a Zod schema is defined top-level either so getting Zod JIT to work is just not really possible in the general case until
new Functioncan be enabled throughout execution.Let me know if I'm missing something. Maybe anonrig has thoughts.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, will take that on and have a think :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahhh this is rough. thanks, explains Sunil's comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
closing this :)