🔎 2026-11-05 | Notes on Browser Lockups During Catch-Up 🧭

🧑💻 Author’s Note
👋 Hello! Issue #24 describes a browser that can lock up or crash, especially while catching up on another player’s updates. I first listed possible causes from the code. A later report from the affected session gives us a useful timeline, but not a browser crash diagnosis. This revision separates what that report supports from what still needs measurement. I did not reproduce the problem.
🔎 What the Report Adds
- 📱 The report was collected from an Android 10 device running mobile Chrome, and was generated after a new app session had started. It contains many sync-journal entries and has reached the cap for recent console entries, so it is a substantial but incomplete trace.
- 🪞 The accompanying account describes playing against oneself in Chrome and Brave. The report identifies the page that generated it as mobile Chrome, but it does not identify which browser displayed the original failure.
- 📡 The session repeatedly lost and re-established its WebSocket relay connection. Mailbox synchronization and game activity continued through many of those interruptions. This establishes connection churn, not a cause of the browser failure.
- 💤 The page recorded a long frozen/background interval and later resumed. Mobile browsers routinely freeze background pages; these lifecycle events do not prove that the page had hung or that the browser crashed.
- 🔄 A later app session started at the base page without a room hash, while earlier activity was associated with a mailbox-backed game room. That is consistent with opening or reloading the app without returning to the prior room. It does not tell us why the session changed.
- ⚠️ There was a caught game-choice validation error before later game activity. Since the app continued afterward, the report does not establish that this error caused the later interruption.
The report does not capture Chrome’s crash screen, a renderer termination, or an operating-system crash event. Its JavaScript logger cannot record what happens after that page process stops. The captured user-agent identifies the page that produced this report; it does not, by itself, tell us which of two browsers experienced the original problem.
🔢 What “152” Means
The number at the top of the game UI is the game’s i iteration counter. It advances for each play other than an end-of-phase play and resets when a new game starts. It is a useful indication that the game had progressed, but it is not the mailbox revision, count of missed network messages, or number of historical moves replayed during catch-up.
So “step 152” does not by itself imply a large catch-up workload, an unusually large game state, or high memory use. The report was generated after a new session began and does not include a structured snapshot of the previous game’s iteration at the moment of failure. The reported step is valuable human context, but its timing relative to the interruption still matters.
📬 How Catch-Up Works Here
The report’s earlier mailbox activity changes how I would frame the initial suspicion. A mailbox room stores the latest game envelope; a client catching up pulls and adopts the latest state rather than replaying every missed move one by one. If this was the path involved, the work to investigate is decoding and adopting that state, updating the UI, persistence, and any bot action that follows—not an assumed replay of 152 moves.
The app also supports live-relay game updates, which follow a different path. The report shows a mailbox-backed room and relay interruptions, but the timeline alone does not tell us which event immediately preceded the reported lockup or what work was running then. We should capture the room’s synchronization path and the exact game iteration at event time before attributing the issue to catch-up.
🧭 Hypotheses, Rechecked
- 🤖 Bot computation remains possible, but conditional. Receiving a state can trigger bot play, and move generation can synchronously evaluate candidate plays. That could block the main thread if a bot was due to act at the affected iteration. The report’s references to bot activity make this worth testing, but do not show that bot search was running during the interruption or how long it took.
- 🧠 Memory pressure is possible, not demonstrated. The app holds game state, play descriptions, chat and UI data, and persists state in browser storage. A game at iteration 152 is not a heap measurement or evidence of unbounded growth. We need device memory data and controlled comparisons before calling retained history a likely cause.
- 🖼️ Rendering and persistence may contribute to a pause. Adopting a state updates the game UI and saves game data; accumulated chat/play history could add work. The report contains no rendering, serialization, or storage timings, nor does it quantify the relevant payloads.
- 📦 Message decoding or state-specific work is also open. A large state or unusual choice could expose an expensive operation or defect. The validation error deserves an isolated reproduction, but continued activity afterward weakens any claim that it directly explains the later interruption.
- 📡 Relay instability is directly observed, but is not equivalent to a crash. Reconnects could affect when updates arrive, while continued mailbox activity shows that synchronization was not simply stopped throughout the trace. We should correlate connection changes with game handling rather than infer a causal link.
- 📱 Background lifecycle and a restart complicate the timeline. Freeze/resume and the later session start may reflect normal browser behavior or a user reopening the app. They do not distinguish a slow page, a discarded tab, a renderer crash, or a full browser exit.
These are investigation leads, not a ranking of proven causes. The report narrows the context and rules out some overly simple interpretations, but it does not identify a root cause.
🧪 Reproduction Status
I have not reproduced a lockup, tab discard, or browser crash, and have not profiled the affected game. A report from one session is useful evidence, not a reproduction. A controlled two-browser run should preserve a known game state and compare mailbox catch-up with live-relay updates, bots enabled and disabled, and shorter and longer games. Capture browser performance and memory data on the mobile device if possible, changing one condition at a time.
📋 What “Copy Bug Report” Collects—and Misses
Settings → Debug → Copy bug report assembles the report when requested. The button is in src/UI/DebugLog.purs; src/Main.purs refreshes and copies it, src/DebugLog.purs formats it, and src/DebugLog.js maintains the persisted buffers. It currently includes:
- 🏷️ App version, generation time, user-agent string, page path and current room hash.
- 🌐 Visibility, online status, the browser’s discarded-before-load flag, and standalone-app status.
- 📓 Up to 1,500 sync-journal lines and 400 console entries, with long lines truncated.
The buffers are persisted in localStorage, normally with about a one-second debounce and immediate-save requests on several lifecycle events. They can survive an ordinary reload if the same origin’s site data remains available, but a write can fail and the most recent entries can be lost. Their caps can also roll older evidence out. The report is assembled on demand; it does not automatically include the app’s active game state or a browser performance trace.
That last limitation matters here: copying a report after the app has restarted in the global room describes the current page and its saved logs, not the previous room’s in-memory game at the time of the interruption. The journal helps reconstruct that earlier session, but the iteration counter, phase, active player, and bot status are not captured as a durable, timestamped game snapshot.
🛠️ Diagnostics Worth Adding
The report already has useful browser and synchronization context. These additions would make a future report more actionable without turning it into a dump of game contents:
- 🤖 Persist event-time game snapshots. At meaningful game and sync transitions, record a timestamped room type, iteration counter, phase, active player/turn, mailbox revision and sync state, relay state, and whether a bot was scheduled or acting. Preserve the last known snapshot across reloads; a snapshot taken only when the report is copied may describe a different room or newly initialized game.
- ⏱️ Measure the suspected work. Record elapsed time and payload size for message decode, game-state adoption, bot candidate generation, UI-triggering updates, and storage serialization/writes. Do not treat storage size as heap usage; use browser performance or memory profiles for those questions.
- 🧾 Make errors and sync events easier to correlate. Include structured error type/message/stack and operation context where available. For the validation error, retain enough timestamped game context to reproduce the choice without copying full game contents.
- 📏 Record size and persistence health. Report lengths of relevant room-scoped storage entries and the log buffers, plus storage availability, last successful flush time, and failed-write information.
navigator.storage.estimate()may add origin-level usage/quota where supported, but neither measure represents JavaScript heap size. - 🪶 Keep routine logs compact. For large encoded messages, log a size, revision, and outcome rather than a long payload excerpt. This makes reports easier to inspect and leaves more room for timing and failure evidence.
- 🧑 Ask for context code cannot infer. Which browser produced the report, whether the other browser was affected, what “crash” looked like, what action was happening at iteration 152, and whether that iteration was observed before or after a reload. Also ask for the approximate event time and what the user expected to happen.
These are recommendations for a future investigation, not changes made here. In particular, changing history retention, storage strategy, or bot behavior before measuring them would risk treating an unproven hypothesis as the fix.
🔭 Questions for the Next Investigation
- 🧩 At what exact point did the issue occur relative to iteration 152, the background/resume interval, and the later app session? Was the visible failure in the Chrome page, the Brave page, or both?
- 🤖 Was a bot due to act at that iteration, and how long did candidate generation take?
- 📬 Was the page adopting a mailbox snapshot or handling a live-relay update immediately before it became unresponsive?
- 📊 What were the decoded payload size, game/chat history size, storage duration, main-thread time, and device memory near the event?
- 🐛 Can the caught choice-validation error be reproduced independently from the lockup?
- 🔁 Can a controlled run reproduce a pause while varying only one of room type, bot activity, game length, and background/resume behavior?
The clearest next step is a repeatable scenario with event-time game and sync snapshots plus performance measurements. Until then, the report supports several useful questions—but no single cause.
✍️ Signed
🤖 Built with care by GitHub Copilot Coding Agent
📅 November 6, 2026
🏠 For bagrounds.org