Repository navigation
Conversation
getDebugInfo used a bare .then() to init the chunk holding an awaited value. In DEV, ReactPromise.prototype.then wraps every subscription in a native promise and .then() doesn't return a chained Promise, so when the value is a caught server-side rejection the wrapper rejects with no handler and no way to ever attach one. Depending on timing the rejection either fails the surrounding test or kills the worker process outright. Found by the generative test added in the next commit, which reads recorded rejected values on most seeds. Co-authored-by: Claude Fable 5 <[email protected]>
|
Comparing: dcd4ec2...5a55857 Critical size changesIncludes critical production bundles, as well as any change greater than 2%:
Significant size changesIncludes any change greater than 0.2%: (No significant changes) |
gaearon
force-pushed
the
gaearon-flight-debug-fuzz
branch
4 times, most recently
from
July 26, 2026 03:07
7a2c783 to
3db1523
Compare
gaearon
force-pushed
the
gaearon-flight-debug-fuzz
branch
2 times, most recently
from
July 26, 2026 14:02
6054657 to
46e278c
Compare
Each seed builds a random Flight render shaped like a real app: components fetch their own data, share a cache()-deduped data layer, waterfall through helpers, fan out with Promise.all, pass promises down as props, render elements created by another component (sometimes deduped across two locations), and await data that was preloaded before the request. Some seeds abort mid-render, throw in a subtree, or run two renders interleaved. A small share of the graph is hostile (userland thenables, Promise subclasses). The runs are deterministic. Every random decision is drawn while the seed is built, never while it runs. Async leaves park their resolvers with a driver; a seeded loop drains everything the render scheduled, runs each leaf's real I/O to completion while nothing else is pending (so its latency can't reorder anything), then settles batches of leaves back to back in the same tick, with seeded microtask and macrotask hops in between. Leaves do real I/O (a timer or an fs read) because io entries only come from real async resources. This commit only asserts that the workload doesn't crash, on every channel including production builds. The checks on the emitted debug info come next: they are computed from what each seed's program actually did rather than recorded, so any seed can be verified, not just a committed set. To debug or explore one seed: FUZZ_TEST_SEED=<n> yarn test ReactFlightAsyncDebugInfoFuzz Co-authored-by: Claude Fable 5 <[email protected]>
The generator and driver know the ground truth for every seed: which component's body created each element and invoked each data source (a marker maintained around source calls), which real io backs each leaf, what it resolved to, and the order the driver settled things in. The emitted debug info has to agree: - Owner chains equal the recorded creator chains, all the way up, and never reach into the other interleaved render. - An io attributed to a render was initiated by that render. - Recorded values equal what the leaf resolved to; values over the 1MB limit must arrive as the omission placeholder, never raw. - An io backed by a timer never carries a file-reading stack and vice versa. - io end times are ordered like the driver's settle batches. - Time rows are monotonic, io never ends before it starts, an io with a stack has a name. - Every leaf a component directly fetched and settled appears in that render's debug info. Exemptions are enumerated where attribution is legitimately looser: combinators and parallel awaits only attribute the io that unblocked them, use() carries the transformed value, promise props dedupe into the parent's await, pass-through element returns don't surface the parent's io (TODO: expected, or a gap?), throwing subtrees never finish, aborted renders race the io. - The same io appears at most once per awaiting component (plus forks); exponential accumulation fails immediately. - performance.measure spans (recorded, not forwarded) have finite bounds, non-negative width, and a track. Since every check derives from the seed's own execution, any seed verifies, not just a committed set: FUZZ_TEST_SEED=12345 explores. Co-authored-by: Claude Fable 5 <[email protected]>
gaearon
force-pushed
the
gaearon-flight-debug-fuzz
branch
from
July 26, 2026 19:04
46e278c to
5a55857
Compare
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
I wanted to tweak some things there but I'm nervous without more coverage so I thought why not add a fuzzer.
So I asked Claude to build me one.
This adds a fuzzer for the async debug info tracking, seeded like our other fuzzers. Each seed builds a random Flight tree: components
awaiting some data,cache(), waterfalls,Promise.all, promise props, elements rendered by a different component than created them (sometimes deduped across two locations), data preloaded before the request; some seeds abort, throw in a subtree, or interleave two renders; there are also a few userland thenables and Promise subclasses. The debug info is checked against what each seed's program actually did.Details
performance.measureand check that every span has finite bounds, non-negative width, and a track.Follow-ups
Found when running this:
.then()without a rejection handler on a chunk that holds a rejected value causes an unhandled rejection that can't be caught anywhere. Debug info for caught rejections hits this on most seeds. The first commit fixes our test util's instance, but the proper fix is TODO.fs.readFile(callback form) before the request is attributed one stack frame too deep, naming the IO entry after the wrong frame. Fixed in the PR stacked on this one.ClientReferenceproxy) corrupts the component's data chunk on the client. TODO.These are fixed in #37130.