From 5b84196f30adcd9f1b02aedec37dbda2d27fcaa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4ng=20Tr=E1=BA=A7n?= Date: Thu, 1 Oct 2026 20:53:06 +0700 Subject: [PATCH] perf(text): change a text view's state only when a parse changes it A TextViewState parses a small text on the UI thread. Two parts of that path wrote the state without changing it, and under GPUI Fast each write counts as a change for every view that read the state: - The first parse ran in the constructor and notified. TextView makes its state as element state, inside the render of the view that holds it, so the notify stamped the state as changed while that view was drawn. - The synchronous parse is still sent to the background parser, so later appends extend its document. The task receiving the parser's acknowledgement updated the state just to find nothing to commit. It did the same for a result whose revision is stale. A text view built for the first time therefore changed twice, once while it was drawn and once a frame later. A list whose rows build text views, like a transcript scrolled to rows it had not shown, repainted its scroll layer for every new row until the layer was demoted. A state being made now parses without announcing it: there is no selection to reset and no reader to tell. The receiving task reads the state first and updates it only to commit a current result. --- crates/base/src/text/state.rs | 55 ++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/crates/base/src/text/state.rs b/crates/base/src/text/state.rs index c360f1eb30..cb1570d07d 100644 --- a/crates/base/src/text/state.rs +++ b/crates/base/src/text/state.rs @@ -183,18 +183,26 @@ impl TextViewState { let _receive_task = cx.spawn({ async move |weak_self, cx| { while let Ok(parsed_update) = rx_result.recv().await { + // A stale result or the acknowledgement of a synchronous + // parse changes nothing, so it is checked by a read: an + // update counts as a change of the state for whatever + // read it, and every small text would otherwise change + // once, a frame after it was first drawn. + if parsed_update.baseline_ack { + continue; + } + let current = weak_self + .read_with(cx, |state, _| parsed_update.revision == state.revision); + if !matches!(current, Ok(true)) { + continue; + } _ = weak_self.update(cx, |state, cx| { - if parsed_update.revision != state.revision { - return; - } - if parsed_update.baseline_ack { - return; - } state.commit_parse( parsed_update.result, parsed_update.revision, !parsed_update.full_parse, parsed_update.selection_compatible, + true, cx, ); }); @@ -255,7 +263,11 @@ impl TextViewState { _parse_task, _receive_task, }; - this.increment_update(text, Change::Replace, cx); + // Nothing has read a state that is being made, so its first parse + // announces nothing: a notify here, inside the render that makes it + // (`TextView` keeps it as element state), counts as a change of what + // that render read. + this.increment_update(text, Change::Replace, false, cx); this } @@ -349,7 +361,7 @@ impl TextViewState { self.text.push_str(text); self.may_hold_definitions = may_hold_definition(text.as_bytes()); self.parsed_error = None; - self.increment_update(text, Change::Replace, cx); + self.increment_update(text, Change::Replace, true, cx); } /// [`Self::set_text`] for the text a `TextView` element hands over every @@ -391,9 +403,9 @@ impl TextViewState { self.may_hold_definitions |= may_hold_definition(&self.text.as_bytes()[unscanned..]); if self.appends_parse_whole_text() { let text = self.text.clone(); - self.increment_update(&text, Change::Extend, cx); + self.increment_update(&text, Change::Extend, true, cx); } else { - self.increment_update(new_text, Change::Append, cx); + self.increment_update(new_text, Change::Append, true, cx); } } @@ -433,7 +445,7 @@ impl TextViewState { self.markdown_extensions = markdown_extensions; if parser_configuration_changed && self.format == TextViewFormat::Markdown { let text = self.text.clone(); - self.increment_update(&text, Change::Replace, cx); + self.increment_update(&text, Change::Replace, true, cx); } } @@ -542,7 +554,16 @@ impl TextViewState { cx.notify(); } - fn increment_update(&mut self, text: &str, change: Change, cx: &mut Context) { + /// Take `text` as `change` says. With `announce`, a parse that lands now + /// resets the selection it breaks and notifies; without, as for a state + /// being made, it lands quietly. + fn increment_update( + &mut self, + text: &str, + change: Change, + announce: bool, + cx: &mut Context, + ) { let append = change == Change::Append; let parse_len = if append { self.appended_parse_len(text.len()) @@ -581,7 +602,8 @@ impl TextViewState { ParsedContent::default() }; let result = parse_content(self.format, base, &update_options); - self.commit_parse(result, self.revision, append, change != Change::Replace, cx); + let compatible = change != Change::Replace; + self.commit_parse(result, self.revision, append, compatible, announce, cx); } // A synchronous update keeps the background parser's accumulated // document in sync, so a later append extends this baseline instead of @@ -609,13 +631,15 @@ impl TextViewState { /// /// `append` is whether it parsed only appended text onto the document /// before it, and `compatible` whether the text before it is still there, - /// so a selection in it holds. + /// so a selection in it holds. Without `announce` it lands quietly: it neither + /// resets the selection nor notifies. fn commit_parse( &mut self, result: Result, revision: usize, append: bool, compatible: bool, + announce: bool, cx: &mut Context, ) { match result { @@ -641,6 +665,9 @@ impl TextViewState { self.parsed_error = Some(err); } } + if !announce { + return; + } // Don't interrupt an active drag-selection; the stored positions // remain valid for append-only updates and will self-correct on the // next mouse-move event.