chore(dev): add burst simulator Chrome extension#1730
Merged
Conversation
Manifest V3 popup-only extension that injects a synthetic event generator into the page's main world via chrome.scripting and dispatches message.new + reaction.new bursts at window.streamChannel for stress/perf testing the chat UI. Dev-only; not part of the npm package.
Stringify each generated event, JSON.parse it locally (mirroring connection.onmessage's local parse), then hand the string to client.handleEvent (which JSON.parses it a second time before dispatching). Production does both parses per WS frame — the simulator now does too, so the per-event CPU cost reflects what the chat UI actually pays under load. README updated to describe the ingestion path and to point at channel.getClient().handleEvent as the simulator's entry point.
Detect window.streamChannel on popup open via main-world executeScript and gate the Run button on success. The Run button is disabled while detecting and stays disabled until a Channel-shaped object (with getClient + handleEvent) is found on the active tab. Reasons for failure are surfaced in the status line: - undefined: window.streamChannel is not set on this page - not-a-channel: set, but doesn't quack like Channel - no-handle-event: getClient().handleEvent missing (version skew) - getclient-threw: getClient() raised A ↻ re-check button appears in the status block whenever a status is settled (idle/ok/error), so users can retry without closing the popup — useful when the channel mounts after popup open or after a page reload while the popup is held open.
…iber walk
Replace the manual prerequisite with automatic detection: on popup
open, walk the React fiber tree on the active tab and find a
Channel-shaped object via duck-typing (getClient + cid + id + type).
Both prop-passed (<Channel channel={ch}>) and context-passed
(<Provider value={{ channel, ... }}>) channels are detected.
When multiple Channels exist (e.g. ChannelList previews + active
<Channel>), tiebreak by largest descendant subtree under the
owning fiber — the active channel wraps the message list while
previews are near-leaves.
A pre-existing window.streamChannel is honored as-is, so manual
overrides still work. Failure modes report distinct reasons:
no-react-root, no-channel-in-tree, preset-not-a-channel, etc.
Two reasons reactions weren't visible after a burst:
1. The reaction `type` used legacy short names ('love', 'haha', ...).
Stream Chat reactions are keyed as `emoji-<unicode codepoint>`
(e.g. 'emoji-1f4af' for 💯). Switched the EMOJIS table to
{ char, code } pairs and emit reaction.type = `emoji-${code}`.
2. channel_state.addReaction (src/channel_state.ts) replaces the
in-state message with a formatMessage copy of event.message —
so denormalized reaction state on event.message is what the UI
ends up rendering. Generated messages had latest_reactions /
reaction_counts / reaction_scores empty and no reaction_groups,
so reactions were applied to the in-memory channel state but
never reflected in the rendered message.
Now: every generated message carries reaction_groups: {}, and each
reaction.new event pre-applies the new reaction onto event.message
(latest_reactions, reaction_counts, reaction_scores,
reaction_groups) before dispatch — mirroring the real backend.
Mutating the message reference also means subsequent reactions on
the same target accumulate correctly across the burst.
Also adds top-level message_id to reaction.new events to match the
real event shape.
Reactions are now visible end-to-end in the UI; biasing the default mix toward more reactions (50/50 vs the previous 30/70) makes the common stress-test case more representative of a busy live chat. Updates the popup form default, the simulator fallback, and the README — all aligned.
The realistic stress-test target is closer to 1000 events than 200; make that the popup default. Aligns popup form, simulator fallback, and README.
- reactionRatio: 0.5 → 0.25 (1-in-4 reactions reads as a busy-but- message-dominated channel, closer to real traffic). - ratePerSec: 50 → 75 (faster pacing keeps wall-clock duration closer to the prior count=200, rate=50 baseline now that count defaults to 1000 — ~13s instead of 20s). Aligns popup form, simulator fallback, and README.
The "window" / reactToLastN field controlled an advanced detail (clustering reactions on the most recent messages) that wasn't worth a dedicated input. Removing it makes the popup simpler and gives the more representative default — reactions are now picked at random from any of the burst-generated messages, exercising the full rendered tree rather than just its tail. Drops the form field, popup.js config entry, simulator config + sliding-window pop, and README bullet. Renames recentMessages → messagesPool inside the simulator since "recent" no longer applies.
Surfaces the local JSON.parse "shadow" parse — currently always-on to mirror today's prod path — as a popup toggle so it can be A/B'd against an in-flight fix that removes the duplicate. When on (default): JSON.parse(jsonString) runs locally and client.handleEvent then parses again — both parses per dispatch, matching production today. When off: only client.handleEvent's parse runs — matches the proposed single-parse pipeline. The setting is also returned in the result object so successive runs can be compared without ambiguity.
Visual rewrite to match the design-system foundations: - Geist + Geist Mono replace Major Mono Display + JetBrains Mono - Light theme on white with slate neutrals; drop the dark instrumentation aesthetic (hot coral accent, dotted texture, radial glows, animated indicator, accent strip) - Blue-600 primary for the Run button and focus rings - Status block reads idle (slate) / running (blue, pulsing) / ok (green) / error (red), each with soft tinted fills - Type tokens map to the foundations table: heading-md (18/500) for the title, p-sm-medium (12/500) for field labels, p-sm-regular-uppercase for unit hints Form refinements: - Rename labels to be more descriptive: count -> total events, rate -> rate per second, reactions -> reactions ratio, users -> user pool size - Remove the double parse toggle. The simulator still mirrors prod's two-per-frame parse cost — just no longer exposed as a popup knob.
…pup close Two features in one cohesive change: 1. Live run readout + Stop button. While a paced run is in flight, the popup polls the simulator's in-page state every 300 ms and shows progress in the status line: "Running · 230 / 1000 · 76 eps · 3s". EPS is a 4-sample rolling average so it doesn't whiplash on the first tick. The Run button morphs into a red Stop (■); clicking it flips simulator.abort, the simulator breaks out at the next tick and returns a partial result with aborted: true. Burst-all mode skips polling and Stop since the entire batch lives in one rAF. 2. State preserved across popup close. The simulator stops nulling `state` at end-of-run — it now keeps the snapshot with phase='done' and stashes the full result on `simulateBurst.lastResult`. After channel detection the popup inspects page status; if a run is still in flight it re-attaches as a watcher (button -> Stop, polling resumes), and if the most recent run finished while the popup was closed it surfaces the summary instead of leaving the channel-detection text up. An isOwnRun flag distinguishes runs the current popup session started (its form-submit await handles the summary) from resumed/observed runs (the polling tick handles the done-transition). A localTimer === pollTimer guard prevents an in-flight poll from clobbering the final summary if polling stops mid-await.
Contributor
|
Size Change: 0 B Total Size: 380 kB ℹ️ View Unchanged
|
Apps that already expose the channel as `window.channel` no longer need extra plumbing — the popup and simulator both look for it as a fallback when `window.streamChannel` isn't set. Precedence: window.streamChannel (canonical) > window.channel (alias) > React fiber walk. The popup's success status surfaces which preset name was used: "Ready · cid · bound to window.channel". The fiber walk has always picked up React props named `channel` via duck-typing; just clarifying that in the README and updating popup hint + recheck tooltip + preset error text accordingly. Drops the "result is also logged to the page console" hint since the popup now persists the summary across open/close.
Tighter wording — "StreamChannel is auto-detected from the React component tree, or taken from window.channel or window.streamChannel."
MartinCupela
approved these changes
May 4, 2026
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.
Summary
Adds a Manifest V3 Chrome extension under
dev/burst-simulator-extension/for stress / perf testing the chat UI by dispatching syntheticmessage.newandreaction.newevents at a Stream ChatChannelinstance. Dev-only — not part of the npm package, no build step, load-unpacked.getClient+cid+id+type— including the conventionalchannelprop on<Channel channel={ch}>). When multiple exist (e.g.ChannelListpreviews + active<Channel>), picks the one whose owning fiber wraps the largest subtree. Apps that already expose the channel aswindow.streamChannel(canonical) orwindow.channel(alias) get honored as-is, skipping the walk.live_stream_chatevents (fullmember,restricted_visibility,reaction_groups, etc.); reactions use theemoji-<unicode codepoint>keying the modern UI expects (e.g.emoji-1f4af) and pre-applylatest_reactions/reaction_counts/reaction_scores/reaction_groupstoevent.messagesochannel_state.addReactionlands them correctly.JSON.parse'd locally (mirroringconnection.onmessage's health-check shortcut), then handed toclient.handleEventwhich parses again before dispatching. Note: this matches the pre-perf: parse WebSocket events once at the transport boundary #1729 prod path; with that PR landed on master, prod now does a single parse — flipping the simulator to single-parse is an obvious follow-up.Running · 230 / 1000 · 76 eps · 3s. The Run button morphs into a red Stop (■); clicking it flips an in-page abort flag, the simulator resolves on its next tick withaborted: true, and the popup reportsStopped at N in Tms.state(withphase: 'done') andlastResultafter the run ends. Re-opening the popup mid-run re-attaches as a watcher; re-opening after a run finished surfaces the summary instead of going back to the channel-detection text.Test plan
chrome://extensions→ Developer mode on → Load unpacked → selectdev/burst-simulator-extension/; pin the extensionChannelis rendered; click the extension; status readsReady · <cid> · auto-bound · context 'value.channel' on <…>(or similar)window.channel = window.streamChannel; delete window.streamChannel;then re-open the popup → status readsReady · <cid> · bound to window.channelcount: 1000, rate per second: 75, reactions ratio: 0.25, user pool size: 10) and confirm:Running · X / 1000 · ~75 eps · Tsduring the runStopped at N in Tmscount: 1000, rate per second: 0while recording in DevTools Performance — confirm one long-task flame, channel state contains 1000 messages, no thrown errorsNo React root found on this page; click ↻ to re-detect