Opening the mesh walk took 4.9 s, and 4.2 s of it was build_topology.
coalesce_prefixes folds a short hop hash onto the longer id it abbreviates.
It restarts the whole pass after each merge; each pass asks _extensions
once per node; and _extensions scanned every node in the graph. On this
device - 3053 stored packet paths, 1145 links, ~80 merges to settle - that
came to 24 million len calls and 7.6 million startswith per build.
Ids sharing a prefix are contiguous once sorted, so the extensions of a
short id are a range, not a search: bisect to its start and stop at the
first id that no longer carries the prefix. build_topology 4.22 -> 1.23 s
with real contacts, and the graph is byte-identical - 1133 links, 345
nodes, both dumped and diffed. On the console the open is 4.94 -> 1.95 s.
Contacts' 1.3 s open turns out not to be ours at all: heard_nodes 23 ms,
the screen 131 ms, first render 43 ms. The rest is the first devstate read
over the UART, already cached for every later open - a warm one is 230 ms.
Warming devstate at connect is the fix, and it is now first in the plan.
The harness needed two things to measure any of this, both folded back into
the skill. An open is not a repaint - it paints the menu closing, falls
silent while it loads, then paints the new screen - so quiescence stopped
the clock on the first burst and reported the walk opening in 124 ms.
Widening the silence window does not help either: past a second the 2 s
header tick lands inside every measurement. `expect TEXT` watches
/dev/vcs1 until the screen is actually there. Its needle has to be unique
to the destination, because every main-menu row carries its own screen's
name - `expect Mesh walk` matched the menu and made a 2.3x win read as no
change at all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>