Skip to content
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

[Fizz] deterministic text separators #24637

Closed
wants to merge 4 commits into from
Closed

Conversation

gnoff
Copy link
Collaborator

@gnoff gnoff commented May 29, 2022

This change addresses two related flaws in the current approach to text separators in Fizz.

First text separators can be emitted more often than necessary. This is because Segments don't know whether they will be followed by text and so if they end in text they assume they are and insert a separators

Second, because of the flaw above, Fizz output is not deterministic. If you wait for everything to be ready before flushing you could still end up with different output depending on whether certain segments are created or rendered inline based on the particular behavior of Suspendable components for any given run. If a Segment emits an unnecessary text separator in one pass and in another the Segment is never created because that Component did not Suspend the string output would differ for otherwise identical content

This change fixes both issues by decorating Segments with additional metadata about what kinds of emitted content is at it's bounds. These are called Edges and can be NODE, TEXT, or a Segment. NODE and TEXT are type identifiers that refer generically to these mutually exclusive concepts. if an edge is a Segment however it is the actual reference to the Segment object, which allows us to make inferences at flushing time as to whether additional text separators are needed.

There are 2 tracked edges per Segment

currentEdge - a pointer to type of the prior thing emitted for a Segment. If the segment has not yet been worked on this will be the Edge just before the Segment start. If the Segment is currently being rendered it will be the type or reference of the last thing this Segment emitted. If the Segment is complete it definitionally be the trailing edge of the Segment representing the type emitted at the boundary.

followingEdge - this edge is set by later Segments when the currentEdge of that segment is a Segment. If the currentEdge is a Segment and a new Edge is identified it is saved to the followingEdge of the currentEdge segment.

Finally, when flushing 2 things happen

If the just written Segment ends with text (currentEdge is TEXT_NODE) and if the followingEdge is also text, then a separator is written in between. If the followingEdge is instead another Completed Segment this segment is empty and we walk to that Segment's followingEdge and continue looking for Text.

This change addresses two related flaws in the current approach to text separators in Fizz.

First text separators can be emitted more often than necessary. This is because Segments don't know whether they will be followed by text and so if they end in text they assume they are and insert a separators

Second, becuase of the flaw above, Fizz output is not deterministic. If you wait for everything to be ready before flushing you could still end up with different output depending on whether certain segments are created or rendered inline based on the particular behavior of Suspendable components for any given run. If a Segment emits an unecessary text separator in one pass and in another the Segment is never created because that Component did not Suspend the string output would differ for otherwise identical content

This change fixes both issues by decorating Segments with additional metadata about what kinds of emitted content is at it's bounds. These are called Edges and can be NODE, TEXT, or a Segment. NODE and TEXT are type identifiers that refer generically to these mutually exclusive concepts. if an edge is a Segment however it is the actual reference to the Segment object, which allows us to make inferrences at flushing time as to whether additional text separators are needed.

There are 2 tracked edges per Segment

currentEdge - a pointer to type of the prior thing emitted for a Segment. If the segment has not yet been worked on this will be the Edge just before the Segment start. If the Segment is currently being rendered it will be the type or reference of the last thing this Segment emitted. If the Segment is complete it definitionally be the trailing edge of the Segment representing the type emitted at the boundary.

followingEdge - this edge is set by later Segments when the currentEdge of that segment is a Segment. If the currentEdge is a Segment and a new Edge is identified it is saved to the followingEdge of the currentEdge segment.

Finally, when flushing 2 things happen

If the just written Segment ends with text (currentEdge is TEXT_NODE) and if the followingEdge is also text, then a separator is written in between. If the followingEdge is instead another Completed Segment this segment is empty and we walk to that Segment's followingEdge and continue looking for Text.
@sizebot
Copy link

sizebot commented May 29, 2022

Comparing: a276638...e68ac36

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 131.28 kB 131.28 kB = 42.13 kB 42.13 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 136.54 kB 136.54 kB = 43.68 kB 43.68 kB
facebook-www/ReactDOM-prod.classic.js = 439.35 kB 439.35 kB = 80.29 kB 80.29 kB
facebook-www/ReactDOM-prod.modern.js = 424.64 kB 424.64 kB = 78.13 kB 78.13 kB
facebook-www/ReactDOMForked-prod.classic.js = 439.35 kB 439.35 kB = 80.29 kB 80.29 kB
oss-stable-semver/react-server/cjs/react-server.production.min.js +3.80% 20.36 kB 21.13 kB +2.64% 6.97 kB 7.15 kB
oss-stable/react-server/cjs/react-server.production.min.js +3.80% 20.36 kB 21.13 kB +2.64% 6.97 kB 7.15 kB
oss-experimental/react-server/cjs/react-server.production.min.js +3.74% 20.69 kB 21.46 kB +2.67% 7.07 kB 7.26 kB
oss-stable-semver/react-server/cjs/react-server.development.js +2.72% 126.28 kB 129.72 kB +2.08% 31.63 kB 32.29 kB
oss-stable/react-server/cjs/react-server.development.js +2.72% 126.28 kB 129.72 kB +2.08% 31.63 kB 32.29 kB
oss-experimental/react-server/cjs/react-server.development.js +2.70% 127.15 kB 130.59 kB +2.07% 31.83 kB 32.49 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.browser.production.min.js +2.32% 32.96 kB 33.72 kB +1.61% 10.84 kB 11.02 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.browser.production.min.js +2.32% 32.98 kB 33.75 kB +1.61% 10.86 kB 11.04 kB
oss-stable-semver/react-dom/umd/react-dom-server-legacy.browser.production.min.js +2.30% 33.11 kB 33.87 kB +1.46% 11.00 kB 11.16 kB
oss-stable/react-dom/umd/react-dom-server-legacy.browser.production.min.js +2.30% 33.13 kB 33.89 kB +1.46% 11.02 kB 11.18 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.production.min.js +2.29% 33.40 kB 34.17 kB +1.58% 11.01 kB 11.18 kB
oss-experimental/react-dom/umd/react-dom-server-legacy.browser.production.min.js +2.27% 33.55 kB 34.31 kB +1.38% 11.16 kB 11.31 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.production.min.js +2.22% 34.09 kB 34.85 kB +1.66% 11.59 kB 11.78 kB
oss-stable/react-dom/cjs/react-dom-server.browser.production.min.js +2.22% 34.11 kB 34.87 kB +1.65% 11.61 kB 11.81 kB
oss-stable-semver/react-dom/umd/react-dom-server.browser.production.min.js +2.20% 34.22 kB 34.97 kB +1.68% 11.72 kB 11.91 kB
oss-stable/react-dom/umd/react-dom-server.browser.production.min.js +2.20% 34.24 kB 34.99 kB +1.69% 11.74 kB 11.94 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.production.min.js +2.19% 34.53 kB 35.29 kB +1.62% 11.76 kB 11.95 kB
oss-experimental/react-dom/umd/react-dom-server.browser.production.min.js +2.17% 34.66 kB 35.41 kB +1.63% 11.87 kB 12.06 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.node.production.min.js +2.08% 36.65 kB 37.41 kB +1.32% 12.08 kB 12.24 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.node.production.min.js +2.08% 36.67 kB 37.43 kB +1.32% 12.10 kB 12.26 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.production.min.js +2.06% 37.15 kB 37.91 kB +1.40% 12.26 kB 12.43 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.production.min.js +2.02% 37.36 kB 38.11 kB +1.35% 12.66 kB 12.83 kB
oss-stable/react-dom/cjs/react-dom-server.node.production.min.js +2.02% 37.38 kB 38.14 kB +1.34% 12.68 kB 12.85 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable-semver/react-server/cjs/react-server.production.min.js +3.80% 20.36 kB 21.13 kB +2.64% 6.97 kB 7.15 kB
oss-stable/react-server/cjs/react-server.production.min.js +3.80% 20.36 kB 21.13 kB +2.64% 6.97 kB 7.15 kB
oss-experimental/react-server/cjs/react-server.production.min.js +3.74% 20.69 kB 21.46 kB +2.67% 7.07 kB 7.26 kB
oss-stable-semver/react-server/cjs/react-server.development.js +2.72% 126.28 kB 129.72 kB +2.08% 31.63 kB 32.29 kB
oss-stable/react-server/cjs/react-server.development.js +2.72% 126.28 kB 129.72 kB +2.08% 31.63 kB 32.29 kB
oss-experimental/react-server/cjs/react-server.development.js +2.70% 127.15 kB 130.59 kB +2.07% 31.83 kB 32.49 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.browser.production.min.js +2.32% 32.96 kB 33.72 kB +1.61% 10.84 kB 11.02 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.browser.production.min.js +2.32% 32.98 kB 33.75 kB +1.61% 10.86 kB 11.04 kB
oss-stable-semver/react-dom/umd/react-dom-server-legacy.browser.production.min.js +2.30% 33.11 kB 33.87 kB +1.46% 11.00 kB 11.16 kB
oss-stable/react-dom/umd/react-dom-server-legacy.browser.production.min.js +2.30% 33.13 kB 33.89 kB +1.46% 11.02 kB 11.18 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.production.min.js +2.29% 33.40 kB 34.17 kB +1.58% 11.01 kB 11.18 kB
oss-experimental/react-dom/umd/react-dom-server-legacy.browser.production.min.js +2.27% 33.55 kB 34.31 kB +1.38% 11.16 kB 11.31 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.production.min.js +2.22% 34.09 kB 34.85 kB +1.66% 11.59 kB 11.78 kB
oss-stable/react-dom/cjs/react-dom-server.browser.production.min.js +2.22% 34.11 kB 34.87 kB +1.65% 11.61 kB 11.81 kB
oss-stable-semver/react-dom/umd/react-dom-server.browser.production.min.js +2.20% 34.22 kB 34.97 kB +1.68% 11.72 kB 11.91 kB
oss-stable/react-dom/umd/react-dom-server.browser.production.min.js +2.20% 34.24 kB 34.99 kB +1.69% 11.74 kB 11.94 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.production.min.js +2.19% 34.53 kB 35.29 kB +1.62% 11.76 kB 11.95 kB
oss-experimental/react-dom/umd/react-dom-server.browser.production.min.js +2.17% 34.66 kB 35.41 kB +1.63% 11.87 kB 12.06 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.node.production.min.js +2.08% 36.65 kB 37.41 kB +1.32% 12.08 kB 12.24 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.node.production.min.js +2.08% 36.67 kB 37.43 kB +1.32% 12.10 kB 12.26 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.production.min.js +2.06% 37.15 kB 37.91 kB +1.40% 12.26 kB 12.43 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.production.min.js +2.02% 37.36 kB 38.11 kB +1.35% 12.66 kB 12.83 kB
oss-stable/react-dom/cjs/react-dom-server.node.production.min.js +2.02% 37.38 kB 38.14 kB +1.34% 12.68 kB 12.85 kB
oss-experimental/react-dom/cjs/react-dom-server.node.production.min.js +1.99% 37.86 kB 38.61 kB +1.48% 12.84 kB 13.03 kB
facebook-www/ReactDOMServer-prod.modern.js +1.92% 75.71 kB 77.16 kB +1.83% 15.74 kB 16.02 kB
facebook-www/ReactDOMServerStreaming-prod.modern.js +1.84% 79.24 kB 80.70 kB +1.66% 16.78 kB 17.06 kB
facebook-www/ReactDOMServerStreaming-dev.modern.js +1.45% 234.94 kB 238.35 kB +1.12% 56.26 kB 56.89 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.development.js +1.41% 232.83 kB 236.12 kB +1.05% 56.93 kB 57.52 kB
oss-stable/react-dom/cjs/react-dom-server.browser.development.js +1.41% 232.85 kB 236.14 kB +1.05% 56.95 kB 57.55 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.development.js +1.40% 233.99 kB 237.28 kB +1.05% 56.86 kB 57.46 kB
oss-stable/react-dom/cjs/react-dom-server.node.development.js +1.40% 234.02 kB 237.30 kB +1.05% 56.89 kB 57.48 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.development.js +1.40% 234.40 kB 237.69 kB +1.04% 57.33 kB 57.93 kB
oss-stable-semver/react-dom/umd/react-dom-server.browser.development.js +1.40% 244.32 kB 247.73 kB +1.03% 57.54 kB 58.13 kB
oss-stable/react-dom/umd/react-dom-server.browser.development.js +1.40% 244.34 kB 247.75 kB +1.03% 57.56 kB 58.16 kB
oss-experimental/react-dom/cjs/react-dom-server.node.development.js +1.40% 235.56 kB 238.85 kB +1.04% 57.27 kB 57.86 kB
oss-experimental/react-dom/umd/react-dom-server.browser.development.js +1.39% 245.98 kB 249.39 kB +1.04% 57.94 kB 58.54 kB
facebook-www/ReactDOMServer-dev.modern.js +1.39% 238.42 kB 241.73 kB +1.03% 57.05 kB 57.64 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.browser.development.js +1.38% 233.23 kB 236.45 kB +1.00% 56.66 kB 57.23 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.browser.development.js +1.38% 233.26 kB 236.47 kB +1.00% 56.69 kB 57.25 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.development.js +1.37% 234.80 kB 238.02 kB +0.99% 57.11 kB 57.67 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.node.development.js +1.37% 234.94 kB 238.16 kB +1.00% 57.13 kB 57.70 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.node.development.js +1.37% 234.97 kB 238.18 kB +1.00% 57.16 kB 57.73 kB
oss-stable-semver/react-dom/umd/react-dom-server-legacy.browser.development.js +1.36% 244.75 kB 248.09 kB +0.98% 57.31 kB 57.88 kB
oss-stable/react-dom/umd/react-dom-server-legacy.browser.development.js +1.36% 244.78 kB 248.12 kB +0.99% 57.34 kB 57.90 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.development.js +1.36% 236.51 kB 239.73 kB +0.99% 57.58 kB 58.15 kB
oss-experimental/react-dom/umd/react-dom-server-legacy.browser.development.js +1.36% 246.41 kB 249.75 kB +1.00% 57.71 kB 58.29 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-server.development.js = 6.16 kB 6.10 kB = 1.71 kB 1.68 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-server.development.js = 6.16 kB 6.10 kB = 1.71 kB 1.68 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-server.development.js = 6.16 kB 6.10 kB = 1.71 kB 1.68 kB

Generated by 🚫 dangerJS against e68ac36

@gnoff gnoff requested a review from sebmarkbage May 29, 2022 22:06
Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should do the object reference thing. It'll be tricky to do with true parallelism since the objects will span threads, and for reuse of segments across caches.

I don't think you should need that though because during flush you'll always have access to what you've just written and it's sequential at that point so there are no parallelization issues.

You can keep track of if the flushSegment just flushed a text edge in flushSubtree and if so insert a separator.

// A SegmentEdge is a descriptor for the kind of object on either side of a segments boundary (leading and trailing).
// In some cases the Edge type isn't known because the edge is a child segment itself. For these cases we use
// the child segment to identify as the edge unless and until a known edge type is found
type SegmentEdge = 0 | 1 | Segment;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't mix number and pointer types. This will mess up the JIT. Always use types as you would in Rust. (This means that we should really not mix null and numbers neither.)

Storing the pointers is not great for memory management purposes when we port this. In general I try to avoid cross-linking between objects unless absolutely necessary since it breaks the simplicity of ownership. It also makes GC less predictable.

It also makes it harder to do tricks like moving segments around or reusing cached ones if the structure assumes some kind of graph.

@gnoff
Copy link
Collaborator Author

gnoff commented May 31, 2022

You can keep track of if the flushSegment just flushed a text edge in flushSubtree and if so insert a separator.

I couldn't figure out how to do this. The challenge I see is segment.chunks are opaque so I need to stash the emitted types at the edges (between children) on the segments that interleave them.

I was able to de-link the segment objects but I now am tracking both sides of a segments front and back edges so I can flush leading and/or trailing separators in the flushSegment.

I pushed a rough version but there are some things I am not loving, for instance setting postEdge on the most recent segment if no new chunks have been written. This feels over-complicated and you seem to have an idea of how it could be done simply but I'm just not seeing it

Copy link

This pull request has been automatically marked as stale. If this pull request is still relevant, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize reviewing it yet. Your contribution is very much appreciated.

@github-actions github-actions bot added the Resolution: Stale Automatically closed due to inactivity label Apr 10, 2024
Copy link

Closing this pull request after a prolonged period of inactivity. If this issue is still present in the latest release, please ask for this pull request to be reopened. Thank you!

@github-actions github-actions bot closed this Apr 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed Resolution: Stale Automatically closed due to inactivity
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants