Add new experimental ChatRoom-React workload - #583
Conversation
✅ Deploy Preview for webkit-speedometer-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
b37263a to
de21677
Compare
Exercises chat-room switching in a two-pane chat UI, so timeline teardown/rebuild cost can be profiled. Original React + Vite code with deterministic, server-free fixtures and no third-party assets. Experimental, so it is off by default and does not affect the official score. SwitchRooms uses flushSync so each programmatic click commits one room switch.
flushSync stopped React 18 batching a burst of programmatic clicks into one deferred render, but it is not what a real app does and it bypasses the concurrent scheduler this workload exists to exercise. The switch is a plain state update now, with the step yielding a task between clicks, which makes the suite async. The yield is a MessageChannel round-trip: an async step is awaited inside the measured window, so setTimeout(0)'s clamp would be reported as workload time, and a microtask would keep all 30 switches in one task. Driving 30 switches: microtask 329ms, flushSync 348ms, MessageChannel 397ms, setTimeout(0) 427ms.
Chat clients open a room at its most recent message; the timeline mounted at offset 0. A layout effect now assigns scrollTop = scrollHeight after commit, so SwitchRooms measures mount plus the scroll to bottom. Not flex-direction: column-reverse, which inverts scrollTop semantics in ways browsers have disagreed on, and later steps drive scrollTop explicitly.
…ctive Bodies were a single plain sentence, so rows were near-uniform in height and a windowing timeline built on that would inherit an unrealistically easy height model. They are now structured blocks -- paragraphs, code, blockquotes, lists, with inline code, links and @mention/#room pills -- giving 48 distinct row heights over 150 rows. Generation moves to a Math.imul integer hash so fields derive independently from the seed. Reaction pills, room pills and reply quotes become real controls, with state in components so the fixtures stay immutable across iterations. The reply jump assigns scrollTop from measured rects rather than scrollIntoView, keeping smooth scrolling out of a timed step. Also fixes senders never repeating, so grouping never triggered, and room pills slugifying 20 names against 40 rooms. SwitchRooms grows from ~245ms to ~450ms, the cost of mounting 150 rich rows.
The app was a read-only viewer. There is now a text input and a Send button below the timeline; sending appends to the end and scrolls to bottom. Sent messages live in Timeline state rather than the fixtures, so the generated data stays immutable and rooms do not grow across iterations. Timestamps continue from the room's last message rather than reading the clock. Enter is an explicit onKeyDown, since a dispatched keydown triggers no default action. No new step: SwitchRooms is unchanged, so this is app surface only.
Message bodies gain an image attachment and an unfurl card block, covering about 11% of messages. Artwork is SVG data URIs built from a palette and shape variant by a new graphics module, so the workload still ships no image assets. The URIs are memoized on a reduced palette/variant key -- with the raw hashes every message got a distinct gradient id, so the browser decoded 652 images instead of 127. Images carry explicit width and height, or a row would resize once the image decoded and invalidate the height cache. Also fixes bullet lists repeating a line about 13% of the time. Row heights now span 24px to 278px with 58 distinct values, up from 229px and 48. SwitchRooms grows from ~367ms to ~442ms.
Speedometer has almost no scroll coverage: across the 32 default suites the only scrolling inside a timed step is a single scrollIntoView. A chat timeline is the natural vehicle for that, but only once it is windowed. Rooms grow from 150 to 1500 messages and the timeline mounts ~15 rows. The virtualizer is hand-rolled, since a generic one assumes a height it can know up front and a chat row's depends on how its text wraps. The new height model estimates, measures, caches and corrects, keeping prefix sums so an offset can be turned back into a row index without touching the DOM. Three constraints it has to respect -- the scroller's height lagging the model by a commit, the row estimate having to sit under the real mean, and overflow-anchor having to be off -- are documented at their sites. ScrollTimeline is the new step, driving explicit offsets back through history and then in long strides. SwitchRooms is unchanged but costs ~4.5x less. PageElement gains scrollTop/scrollHeight/clientHeight, since scrollIntoView cannot reach an unmounted row.
Preserving the scroll position while content is inserted above the viewport is the chat and feed pattern nothing else in Speedometer exercises, and the one real clients visibly get wrong. A room now opens on the newest 300 of its 1500 messages and fetches 120 more whenever a scroll lands within a viewport of the top. The height model grows at the front as well as the end: a row keeps its index when older history arrives, so measured heights stay valid and only the offsets shift, uniformly, which makes the correction exact. Anchored by hand rather than with the browser's scroll anchoring, which would apply a second correction for the same insertion. LoadOlderMessages is the new step; ScrollTimeline stops one stride short of the top so the two stay independent. Following a reply quote can now load history too.
A profile shows a ~14ms nursery collection landing inside a timed step in three of the thirty measured windows, and those three are the slowest iteration of their step. The fixture build is not itself measured, but it pushes ~117MB of permanently-live data through the nursery per iteration, and a minor GC costs what it has to promote. The fixture now shares what it can: bodies come from a pool of 2048 rather than one per message, reaction rows collapse to the nine distinct values they always had, and timestamps and initials stop being rebuilt 60,000 times. The pool is drawn from the same seed space, so the spread of row heights is unchanged. The per-message preview string is gone; nothing rendered it. Building the fixtures drops from 197ms to 31ms and retains 14MB instead of 117MB, taking the unmeasured prepare phase from 335ms to 48ms per iteration.
Turning a message id back into a row index is only needed when a reply quote is clicked, and neither a scroll nor a room switch clicks one. Building it during render rebuilt a map over the room's whole history on every switch, inside the timed step, where the profile puts it at 8.8% of SwitchRooms -- the largest single piece of the app's own JavaScript in the run. With the fixture sharing beneath it, this takes the content process from 1.04GB of net allocation to 459MB, minor collections from 77 to 16, and the ones inside a timed step from 14.1ms to 7.2ms on average. What is left is React's own reconciliation filling the nursery, which is the work the steps exist to measure.
Per-render chrome that chat clients carry and this workload had none of: a labelled rule where the conversation crosses a day, and a marker above the first message the reader has not seen. Neither is a row of its own -- a row here is one message, and the height cache, spacers and offset search are all indexed by message index -- so both are drawn inside the row they belong to, keyed off the message array rather than the mounted window. Day lengths now vary between 8 and 56 messages. At a fixed size the boundary always landed just above the newest page, so no room opened with a separator in view; twenty of the forty now do. Cost is noise: 100.65ms -> 105.00ms over ten Firefox iterations against a +/-20ms spread.
Output of `npm run build` in suites-experimental/chat-room. Build artifacts only, so it can be dropped and regenerated whenever the source commits below it change.
de21677 to
9581719
Compare
|
@camillobruni Hey, wanted to point out something and make sure we're on the same page, since I'm doing something a bit differently in this workload. I added a The other async workload "Responsive-Design" just calls It's a Let me know what you think! I also lowered some of the iteration counts in the steps, so they're a bit more on par with the other workloads. |
Hey everyone! This PR adds a new workload called
ChatRoom-React. It's similar to chat room applications like Slack/Element/Discord etc. The workload exercises room switching. It's built using React + Vite. It contains 40 rooms of 1500 deterministically generated messages each.There are 3 times steps:
Note that I captured some profiles of this workload vs Slack vs Element(Matrix) and it looks like they are similar in terms of the things that they spend time on.