Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +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.
19 changes: 19 additions & 0 deletions patch6.diff
Original file line number Diff line number Diff line change
@@ -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
5 changes: 1 addition & 4 deletions pkgs/id/src/blob_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Hash, BTreeSet<String>>) -> Self {
Self {
worlds,
references,
}
Self { worlds, references }
}
}

Expand Down
2 changes: 1 addition & 1 deletion pkgs/id/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
5 changes: 2 additions & 3 deletions pkgs/id/src/commands/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,13 @@ fn evaluate_group(group: Hash, members: usize) -> (usize, Option<String>) {
};
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())),
Expand Down
14 changes: 0 additions & 14 deletions pkgs/id/src/event_store/ingest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Ingest, StoreError> {
match self {
Expand Down
21 changes: 10 additions & 11 deletions pkgs/id/src/event_store/range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ impl Params {
/// # Errors
///
/// Refuses `k` below 2, and `t` below 1.
pub fn new(k: usize, t: usize) -> Result<Self, ParamsError> {
pub const fn new(k: usize, t: usize) -> Result<Self, ParamsError> {
if k < 2 {
return Err(ParamsError::FanOut(k));
}
Expand Down Expand Up @@ -133,7 +133,7 @@ impl Params {
/// # Errors
///
/// Refuses a budget below one entry.
pub fn with_session_budget(self, bytes: usize) -> Result<Self, ParamsError> {
pub const fn with_session_budget(self, bytes: usize) -> Result<Self, ParamsError> {
if bytes < MIN_FRAME_BUDGET {
return Err(ParamsError::SessionBudget(bytes));
}
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -388,13 +388,12 @@ impl Party {

/// Appends an entry, merging it into the previous one when both are skips.
fn push(out: &mut Vec<Entry>, upper: Option<Vec<u8>>, 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 });
}
Expand Down
6 changes: 3 additions & 3 deletions pkgs/id/src/event_store/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
///
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions pkgs/id/src/tag_claims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
23 changes: 8 additions & 15 deletions pkgs/id/web/src/cursors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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}`,
}),
);
}
Expand Down Expand Up @@ -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}`,
}),
);
});
Expand All @@ -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}`,
}),
);
}
Expand All @@ -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}`,
},
),
);
Expand Down
Loading