From d9ece42cf5543c6b3272ff4a18657dae027e6d30 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 20 Feb 2026 11:17:22 +0100 Subject: [PATCH] text: Inline `text::Anchor`'s timestamp field, shrinking its size (#49703) This shrinks the size of `text::Anchor` from 32 bytes to 24 and `multi_buffer::Anchor` from 72 bytes to 56 Release Notes: - Improved the memory usage of Zed a bit --- .../assistant_text_thread/src/text_thread.rs | 9 +-- crates/language/src/proto.rs | 29 +++++----- crates/project/src/lsp_store.rs | 2 +- crates/text/src/anchor.rs | 55 ++++++++++++++---- crates/text/src/text.rs | 56 ++++++++++--------- 5 files changed, 95 insertions(+), 56 deletions(-) diff --git a/crates/assistant_text_thread/src/text_thread.rs b/crates/assistant_text_thread/src/text_thread.rs index 684ef57602651aa2b05defe3e3ec2c2682684013..18b37808b936e354614f6681bbcb263b184f832c 100644 --- a/crates/assistant_text_thread/src/text_thread.rs +++ b/crates/assistant_text_thread/src/text_thread.rs @@ -1094,7 +1094,7 @@ impl TextThread { .buffer .read(cx) .version - .observed(anchor.start.timestamp), + .observed(anchor.start.timestamp()), TextThreadOperation::UpdateMessage { message_id, .. } => { self.messages_metadata.contains_key(message_id) } @@ -1121,10 +1121,11 @@ impl TextThread { cx: &App, ) -> bool { let version = &self.buffer.read(cx).version; - let observed_start = - range.start.is_min() || range.start.is_max() || version.observed(range.start.timestamp); + let observed_start = range.start.is_min() + || range.start.is_max() + || version.observed(range.start.timestamp()); let observed_end = - range.end.is_min() || range.end.is_max() || version.observed(range.end.timestamp); + range.end.is_min() || range.end.is_max() || version.observed(range.end.timestamp()); observed_start && observed_end } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 242cce1c64d1d45b71d615e444409298ec2205db..132f971675ede12bb8ef5f941b57415f22d7ba88 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -251,9 +251,10 @@ pub fn serialize_diagnostics<'a>( /// Serializes an [`Anchor`] to be sent over RPC. pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor { + let timestamp = anchor.timestamp(); proto::Anchor { - replica_id: anchor.timestamp.replica_id.as_u16() as u32, - timestamp: anchor.timestamp.value, + replica_id: timestamp.replica_id.as_u16() as u32, + timestamp: timestamp.value, offset: anchor.offset as u64, bias: match anchor.bias { Bias::Left => proto::Bias::Left as i32, @@ -485,18 +486,20 @@ pub fn deserialize_anchor(anchor: proto::Anchor) -> Option { } else { None }; - Some(Anchor { - timestamp: clock::Lamport { - replica_id: ReplicaId::new(anchor.replica_id as u16), - value: anchor.timestamp, - }, - offset: anchor.offset as usize, - bias: match proto::Bias::from_i32(anchor.bias)? { - proto::Bias::Left => Bias::Left, - proto::Bias::Right => Bias::Right, - }, + let timestamp = clock::Lamport { + replica_id: ReplicaId::new(anchor.replica_id as u16), + value: anchor.timestamp, + }; + let bias = match proto::Bias::from_i32(anchor.bias)? { + proto::Bias::Left => Bias::Left, + proto::Bias::Right => Bias::Right, + }; + Some(Anchor::new( + timestamp, + anchor.offset as usize, + bias, buffer_id, - }) + )) } /// Returns a `[clock::Lamport`] timestamp for the given [`proto::Operation`]. diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 276ecc70c10f157f6fcef37f508fb4d9c16d3f04..ad3d4bdb703548f86304ac6c3892f3cabab01caa 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -5598,7 +5598,7 @@ impl LspStore { buffer .update(cx, |buffer, _| { - buffer.wait_for_edits(Some(position.timestamp)) + buffer.wait_for_edits(Some(position.timestamp())) }) .await?; this.update(cx, |this, cx| { diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 406a065f65368a1c8b6cb4df820d274c9200ec85..22a5c3090a1ef9e1c3581893ae8cbe16f79d776b 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -9,9 +9,15 @@ use sum_tree::{Bias, Dimensions}; #[doc(alias = "TextAnchor")] #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Anchor { - /// The timestamp of the operation that inserted the text - /// in which this anchor is located. - pub timestamp: clock::Lamport, + // /// The timestamp of the operation that inserted the text + // /// in which this anchor is located. + // pub(crate) timestamp: clock::Lamport, + // we store the replica id and sequence number of the timestamp inline + // to avoid the alignment of our fields from increasing the size of this struct + // This saves 8 bytes, by allowing replica id, value and bias to occupy the padding + timestamp_replica_id: clock::ReplicaId, + timestamp_value: clock::Seq, + /// The byte offset into the text inserted in the operation /// at `timestamp`. pub offset: usize, @@ -31,7 +37,7 @@ impl Debug for Anchor { } f.debug_struct("Anchor") - .field("timestamp", &self.timestamp) + .field("timestamp", &self.timestamp()) .field("offset", &self.offset) .field("bias", &self.bias) .field("buffer_id", &self.buffer_id) @@ -41,22 +47,40 @@ impl Debug for Anchor { impl Anchor { pub const MIN: Self = Self { - timestamp: clock::Lamport::MIN, + timestamp_replica_id: clock::Lamport::MIN.replica_id, + timestamp_value: clock::Lamport::MIN.value, offset: usize::MIN, bias: Bias::Left, buffer_id: None, }; pub const MAX: Self = Self { - timestamp: clock::Lamport::MAX, + timestamp_replica_id: clock::Lamport::MAX.replica_id, + timestamp_value: clock::Lamport::MAX.value, offset: usize::MAX, bias: Bias::Right, buffer_id: None, }; + pub fn new( + timestamp: clock::Lamport, + offset: usize, + bias: Bias, + buffer_id: Option, + ) -> Self { + Self { + timestamp_replica_id: timestamp.replica_id, + timestamp_value: timestamp.value, + offset, + bias, + buffer_id, + } + } + pub fn min_for_buffer(buffer_id: BufferId) -> Self { Self { - timestamp: clock::Lamport::MIN, + timestamp_replica_id: clock::Lamport::MIN.replica_id, + timestamp_value: clock::Lamport::MIN.value, offset: usize::MIN, bias: Bias::Left, buffer_id: Some(buffer_id), @@ -65,7 +89,8 @@ impl Anchor { pub fn max_for_buffer(buffer_id: BufferId) -> Self { Self { - timestamp: clock::Lamport::MAX, + timestamp_replica_id: clock::Lamport::MAX.replica_id, + timestamp_value: clock::Lamport::MAX.value, offset: usize::MAX, bias: Bias::Right, buffer_id: Some(buffer_id), @@ -85,7 +110,7 @@ impl Anchor { } pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering { - let fragment_id_comparison = if self.timestamp == other.timestamp { + let fragment_id_comparison = if self.timestamp() == other.timestamp() { Ordering::Equal } else { buffer @@ -164,16 +189,24 @@ impl Anchor { } pub fn is_min(&self) -> bool { - self.timestamp == clock::Lamport::MIN + self.timestamp() == clock::Lamport::MIN && self.offset == usize::MIN && self.bias == Bias::Left } pub fn is_max(&self) -> bool { - self.timestamp == clock::Lamport::MAX + self.timestamp() == clock::Lamport::MAX && self.offset == usize::MAX && self.bias == Bias::Right } + + #[inline] + pub fn timestamp(&self) -> clock::Lamport { + clock::Lamport { + replica_id: self.timestamp_replica_id, + value: self.timestamp_value, + } + } } pub trait OffsetRangeExt { diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 91767d99841b721d529de45229c1b067a563cf72..43e41fd4d289dbd6dd5dc0c49f368ac2385b518a 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1657,10 +1657,10 @@ impl Buffer { ) -> impl 'static + Future> + use { let mut futures = Vec::new(); for anchor in anchors { - if !self.version.observed(anchor.timestamp) && !anchor.is_max() && !anchor.is_min() { + if !self.version.observed(anchor.timestamp()) && !anchor.is_max() && !anchor.is_min() { let (tx, rx) = oneshot::channel(); self.edit_id_resolvers - .entry(anchor.timestamp) + .entry(anchor.timestamp()) .or_default() .push(tx); futures.push(rx); @@ -2275,7 +2275,7 @@ impl BufferSnapshot { }; assert_eq!( insertion.timestamp, - anchor.timestamp, + anchor.timestamp(), "invalid insertion for buffer {}@{:?} and anchor {:?}", self.remote_id(), self.version, @@ -2309,13 +2309,14 @@ impl BufferSnapshot { } else { debug_assert_eq!(anchor.buffer_id, Some(self.remote_id)); debug_assert!( - self.version.observed(anchor.timestamp), + self.version.observed(anchor.timestamp()), "Anchor timestamp {:?} not observed by buffer {:?}", - anchor.timestamp, + anchor.timestamp(), self.version ); let item = self.try_find_fragment(anchor); - let Some(insertion) = item.filter(|insertion| insertion.timestamp == anchor.timestamp) + let Some(insertion) = + item.filter(|insertion| insertion.timestamp == anchor.timestamp()) else { self.panic_bad_anchor(anchor); }; @@ -2343,7 +2344,7 @@ impl BufferSnapshot { "invalid anchor - buffer id does not match: anchor {anchor:?}; buffer id: {}, version: {:?}", self.remote_id, self.version ); - } else if !self.version.observed(anchor.timestamp) { + } else if !self.version.observed(anchor.timestamp()) { panic!( "invalid anchor - snapshot has not observed lamport: {:?}; version: {:?}", anchor, self.version @@ -2369,7 +2370,7 @@ impl BufferSnapshot { } else { let item = self.try_find_fragment(anchor); item.filter(|insertion| { - !cfg!(debug_assertions) || insertion.timestamp == anchor.timestamp + !cfg!(debug_assertions) || insertion.timestamp == anchor.timestamp() }) .map(|insertion| &insertion.fragment_id) } @@ -2377,7 +2378,7 @@ impl BufferSnapshot { fn try_find_fragment(&self, anchor: &Anchor) -> Option<&InsertionFragment> { let anchor_key = InsertionFragmentKey { - timestamp: anchor.timestamp, + timestamp: anchor.timestamp(), split_offset: anchor.offset, }; match self.insertions.find_with_prev::( @@ -2453,19 +2454,20 @@ impl BufferSnapshot { return Anchor::max_for_buffer(self.remote_id); }; let overshoot = offset - start; - Anchor { - timestamp: fragment.timestamp, - offset: fragment.insertion_offset + overshoot, + Anchor::new( + fragment.timestamp, + fragment.insertion_offset + overshoot, bias, - buffer_id: Some(self.remote_id), - } + Some(self.remote_id), + ) } } pub fn can_resolve(&self, anchor: &Anchor) -> bool { anchor.is_min() || anchor.is_max() - || (Some(self.remote_id) == anchor.buffer_id && self.version.observed(anchor.timestamp)) + || (Some(self.remote_id) == anchor.buffer_id + && self.version.observed(anchor.timestamp())) } pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { @@ -2760,18 +2762,18 @@ impl bool> Iterator for Ed break; } - let start_anchor = Anchor { - timestamp: fragment.timestamp, - offset: fragment.insertion_offset, - bias: Bias::Right, - buffer_id: Some(self.buffer_id), - }; - let end_anchor = Anchor { - timestamp: fragment.timestamp, - offset: fragment.insertion_offset + fragment.len, - bias: Bias::Left, - buffer_id: Some(self.buffer_id), - }; + let start_anchor = Anchor::new( + fragment.timestamp, + fragment.insertion_offset, + Bias::Right, + Some(self.buffer_id), + ); + let end_anchor = Anchor::new( + fragment.timestamp, + fragment.insertion_offset + fragment.len, + Bias::Left, + Some(self.buffer_id), + ); if !fragment.was_visible(self.since, self.undos) && fragment.visible { let mut visible_end = cursor.end().visible;