From 4e3fde5135caf87d5fc7fbfd4f09a39979219fca Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:36:34 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Use=20stable=20keys=20f?= =?UTF-8?q?or=20ProseMirror=20decorations=20to=20fix=20layout=20thrashing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the global `decorationGeneration` counter (which forced DOM node recreation on every editor state change) with stable keys based on actual cursor data (client ID, position, opacity, and connection state). This allows ProseMirror to recycle DOM elements effectively. Also wrapped `Decoration.widget` components in a factory function to conform with ProseMirror's recycling APIs. Co-authored-by: dezren39 <11225574+dezren39@users.noreply.github.com> --- .jules/bolt.md | 3 +++ pkgs/id/web/src/cursors.ts | 23 ++++++++--------------- 2 files changed, 11 insertions(+), 15 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 000000000..4e97847f4 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-18 - ProseMirror Decoration Layout Thrashing +**Learning:** In ProseMirror, deriving decoration keys from random variables (like `Math.random()`, or a `decorationGeneration` global counter incremented every update) causes ProseMirror to unmount and remount the DOM node on every state change, even when the underlying cursor data hasn't changed. This leads to layout thrashing and high CPU usage. +**Action:** When implementing ProseMirror decorations, always use stable keys derived from the underlying data properties (e.g. `clientID`, `position`, `head`, `anchor`). Never use random or incrementing generations for decoration widget keys. diff --git a/pkgs/id/web/src/cursors.ts b/pkgs/id/web/src/cursors.ts index b12729032..788c8d68d 100644 --- a/pkgs/id/web/src/cursors.ts +++ b/pkgs/id/web/src/cursors.ts @@ -40,9 +40,6 @@ const RECONNECT_CLEANUP_DELAY_MS = 3_000; // Wait 3s after Init before cleanup ( const HOVER_RESTORE_DELAY_MS = 1_000; // 1s delay before restoring after hover const PENDING_REMOVAL_MS = 5_000; // Show fading cursor for 5s after disconnect -// Global counter to force widget recreation on every decoration call -let decorationGeneration = 0; - export interface CursorInfo { clientID: string | number; head: number; @@ -457,10 +454,6 @@ function createCursorDecorations( const now = Date.now(); const localHead = state.selection.head; - // Increment generation to force widget recreation - decorationGeneration++; - const gen = decorationGeneration; - // Normalize myClientID to number for comparison (handles string/number mismatch) const myClientIDNum = myClientID !== null ? Number(myClientID) : null; @@ -653,9 +646,9 @@ function createCursorDecorations( ownCursorLine.setAttribute("data-client-id", String(ownCursor.clientID)); decorations.push( - Decoration.widget(ownCursor.head, ownCursorLine, { + Decoration.widget(ownCursor.head, () => ownCursorLine, { side: 1, - key: `cursor-own-${ownCursor.clientID}-g${gen}`, + key: `cursor-own-${ownCursor.clientID}-${ownCursor.head}-${ownCursor.opacity}-${connectionState}`, }), ); } @@ -715,11 +708,11 @@ function createCursorDecorations( }); } - // Use cluster-wide key + generation to force recreation on every update + // Use cluster-wide key based on position and cluster hash decorations.push( - Decoration.widget(group.position, cursorLine, { + Decoration.widget(group.position, () => cursorLine, { side: 1, - key: `cursor-cluster-${group.position}-${clusterKey}-g${gen}`, + key: `cursor-cluster-${group.position}-${clusterKey}-${group.mostRecentColor}-${connectionState}`, }), ); }); @@ -740,9 +733,9 @@ function createCursorDecorations( : createCursorLine(cursor, false, connectionState); decorations.push( - Decoration.widget(cursor.head, cursorWidget, { + Decoration.widget(cursor.head, () => cursorWidget, { side: 1, - key: `cursor-${cursor.clientID}-g${gen}`, + key: `cursor-${cursor.clientID}-${cursor.head}-${cursor.opacity}-${connectionState}`, }), ); } @@ -765,7 +758,7 @@ function createCursorDecorations( style: `background-color: ${cursor.color}${selectionOpacity};`, }, { - key: `selection-${cursor.clientID}-g${gen}`, + key: `selection-${cursor.clientID}-${from}-${to}-${cursor.opacity}`, }, ), ); From ee31f2cc13d8be3202d20780c089e2633be39a0f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:41:37 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Use=20stable=20keys=20f?= =?UTF-8?q?or=20ProseMirror=20decorations=20to=20fix=20layout=20thrashing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the global `decorationGeneration` counter (which forced DOM node recreation on every editor state change) with stable keys based on actual cursor data (client ID, position, opacity, and connection state). This allows ProseMirror to recycle DOM elements effectively. Also wrapped `Decoration.widget` components in a factory function to conform with ProseMirror's recycling APIs. Also fixes formatting in .jules/bolt.md using prettier. Co-authored-by: dezren39 <11225574+dezren39@users.noreply.github.com> --- .jules/bolt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.jules/bolt.md b/.jules/bolt.md index 4e97847f4..914c142f3 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,4 @@ ## 2024-05-18 - ProseMirror Decoration Layout Thrashing + **Learning:** In ProseMirror, deriving decoration keys from random variables (like `Math.random()`, or a `decorationGeneration` global counter incremented every update) causes ProseMirror to unmount and remount the DOM node on every state change, even when the underlying cursor data hasn't changed. This leads to layout thrashing and high CPU usage. **Action:** When implementing ProseMirror decorations, always use stable keys derived from the underlying data properties (e.g. `clientID`, `position`, `head`, `anchor`). Never use random or incrementing generations for decoration widget keys. From 6a75dabe7a2140a6e143f9c5d9ab6eac3292b482 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:56:40 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Use=20stable=20keys=20f?= =?UTF-8?q?or=20ProseMirror=20decorations=20to=20fix=20layout=20thrashing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the global `decorationGeneration` counter (which forced DOM node recreation on every editor state change) with stable keys based on actual cursor data (client ID, position, opacity, and connection state). This allows ProseMirror to recycle DOM elements effectively. Also wrapped `Decoration.widget` components in a factory function to conform with ProseMirror's recycling APIs. Also fixes formatting in .jules/bolt.md using prettier. Co-authored-by: dezren39 <11225574+dezren39@users.noreply.github.com> --- pkgs/id/src/blob_gate.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pkgs/id/src/blob_gate.rs b/pkgs/id/src/blob_gate.rs index d8d207fd6..e64b3bdd3 100644 --- a/pkgs/id/src/blob_gate.rs +++ b/pkgs/id/src/blob_gate.rs @@ -125,10 +125,7 @@ pub struct FoldedWorlds { impl FoldedWorlds { /// Pairs the folded worlds with a reference index. pub const fn new(worlds: Worlds, references: HashMap>) -> Self { - Self { - worlds, - references, - } + Self { worlds, references } } } From 1e7fbe8b7008c1e872e16055b1f76bc200af498b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:45:47 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Use=20stable=20keys=20f?= =?UTF-8?q?or=20ProseMirror=20decorations=20to=20fix=20layout=20thrashing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the global `decorationGeneration` counter (which forced DOM node recreation on every editor state change) with stable keys based on actual cursor data (client ID, position, opacity, and connection state). This allows ProseMirror to recycle DOM elements effectively. Also wrapped `Decoration.widget` components in a factory function to conform with ProseMirror's recycling APIs. Also fixes formatting in .jules/bolt.md using prettier and `cargo clippy` linting warnings in `pkgs/id`. Co-authored-by: dezren39 <11225574+dezren39@users.noreply.github.com> --- patch6.diff | 19 +++++++++++++++++++ pkgs/id/src/cli.rs | 2 +- pkgs/id/src/commands/group.rs | 5 ++--- pkgs/id/src/event_store/ingest.rs | 14 -------------- pkgs/id/src/event_store/range.rs | 21 ++++++++++----------- pkgs/id/src/event_store/sync.rs | 6 +++--- pkgs/id/src/tag_claims.rs | 1 + 7 files changed, 36 insertions(+), 32 deletions(-) create mode 100644 patch6.diff diff --git a/patch6.diff b/patch6.diff new file mode 100644 index 000000000..5351fec42 --- /dev/null +++ b/patch6.diff @@ -0,0 +1,19 @@ +<<<<<<< SEARCH + /// Whether the event is kept: stored, or held. A refused event and a fork are not. + const fn keeps(&self) -> bool { + matches!( + self, + Self::Stored { .. } + | Self::Ready( + Ingest::Accepted { .. } + | Ingest::Voided { .. } + | Ingest::Duplicate { .. } + | Ingest::Waiting { .. }, + ) + ) + } + + /// The outcome of the event. +======= + /// The outcome of the event. +>>>>>>> REPLACE diff --git a/pkgs/id/src/cli.rs b/pkgs/id/src/cli.rs index 4cdc5a822..2e2ce9ec5 100644 --- a/pkgs/id/src/cli.rs +++ b/pkgs/id/src/cli.rs @@ -318,7 +318,7 @@ pub enum Command { no_mdns: bool, /// Enable mainline DHT address lookup and publishing. /// - /// By default, the server does not use the mainline BitTorrent DHT + /// By default, the server does not use the mainline `BitTorrent` DHT /// for address discovery. This flag enables DHT lookup and publishing /// for the server's endpoint. #[arg(long)] diff --git a/pkgs/id/src/commands/group.rs b/pkgs/id/src/commands/group.rs index 39092c9ba..89bb04905 100644 --- a/pkgs/id/src/commands/group.rs +++ b/pkgs/id/src/commands/group.rs @@ -173,14 +173,13 @@ fn evaluate_group(group: Hash, members: usize) -> (usize, Option) { }; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); + .map_or(0, |d| d.as_secs()); let mut evaluator = Evaluator::default(); let warnings = evaluator.observe(&observation, now); let size_warning = warnings .iter() .filter(|w| matches!(w, Warning::Size { .. })) - .last(); + .next_back(); let churn_warning = warnings.iter().find(|w| matches!(w, Warning::Churn { .. })); let warning = match (size_warning, churn_warning) { (Some(size), Some(churn)) => Some(format!("{}; {}", size.message(), churn.message())), diff --git a/pkgs/id/src/event_store/ingest.rs b/pkgs/id/src/event_store/ingest.rs index 984e2bdbf..9544be0fd 100644 --- a/pkgs/id/src/event_store/ingest.rs +++ b/pkgs/id/src/event_store/ingest.rs @@ -464,20 +464,6 @@ enum Staged { } impl Staged { - /// Whether the event is kept: stored, or held. A refused event and a fork are not. - fn keeps(&self) -> bool { - matches!( - self, - Self::Stored { .. } - | Self::Ready( - Ingest::Accepted { .. } - | Ingest::Voided { .. } - | Ingest::Duplicate { .. } - | Ingest::Waiting { .. }, - ) - ) - } - /// The outcome, with its labels read from the store as it is now. fn finish(self, txn: &WriteTransaction) -> Result { match self { diff --git a/pkgs/id/src/event_store/range.rs b/pkgs/id/src/event_store/range.rs index fd92f4fb8..2e8de7049 100644 --- a/pkgs/id/src/event_store/range.rs +++ b/pkgs/id/src/event_store/range.rs @@ -97,7 +97,7 @@ impl Params { /// # Errors /// /// Refuses `k` below 2, and `t` below 1. - pub fn new(k: usize, t: usize) -> Result { + pub const fn new(k: usize, t: usize) -> Result { if k < 2 { return Err(ParamsError::FanOut(k)); } @@ -133,7 +133,7 @@ impl Params { /// # Errors /// /// Refuses a budget below one entry. - pub fn with_session_budget(self, bytes: usize) -> Result { + pub const fn with_session_budget(self, bytes: usize) -> Result { if bytes < MIN_FRAME_BUDGET { return Err(ParamsError::SessionBudget(bytes)); } @@ -250,12 +250,12 @@ impl Party { } /// The number of keys. - pub fn len(&self) -> usize { + pub const fn len(&self) -> usize { self.keys.len() } /// Whether the set is empty. - pub fn is_empty(&self) -> bool { + pub const fn is_empty(&self) -> bool { self.keys.is_empty() } @@ -388,13 +388,12 @@ impl Party { /// Appends an entry, merging it into the previous one when both are skips. fn push(out: &mut Vec, upper: Option>, mode: Mode) { - if mode == Mode::Skip { - if let Some(last) = out.last_mut() { - if last.mode == Mode::Skip { - last.upper = upper; - return; - } - } + if mode == Mode::Skip + && let Some(last) = out.last_mut() + && last.mode == Mode::Skip + { + last.upper = upper; + return; } out.push(Entry { upper, mode }); } diff --git a/pkgs/id/src/event_store/sync.rs b/pkgs/id/src/event_store/sync.rs index c79383eb3..73d8599cd 100644 --- a/pkgs/id/src/event_store/sync.rs +++ b/pkgs/id/src/event_store/sync.rs @@ -103,7 +103,7 @@ struct Tally { } impl Tally { - fn add(&mut self, outcome: &Ingest) { + const fn add(&mut self, outcome: &Ingest) { match outcome { Ingest::Accepted { .. } => self.accepted += 1, Ingest::Duplicate { .. } => self.duplicates += 1, @@ -128,7 +128,7 @@ pub(crate) enum Cut { /// Runs one range session over an open connection. The dialer opens the stream and passes /// `initiator = true`. `now_ms` is the clock the store's waiting rules use. `deadline` bounds -/// the whole session, from opening the stream to the last event, as LogSync's does. +/// the whole session, from opening the stream to the last event, as `LogSync`'s does. /// /// # Errors /// @@ -273,7 +273,7 @@ struct ReadBudget { impl ReadBudget { /// Charges `cost` bytes. A charge that would pass the budget is refused, and nothing is /// charged. - fn charge(&mut self, cost: usize) -> Result<(), BudgetExceeded> { + const fn charge(&mut self, cost: usize) -> Result<(), BudgetExceeded> { let total = self.read.saturating_add(cost); if total > self.budget { return Err(BudgetExceeded { diff --git a/pkgs/id/src/tag_claims.rs b/pkgs/id/src/tag_claims.rs index 6b8126310..509c5d597 100644 --- a/pkgs/id/src/tag_claims.rs +++ b/pkgs/id/src/tag_claims.rs @@ -156,6 +156,7 @@ fn check_text(field: &str, text: &str, min: usize, max: usize) -> Result<(), Tag } /// The events that `start` reaches by following parent links: its own causal past, transitively. +/// /// `parents` maps each event to the events it names, its `after` hashes and its backlink. The /// starting events are included. #[must_use]