You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/23 06:29:11 UTC

(maka) branch rust-preview updated: perf(nsh): reuse layouts when changed text measures identically

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/rust-preview by this push:
     new ba41143b8e perf(nsh): reuse layouts when changed text measures identically
ba41143b8e is described below

commit ba41143b8e2fec7a1b3471cd3e2b472afc367e86
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Fri Oct 23 15:28:56 2026 +0900

    perf(nsh): reuse layouts when changed text measures identically
    
    Integrate longbridge/nsh 44278a653d73b7f8626e964f7895b8a4c4a964dc with directory-level three-way merging. Preserve local hint owner lifetimes, reading behavior, terminal features and attachment relayout. Refresh imported source provenance and only the seven changed NSH notice inputs.
    
    Generated-by: OpenAI Codex
---
 products/maka/cli/notices.json                     |  14 +--
 third-party/nsh/SOURCE.md                          |   2 +-
 .../nsh/crates/nsh-core/src/core/layout_tree.rs    |  61 ++++++++++-
 third-party/nsh/crates/nsh-core/src/core/mod.rs    |  19 +++-
 third-party/nsh/crates/nsh-core/src/core/tests.rs  | 114 ++++++++++++++++++---
 third-party/nsh/docs/dev/performance.md            |   8 +-
 third-party/nsh/docs/dev/pitfalls.md               |   6 ++
 third-party/nsh/upstream.json                      |  22 ++--
 8 files changed, 208 insertions(+), 38 deletions(-)

diff --git a/products/maka/cli/notices.json b/products/maka/cli/notices.json
index 87dfdfa52e..0fc4be1ac0 100644
--- a/products/maka/cli/notices.json
+++ b/products/maka/cli/notices.json
@@ -1027,7 +1027,7 @@
     "third-party/nsh/Cargo.toml": "8b8808f82642ac958bb922917b2d986b485c12fcc7667997ca2123c83ec06a92",
     "third-party/nsh/LICENSE-APACHE": "ec75427ad955e3c740f22e7dc3f6db6fc5b2cc1d9135c2af91cece8dd47a2d38",
     "third-party/nsh/README.md": "ff632ba97e2af5f8f0d69ed03250dd2a7d90f53da48b7a07976394d37f85aacb",
-    "third-party/nsh/SOURCE.md": "443dbf9c5526010aa6bc3e94cbfc82970fbb7cbb90317b954c6508c76c945435",
+    "third-party/nsh/SOURCE.md": "031818282de156628cee8236b5ebb455a5afa6e29aa294f7ab653d5622f2ad33",
     "third-party/nsh/crates/gpui/Cargo.lock": "2b8a4fd17ecabf2cb11520392322bf5aaa2736a8f79989d9e55ab8795360487b",
     "third-party/nsh/crates/gpui/Cargo.toml": "1d53eec1242cd480700a9e2ffd5c013f21c8b0c3207cb9544733fd5ea375f0f1",
     "third-party/nsh/crates/gpui/nsh-gpui-host/Cargo.toml": "f36b982a17b38bd1d91760a2d8d9db06b42db949c227878cef294a5ff2100844",
@@ -1217,17 +1217,17 @@
     "third-party/nsh/crates/nsh-core/src/color.rs": "93afe9bf38e8f7b12108fedc4863f8e1bee3a55ae7d0c75e788b79b02fb72a93",
     "third-party/nsh/crates/nsh-core/src/core/classes.rs": "eeea690ba30e01bd1b74aebb83358475a62265fe2d902b119cb97389b84d8765",
     "third-party/nsh/crates/nsh-core/src/core/components.rs": "93f3cb23f3a570c620d8204bf46a125885f35cebe71e56e637eaafcb673dd057",
-    "third-party/nsh/crates/nsh-core/src/core/layout_tree.rs": "d52a8d38c0dd4d93d193b61d9070a8d969d3067e1212e1fc6af758d1446809e3",
+    "third-party/nsh/crates/nsh-core/src/core/layout_tree.rs": "d28e3c5657e308c9cc254529457a025b057735d743e529bcf5e11734b8b91109",
     "third-party/nsh/crates/nsh-core/src/core/list.rs": "e709dcf029ac3d8d0c4bc87b4e1984ce07f50e18266d0759eaed773de1902684",
     "third-party/nsh/crates/nsh-core/src/core/measure.rs": "c6692abd3002576afef5b6d777d7694e28e4754bf7ada0adf8ed1c92419289c1",
-    "third-party/nsh/crates/nsh-core/src/core/mod.rs": "4c3df633d6260d6da543b675b23f46007c058e0eb078a2ee22b8e90ada2c7ba6",
+    "third-party/nsh/crates/nsh-core/src/core/mod.rs": "396f2523801a88fcf050ef3e80a6e0fc8eea15b19315049e88166852f9a2d3fe",
     "third-party/nsh/crates/nsh-core/src/core/overlay.rs": "4a65103b3cf7a35e11036532866b171dafd0bddbfe6517849520010e39d01239",
     "third-party/nsh/crates/nsh-core/src/core/parent_viewport.rs": "af5798fb792c837a9cbd0654bb04aeb867ecf784d140ea159ebce326cbaf6e7d",
     "third-party/nsh/crates/nsh-core/src/core/reading.rs": "3bb3d25dc8b97f35eb73dcbaf2d6f1b5dff8cdbdd012a8edcef7fab6661f3d9f",
     "third-party/nsh/crates/nsh-core/src/core/renders.rs": "eba08379b95272762630258bba8c9d1908f8e8ec94d9518bfd59470edf49b5cd",
     "third-party/nsh/crates/nsh-core/src/core/services.rs": "8812eefd00661841ff2b1de5d4f8cf68dd667da4151b4c79ed966cc940934752",
     "third-party/nsh/crates/nsh-core/src/core/sticky.rs": "2c508038c1677fc10f11b5e6c1bf245cb78a75a02beaac24d0b09b2033a4beaa",
-    "third-party/nsh/crates/nsh-core/src/core/tests.rs": "183cc85ffbedd4144a888af749433e1bbe77184548625c80fa0abeaef0877700",
+    "third-party/nsh/crates/nsh-core/src/core/tests.rs": "9df804c36eecc30b8f8efad2c3b60ef67e1401c17f81b1ead276d9c504b6dd9c",
     "third-party/nsh/crates/nsh-core/src/core/touch.rs": "a8559c023a443a6defad135af2f319e60130e7df23af2a8353239b0862e494a8",
     "third-party/nsh/crates/nsh-core/src/core/views.rs": "c32c59533d5ee33bc16677b0459816f7670c224f3f8de3507c72029989660834",
     "third-party/nsh/crates/nsh-core/src/css.rs": "56760520c706f64505849d5a32a1eada2d9fd243a5c70e07f21bc8ed0105217f",
@@ -1846,8 +1846,8 @@
     "third-party/nsh/docs/dev/linux.md": "4441ec64b6edc5c03084bd203aa5114c066f9e2c0541ee320be8f17b6f82c3fe",
     "third-party/nsh/docs/dev/macos.md": "827c809b5774681c77daf05ac6603342c027dfd459e78f55558044861711dcd3",
     "third-party/nsh/docs/dev/native-pipeline.md": "07f803365af6ac90d5247c70d2e85ce9e03393f0e9e122c46690f85e381a0c55",
-    "third-party/nsh/docs/dev/performance.md": "9b763371132747a068259288cdd6019d7dfe37553d4413c0a9a330d9339f704a",
-    "third-party/nsh/docs/dev/pitfalls.md": "366400d2c07ab095536caa2907e0e5e451dcc68c2a08ccb88264cbc2fe23e24d",
+    "third-party/nsh/docs/dev/performance.md": "6d3f36e2e44b5ccba8e1110b0b6fdfe315768444af325451bcebf019734e6d11",
+    "third-party/nsh/docs/dev/pitfalls.md": "80fc820be5136534546eee9790dbc3b824cd1053fe45432f0187f4d25641bdb3",
     "third-party/nsh/docs/dev/reactive-components.md": "cd447fe8cfbe9026d7cadd5fa42d9c387e4c1b1b7e1c5adddd55c3f1beb94a4d",
     "third-party/nsh/docs/dev/rust-classes.md": "1f70015213dbcf56e6d6e4641019092003c22eb57ca324d9c4a85578b4468139",
     "third-party/nsh/docs/dev/rust-modules.md": "f793bfb12272ded93e80876fd2c72ff8dc0f1bd0337977f39b750af495a83592",
@@ -2300,7 +2300,7 @@
     "third-party/nsh/third-party/icy_sixel/src/encoder.rs": "9f2941a073f3df5d1216bb4f71487a3c43b5ceded1ec88cd13a804b4d7e31999",
     "third-party/nsh/third-party/icy_sixel/src/lib.rs": "126c4edada7c430377a73839c9da93cae95f9191be69c1401fb4ac1cad9cbf5e",
     "third-party/nsh/third-party/icy_sixel/src/sixel_image.rs": "1e968370dd0ff8c69751213a1ed11cfce6783feabb30c2b5074ac1c4ba3d8503",
-    "third-party/nsh/upstream.json": "abd7c1558fd84b72a24db1d5b76b5d4009f484081a17af07ab703899daca816f",
+    "third-party/nsh/upstream.json": "af929e730cc5f51898817f8524d59ae37b193bb961c02a8fcab3c21f06958b49",
     "third-party/nsh/website/DESIGN.md": "484ab5b78233c1c57b0d46e631d2341031b2b34a74740abbb88a275b4c8134b7",
     "third-party/nsh/website/README.md": "50de9faed8cd3b0751991d6168ac6e33a6437dfd36aa15dc30d35b13dfb94284",
     "third-party/nsh/website/astro.config.mjs": "0cfae54c7791bd915865dc535669b311e4f76001efed09fdb2772d332812924f",
diff --git a/third-party/nsh/SOURCE.md b/third-party/nsh/SOURCE.md
index a52c40b41e..f40fcb760f 100644
--- a/third-party/nsh/SOURCE.md
+++ b/third-party/nsh/SOURCE.md
@@ -26,7 +26,7 @@ at `de176695240d550f2b303643b8019146c7989e7f`. The upstream project is
 
 `upstream.json` keeps that initial `repository` and `revision`. Its
 `upstream_revision` records the latest integrated `longbridge/nsh` main revision:
-`111b3a933319fadb0f1da9a59909ca526f256386`. The first upstream synchronization
+`44278a653d73b7f8626e964f7895b8a4c4a964dc`. The first upstream synchronization
 starts from `6bb0b7908043132067456e4a21dd57c90a0bdce5`, already included in the
 initial fork import.
 
diff --git a/third-party/nsh/crates/nsh-core/src/core/layout_tree.rs b/third-party/nsh/crates/nsh-core/src/core/layout_tree.rs
index 7da42b2415..082abaca8f 100644
--- a/third-party/nsh/crates/nsh-core/src/core/layout_tree.rs
+++ b/third-party/nsh/crates/nsh-core/src/core/layout_tree.rs
@@ -22,6 +22,12 @@
 //!   when what it gives back differs (its overflow, say) is its parent laid
 //!   out again. A large page with values changing often costs what changes,
 //!   not what the page is.
+//! - **leaves that measure as before.** A leaf whose content changed (its
+//!   text) dirties nothing when it measures the same for every input the
+//!   layout asked of it since its cache was last cleared
+//!   ([`LayoutTree::measures_as_before`]): every layout above was computed
+//!   from those answers alone, so all of them stand. A price that keeps its
+//!   width, a wrapped paragraph that keeps its lines, cost their text.
 
 use slab::Slab;
 use taffy::{
@@ -52,8 +58,18 @@ struct LayoutNode<C> {
     last: Option<(LayoutInput, LayoutOutput)>,
     /// It is a boundary waiting in [`LayoutTree::dirty_boundaries`].
     queued: bool,
+    /// A leaf's answers to the inputs the layout asked of it since its
+    /// cache was last cleared, each once: what the layouts above it were
+    /// computed from. `None` once there were more than [`MEASURED`] to keep.
+    measured: Option<Vec<(LayoutInput, LayoutOutput)>>,
 }
 
+/// How many answers of a leaf [`LayoutNode::measured`] keeps: a layout asks
+/// a few (its sizes under a measuring pass and the final one); more come of
+/// many layouts at other sizes (a window resized), and are not worth
+/// measuring again.
+const MEASURED: usize = 8;
+
 impl<C> LayoutNode<C> {
     fn new(style: Style, context: Option<C>, lifetime: u64) -> Self {
         LayoutNode {
@@ -68,8 +84,15 @@ impl<C> LayoutNode<C> {
             tag: None,
             last: None,
             queued: false,
+            measured: Some(Vec::new()),
         }
     }
+
+    /// Clears its cached layouts, and so what they were computed from.
+    fn clear_cache(&mut self) {
+        self.cache.clear();
+        self.measured = Some(Vec::new());
+    }
 }
 
 /// The layout nodes of a core, each with a context `C` its leaves are
@@ -156,7 +179,7 @@ impl<C> LayoutTree<C> {
     pub(crate) fn set_style(&mut self, node: NodeId, style: Style) {
         let n = &mut self.nodes[key(node)];
         n.style = style;
-        n.cache.clear();
+        n.clear_cache();
         if let Some(parent) = n.parent {
             self.mark_dirty(parent);
         }
@@ -213,7 +236,7 @@ impl<C> LayoutTree<C> {
     pub(crate) fn mark_dirty(&mut self, node: NodeId) {
         let mut at = Some(node);
         while let Some(node) = at {
-            self.nodes[key(node)].cache.clear();
+            self.nodes[key(node)].clear_cache();
             if self.is_boundary(node) {
                 let n = &mut self.nodes[key(node)];
                 if !n.queued {
@@ -258,6 +281,26 @@ impl<C> LayoutTree<C> {
             && !style.align_self.is_some_and(baseline)
     }
 
+    /// What measures the leaf `node` changed (its text, say): whether it
+    /// measures with `measure` as it did for every input the layout asked of
+    /// it since its cache was last cleared, so that every layout above it
+    /// stands and nothing needs laying out again. When it does not, or it
+    /// was asked too much to tell, the caller marks it dirty
+    /// ([`LayoutTree::mark_dirty`]). A leaf never asked (new, or dirty
+    /// already) measures as before: nothing was computed from it.
+    pub(crate) fn measures_as_before<M>(&mut self, node: NodeId, mut measure: M) -> bool
+    where
+        M: FnMut(LayoutInput, NodeId, Option<&mut C>, &Style) -> LayoutOutput,
+    {
+        let n = &mut self.nodes[key(node)];
+        let Some(measured) = &n.measured else {
+            return false;
+        };
+        measured
+            .iter()
+            .all(|(input, output)| measure(*input, node, n.context.as_mut(), &n.style) == *output)
+    }
+
     /// Whether `node` has no cached layout: it changed, or something in it
     /// did, since it was last laid out.
     pub(crate) fn dirty(&self, node: NodeId) -> bool {
@@ -367,7 +410,17 @@ where
                 (Display::Grid, true) => compute_grid_layout(view, node, inputs),
                 (_, false) => {
                     let n = &mut view.tree.nodes[key(node)];
-                    (view.measure)(inputs, node, n.context.as_mut(), &n.style)
+                    let output = (view.measure)(inputs, node, n.context.as_mut(), &n.style);
+                    if let Some(measured) = &mut n.measured
+                        && !measured.iter().any(|(asked, _)| *asked == inputs)
+                    {
+                        if measured.len() < MEASURED {
+                            measured.push((inputs, output.clone()));
+                        } else {
+                            n.measured = None;
+                        }
+                    }
+                    output
                 }
             };
             // The out-of-flow nodes this one is the containing block of, in
@@ -476,7 +529,7 @@ where
     }
 
     fn cache_clear(&mut self, node: NodeId) {
-        self.tree.nodes[key(node)].cache.clear();
+        self.tree.nodes[key(node)].clear_cache();
     }
 }
 
diff --git a/third-party/nsh/crates/nsh-core/src/core/mod.rs b/third-party/nsh/crates/nsh-core/src/core/mod.rs
index ab45f8c35e..9abd124399 100644
--- a/third-party/nsh/crates/nsh-core/src/core/mod.rs
+++ b/third-party/nsh/crates/nsh-core/src/core/mod.rs
@@ -4315,8 +4315,23 @@ impl<M: TextEngine> Core<M> {
         if !same_layout {
             cx.layout_hash = cx.alignments.layout_hash(&cx.text);
             cx.cache = None;
-            self.layout.mark_dirty(layout);
-            self.needs_layout = true;
+            // A text that measures as before leaves every layout standing:
+            // only the text is mounted again. Attachments are placed by its
+            // final layout, laid out again with the rest.
+            let attachments =
+                !cx.alignments.is_empty() || !self.nodes[key as usize].attached.is_empty();
+            let engine = &self.text_engine;
+            let cache = &mut self.measure_cache;
+            if attachments
+                || !self
+                    .layout
+                    .measures_as_before(layout, |input, _, cx, style| {
+                        measure_leaf(engine, cache, input, cx, style)
+                    })
+            {
+                self.layout.mark_dirty(layout);
+                self.needs_layout = true;
+            }
         }
         true
     }
diff --git a/third-party/nsh/crates/nsh-core/src/core/tests.rs b/third-party/nsh/crates/nsh-core/src/core/tests.rs
index 8f25e2b1a6..f53a58d785 100644
--- a/third-party/nsh/crates/nsh-core/src/core/tests.rs
+++ b/third-party/nsh/crates/nsh-core/src/core/tests.rs
@@ -12295,14 +12295,19 @@ mod views {
         /// Its cells have a size of their own: what their text says moves
         /// nothing around them.
         fixed: bool,
+        /// Each tick changes how wide the changing values are (a sign comes
+        /// and goes); otherwise they keep their width, but for the rare
+        /// digit a price gains (the tests' text engine gives every
+        /// character one width, as tabular figures have).
+        widening: bool,
     }
 
     /// A page of high-frequency updates, a quote board: 80 rows of a
     /// mark and five cells (about a thousand nodes), the first cells of 40
-    /// rows changing together on each press, their texts' lengths too.
+    /// rows changing together on each press.
     #[component]
     fn Board(props: BoardProps) -> impl IntoElement {
-        let fixed = props.fixed;
+        let (fixed, widening) = (props.fixed, props.widening);
         let ticks: Vec<Signal<u32>> = (0..BOARD_TICKING).map(|_| signal(0)).collect();
         let pressed = ticks.clone();
         let cell = move |content: Text| {
@@ -12323,10 +12328,11 @@ mod views {
             })
             .children((0..BOARD_ROWS).map(|row| {
                 let first = match ticks.get(row) {
-                    // A price: its length changes as it grows.
-                    Some(&tick) => {
-                        text(move || format!("{}.{:02}", 9 + tick.get() / 3, tick.get() % 100))
-                    }
+                    Some(&tick) => text(move || {
+                        let t = tick.get();
+                        let sign = if widening && t % 2 == 1 { "+" } else { "" };
+                        format!("{sign}{}.{:02}", 9 + t / 3, t % 100)
+                    }),
                     None => text(format!("{row}.00")),
                 };
                 view()
@@ -12343,14 +12349,18 @@ mod views {
     /// `cargo test --release -p nsh-core bench_high_frequency -- --ignored --nocapture`
     ///
     /// The cost of a commit when many values of a large page change at
-    /// once and often: [`Board`] with fixed cells and with cells that
-    /// take their text's size.
+    /// once and often: [`Board`] with fixed cells, and with cells that
+    /// take their text's size, the values keeping their widths or not.
     #[test]
     #[ignore]
     fn bench_high_frequency() {
-        for fixed in [true, false] {
+        for (fixed, widening, label) in [
+            (true, false, "fixed cells"),
+            (false, false, "content-sized cells, same widths"),
+            (false, true, "content-sized cells, new widths"),
+        ] {
             let mut c = core();
-            c.mount_root(&Root::new::<Board>(move || BoardProps { fixed }));
+            c.mount_root(&Root::new::<Board>(move || BoardProps { fixed, widening }));
             let first = c.commit().unwrap();
             let nodes = first
                 .items
@@ -12370,9 +12380,8 @@ mod views {
                 commit += mid.elapsed();
             }
             eprintln!(
-                "high frequency ({} cells, {nodes} views, {BOARD_TICKING} values a tick): \
+                "high frequency ({label}, {nodes} views, {BOARD_TICKING} values a tick): \
                  {:?} per event, {:?} per commit, {} mount items",
-                if fixed { "fixed" } else { "content-sized" },
                 event / n,
                 commit / n,
                 items / n as usize
@@ -18115,3 +18124,84 @@ fn a_relayout_boundary_that_overflows_more_lays_its_parent_out_again() {
     });
     assert_eq!(content, Some(500.0));
 }
+
+#[test]
+fn a_text_that_measures_as_before_lays_nothing_out_again() {
+    let mut c = core();
+    apply(
+        &mut c,
+        vec![
+            Op::Create {
+                id: 2,
+                tag: Tag::View,
+            },
+            style_op(2, &[("flexDirection", StyleValue::Str("row"))]),
+            Op::Create {
+                id: 4,
+                tag: Tag::Text,
+            },
+            Op::CreateText {
+                id: 5,
+                text: "9.07".into(),
+            },
+            Op::Insert {
+                parent: 4,
+                child: 5,
+                before: None,
+            },
+            Op::Create {
+                id: 6,
+                tag: Tag::View,
+            },
+            style_op(
+                6,
+                &[
+                    ("width", StyleValue::Number(10.0)),
+                    ("height", StyleValue::Number(10.0)),
+                ],
+            ),
+            Op::Insert {
+                parent: 2,
+                child: 4,
+                before: None,
+            },
+            Op::Insert {
+                parent: 2,
+                child: 6,
+                before: None,
+            },
+            Op::Insert {
+                parent: ROOT_ID,
+                child: 2,
+                before: None,
+            },
+        ],
+    );
+    let first = c.commit().unwrap();
+    let after = frame_of(&first, 6).unwrap().origin.x;
+    let root = c.nodes[c.root as usize].layout.unwrap();
+    // As wide as before (the tests' engine gives each character one width):
+    // nothing to lay out again, the text mounted again.
+    c.apply(Op::SetText {
+        id: 5,
+        text: "9.14".into(),
+    })
+    .unwrap();
+    c.flush_dirty();
+    assert!(!c.needs_layout && !c.layout.dirty(root));
+    let batch = c.commit().unwrap();
+    assert!(
+        matches!(&batch.items[..], [MountItem::Text { id: 4, text, .. }] if text.text == "9.14")
+    );
+    // Wider: laid out again, and its sibling moves.
+    c.apply(Op::SetText {
+        id: 5,
+        text: "+9.21".into(),
+    })
+    .unwrap();
+    c.flush_dirty();
+    assert!(c.needs_layout && c.layout.dirty(root));
+    let batch = c.commit().unwrap();
+    assert!(frame_of(&batch, 4).is_some());
+    assert!(frame_of(&batch, 6).unwrap().origin.x > after);
+}
diff --git a/third-party/nsh/docs/dev/performance.md b/third-party/nsh/docs/dev/performance.md
index 37febe6bf8..c8bf517d86 100644
--- a/third-party/nsh/docs/dev/performance.md
+++ b/third-party/nsh/docs/dev/performance.md
@@ -123,7 +123,7 @@ coalescing interval, with the latest coordinates kept independently per view and
   ~82µs (the text's relayout and the tree's layout pass), one mount item. The former model rendered the whole view
   again and diffed it: ~312µs per commit for the same view.
 - High-frequency updates (`cargo test --release -p nsh-core bench_high_frequency -- --ignored --nocapture`: `Board`, a quote board of
-  80 rows of a mark and five cells, 961 views, the first cells of 40 rows changing together on each press, their texts' lengths too):
+  80 rows of a mark and five cells, 961 views, the first cells of 40 rows changing together on each press):
   ~4.4µs per event, then per commit ~140µs when the cells have a size of their own and ~180µs when they take their text's, 40 mount
   items either way. Nearly all of the commit is Taffy laying the dirty paths out again (~116µs of it, measured once by timing the
   commit's phases): a changed text dirties every view up to the root, and the flex algorithm of the root and of each changed row runs
@@ -131,6 +131,12 @@ coalescing interval, with the latest coordinates kept independently per view and
   frame walk is ~10µs, the texts (the tests' engine) under 2µs. This is the baseline relayout boundaries are measured against.
   With relayout boundaries (`LayoutTree::is_boundary`): ~36µs per commit for the fixed cells, each changed cell laid out on its own
   and nothing above it; content-sized cells are not boundaries (their size is their text's) and stay at ~180µs.
+  With texts that measure as before (`LayoutTree::measures_as_before`: a changed text measured again for every input the layout asked
+  of it; the same answers, and nothing is laid out again): ~20µs for the fixed cells and ~15µs for content-sized cells whose values
+  keep their widths (the tests' text engine gives every character one width, as tabular figures do), against ~185µs before; when
+  every value changes width (a sign coming and going) nothing measures as before and the commit is ~192µs, against ~184µs (the
+  answers recorded and measured again). The component benchmark's update of 200 titles: core 0.19ms (Rust Button) and 0.09ms
+  (TSX), against 0.43ms and 0.35ms.
 - Rust components' handles (`handle_bench`: `cargo test --release -p nsh-engine --test engine bench_handles --
   --ignored --nocapture --test-threads=1`, 1000 calls in one press, the minimum of 200 presses, per call; steady
   state of five runs). In brackets, when `AnimatedValue` was an `Rc` of its node and `ElementRef` an `Rc` of its
diff --git a/third-party/nsh/docs/dev/pitfalls.md b/third-party/nsh/docs/dev/pitfalls.md
index 2cb086bc17..a452ac897a 100644
--- a/third-party/nsh/docs/dev/pitfalls.md
+++ b/third-party/nsh/docs/dev/pitfalls.md
@@ -65,6 +65,12 @@ Things that were found the hard way. Read the section of the area you change bef
   content), and shrinking is floored at the content's automatic minimum, so either let the content change the size (the layout
   consistency harness found the first). Growing is fine: it takes the free space the parent and siblings leave. A restyled node always
   dirties its parent (`set_style`), a boundary included. Boundaries' frames are emitted after the passes (`commit_round`).
+- **A changed text dirties nothing when it measures as before** (`LayoutTree::measures_as_before`, from `rebuild_text`): every
+  layout above a leaf was computed from the leaf's answers to the inputs it was asked (Taffy calls it under measuring passes and for
+  its final size), so the leaf keeps those answers since its cache was last cleared (`LayoutNode::measured`, at most eight: more come
+  of layouts at many sizes) and measures them again; all the same, and only the text is mounted again. The record is cleared with the
+  cache, never alone: an answer dropped while a layout above still holds what it was computed from would let a change through
+  unseen. Texts with attachments always relayout (their views are placed by the text's final layout).
 - **List cells are independent Taffy roots** (not children of the list's Taffy node; their width follows the list's cross axis): scrolling only
   relays out dirty cells and only emits frames for relaid cell subtrees; when the main tree is clean, `compute_layout` and the frame walk of the
   main tree are skipped.
diff --git a/third-party/nsh/upstream.json b/third-party/nsh/upstream.json
index 1fd22c9289..a21d2d7bb8 100644
--- a/third-party/nsh/upstream.json
+++ b/third-party/nsh/upstream.json
@@ -231,17 +231,17 @@
     "crates/nsh-core/src/color.rs": "93afe9bf38e8f7b12108fedc4863f8e1bee3a55ae7d0c75e788b79b02fb72a93",
     "crates/nsh-core/src/core/classes.rs": "242ff14438c0785b6d29af6315273d4f0256cc048fad221b4cabd2f3a0f2027f",
     "crates/nsh-core/src/core/components.rs": "93f3cb23f3a570c620d8204bf46a125885f35cebe71e56e637eaafcb673dd057",
-    "crates/nsh-core/src/core/layout_tree.rs": "dc834a3994e16fc86df87c99bb6d6a718adf30693b3e9ea5f91e5bbd3796741d",
+    "crates/nsh-core/src/core/layout_tree.rs": "767715a7dbbb6d28718cfea4b2893dddc7a0237d48057618738ea80616080c33",
     "crates/nsh-core/src/core/list.rs": "e709dcf029ac3d8d0c4bc87b4e1984ce07f50e18266d0759eaed773de1902684",
     "crates/nsh-core/src/core/measure.rs": "c6692abd3002576afef5b6d777d7694e28e4754bf7ada0adf8ed1c92419289c1",
-    "crates/nsh-core/src/core/mod.rs": "641f85478a333a2063fb7c2899869bbb271f8eae89b7d38322f374e5300eb00d",
+    "crates/nsh-core/src/core/mod.rs": "50805dbbb2f7f3b00a100d6b7a30cb4c5702c22063ddb4fa38f8d4e3a8d57906",
     "crates/nsh-core/src/core/overlay.rs": "7839b8e7da75b4599321d486a8891dfbd3a4514bc1992926b60fdb5b64640f29",
     "crates/nsh-core/src/core/parent_viewport.rs": "1326e1b74de2d14cb9b0a99be4ff00b6f2509f79c6a98a3597e8508ac08d3fa6",
     "crates/nsh-core/src/core/reading.rs": "3bb3d25dc8b97f35eb73dcbaf2d6f1b5dff8cdbdd012a8edcef7fab6661f3d9f",
     "crates/nsh-core/src/core/renders.rs": "eba08379b95272762630258bba8c9d1908f8e8ec94d9518bfd59470edf49b5cd",
     "crates/nsh-core/src/core/services.rs": "8812eefd00661841ff2b1de5d4f8cf68dd667da4151b4c79ed966cc940934752",
     "crates/nsh-core/src/core/sticky.rs": "2c508038c1677fc10f11b5e6c1bf245cb78a75a02beaac24d0b09b2033a4beaa",
-    "crates/nsh-core/src/core/tests.rs": "fdecbbbaee572484f54084e954911afd34f385e7f2cfb02874c82a7eca592386",
+    "crates/nsh-core/src/core/tests.rs": "d6f30766f59f6a3ad46c8e9d46b7416dd14142666f7743e6be2f82797316585a",
     "crates/nsh-core/src/core/touch.rs": "a8559c023a443a6defad135af2f319e60130e7df23af2a8353239b0862e494a8",
     "crates/nsh-core/src/core/views.rs": "c32c59533d5ee33bc16677b0459816f7670c224f3f8de3507c72029989660834",
     "crates/nsh-core/src/css.rs": "56760520c706f64505849d5a32a1eada2d9fd243a5c70e07f21bc8ed0105217f",
@@ -860,8 +860,8 @@
     "docs/dev/linux.md": "4441ec64b6edc5c03084bd203aa5114c066f9e2c0541ee320be8f17b6f82c3fe",
     "docs/dev/macos.md": "827c809b5774681c77daf05ac6603342c027dfd459e78f55558044861711dcd3",
     "docs/dev/native-pipeline.md": "07f803365af6ac90d5247c70d2e85ce9e03393f0e9e122c46690f85e381a0c55",
-    "docs/dev/performance.md": "55a4d5a9af1098aa3d7f1ee9fbb71d188576542e539b989aacdcaffc28d140bf",
-    "docs/dev/pitfalls.md": "366400d2c07ab095536caa2907e0e5e451dcc68c2a08ccb88264cbc2fe23e24d",
+    "docs/dev/performance.md": "4f23f433adab44865db61546c7b6e4515ea0a292ec3f625fe86881edf5e6997b",
+    "docs/dev/pitfalls.md": "80fc820be5136534546eee9790dbc3b824cd1053fe45432f0187f4d25641bdb3",
     "docs/dev/reactive-components.md": "cd447fe8cfbe9026d7cadd5fa42d9c387e4c1b1b7e1c5adddd55c3f1beb94a4d",
     "docs/dev/rust-classes.md": "1f70015213dbcf56e6d6e4641019092003c22eb57ca324d9c4a85578b4468139",
     "docs/dev/rust-modules.md": "f793bfb12272ded93e80876fd2c72ff8dc0f1bd0337977f39b750af495a83592",
@@ -1674,7 +1674,7 @@
     "website/zh-CN/modules/tailwind-reference.md": "9f366de10fe05a3fcf34f7e2ad7a6d7f8dc82379084f4958bef2c5761aff968f"
   },
   "upstream_repository": "https://github.com/longbridge/nsh",
-  "upstream_revision": "111b3a933319fadb0f1da9a59909ca526f256386",
+  "upstream_revision": "44278a653d73b7f8626e964f7895b8a4c4a964dc",
   "file_sources": {
     "AGENTS.md": {
       "repository": "https://github.com/longbridge/nsh",
@@ -1690,11 +1690,11 @@
     },
     "crates/nsh-core/src/core/layout_tree.rs": {
       "repository": "https://github.com/longbridge/nsh",
-      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+      "revision": "44278a653d73b7f8626e964f7895b8a4c4a964dc"
     },
     "crates/nsh-core/src/core/mod.rs": {
       "repository": "https://github.com/longbridge/nsh",
-      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+      "revision": "44278a653d73b7f8626e964f7895b8a4c4a964dc"
     },
     "crates/nsh-core/src/core/overlay.rs": {
       "repository": "https://github.com/longbridge/nsh",
@@ -1706,7 +1706,7 @@
     },
     "crates/nsh-core/src/core/tests.rs": {
       "repository": "https://github.com/longbridge/nsh",
-      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+      "revision": "44278a653d73b7f8626e964f7895b8a4c4a964dc"
     },
     "docs/dev/compatibility.md": {
       "repository": "https://github.com/longbridge/nsh",
@@ -1714,11 +1714,11 @@
     },
     "docs/dev/performance.md": {
       "repository": "https://github.com/longbridge/nsh",
-      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+      "revision": "44278a653d73b7f8626e964f7895b8a4c4a964dc"
     },
     "docs/dev/pitfalls.md": {
       "repository": "https://github.com/longbridge/nsh",
-      "revision": "111b3a933319fadb0f1da9a59909ca526f256386"
+      "revision": "44278a653d73b7f8626e964f7895b8a4c4a964dc"
     },
     "website/DESIGN.md": {
       "repository": "https://github.com/longbridge/nsh",


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org