Live query re-query cost: one write, one walk of the mesh

The one sentence

A live query in StorageAdapterMeshQueryProvider re-reads its whole scope on every change under that scope, and the security fold keeps three such queries open for as long as the mesh runs. So every write, of anything, re-walked the partition and then the whole mesh. The cost of one write grew with the size of the mesh.

What it looked like

The CD job Plugins: bake + seal failed in 8 of 9 runs after 2026-09-29 09:41Z, with two signatures:

Both come from the same place. The gate installs about 75 packages into one in-memory mesh, one after another. Here is the per-node write time of the installer, taken from the job logs (for each package, ── X: installing N file(s) to Installed node-repo plugin X: N written):

package (install position) run 36543299719 (green) run 36642931001 (red)
Store (1st) 0.003 s/node 0.003 s/node
Edu (23rd) 0.10 0.12
Governance (~57th) 0.67 0.48
Hosting 0.23 (32nd, 397 nodes, 91 s) 1.11 (last, 414 nodes, 459 s)

Plugins #2554 added Essentials@^1.0.0 to the requires of Hosting and Feedback. That moved both packages to the end of the install order. The trigger was that reorder. The cause was that a write at the end of the install costs about 300 times what the first write costs. So Hosting, which is the largest package, was installed at the most expensive point, and the Fleet Console journey ran its OperationalSpaceProvisioning.Ensure (two queries and a few writes) straight after, at that same point.

The repro and the profile

A monolith test mesh that writes Markdown nodes one after another, 150 per batch, took 3.2 ms per write at 150 nodes, 22 ms at 1,200 and 88 ms at 4,500. The growth was linear. A dotnet-trace (dotnet-sampled-thread-time) of the late batches put 92 % of busy time under the provider's scope walk: a ToObservableRecursive<(string, QueryScope)> recursion into InMemoryStorageAdapter.ListChildPaths, reached from the live pipeline's CoalesceWhileRunning(RunQuery). A tally of the re-queries during 450 writes named exactly three queries, with 451 re-runs each:

path:TestData scope:descendants nodeType:AccessAssignment …          (SecurityQueries.PartitionAssignments)
path:TestData scope:descendants id:_Policy nodeType:PartitionAccessPolicy …   (SecurityQueries.PartitionPolicies)
nodeType:GroupMembership partitions:all scope:subtree …              (SecurityQueries.Memberships)

None of these three can ever return a Markdown node. They were re-run anyway, because the live pipeline's relevance test was PathMatcher.ShouldNotify, and that test looks at the path alone.

The fix

A change is irrelevant to a live RAW query when the query confines its rows to a fixed set of node types and neither the type the path now holds nor the type it held before is in that set. That is NodeTypeChangeRelevance, applied in ObserveQueryInternal together with the scope test.

After the fix the same repro holds ~1 ms per write, flat, from 150 nodes to 1,200.

Regression test

LiveQueryIgnoresOtherNodeTypesTest (MeshWeaver.Hosting.Test) counts walks rather than timing them. It uses a pass-through adapter over a real InMemoryStorageAdapter and counts the listings of the query's base path:

The secured surface: System catalogs re-walked the mesh on every write

The fix above covered the raw surface only. The secured surface kept re-querying on every change under its scope, and the platform keeps several secured live queries open for the life of the mesh whose scope is the whole mesh: the UiContribution menu catalog (UiContributionCatalog), the NodeType catalogs and the Store/Plugin package index. Each is a partitions:all query confined to one node type, and each is read as System. So every write, of anything, still re-walked the mesh three times.

What it looked like

MeshWeaver.Reinsurance's required check test-repos / Compile + render node repos (MeshWeaver from ACR) runs its gate in one shard: 56 package installs into one in-memory mesh (40 upstream packages from the sealed Plugins publication, then the repository's own 17). It was cut by the 45-minute job cap while installing ReinsurancePractice (main schedule run 37411388338, ci.10047), and the one green main run (37414123629) took 41 minutes. The per-node write time of the installer in the cut run (── X: installing N file(s) to Installed node-repo plugin X: N written):

package (install position) nodes already installed s/node
Store (1st) 0 0.006
Edu (15th) 499 0.025
HomeAssistant (26th) 850 0.115
Ifrs17 (30th) 1,558 0.37
Reinsurance (42nd) 2,319 0.63
ReinsuranceDemo (56th) 3,346 1.02 (300 nodes, 305 s)

Nothing was compiling, baking or waiting: the time went into writes, and the cost of one write grew with the mesh.

The measurement

The gate was run locally (mw-plugin-test from this repository, the AI module built from MeshWeaver.Plugins, the packages Store, Edu, Essentials, Training and Hosting plus all of MeshWeaver.Reinsurance) with every live re-query tallied by query, viewer and duration:

 4156 re-runs   113 s   secured  viewer=system-security  nodeType:Store/Plugin is:main partitions:all
 3804 re-runs   112 s   secured  viewer=system-security  nodeType:UiContribution partitions:all
 2596 re-runs    93 s   secured  viewer=system-security  nodeType:NodeType partitions:all

About 2,950 node writes paid for about 10,500 re-walks of the whole mesh.

The fix

A secured read answered as System, in a mesh whose every Read validator admits System unconditionally, IS the raw read, so the node-type test applies to it unchanged. StorageAdapterMeshQueryProvider.SecuredReadIsTheRawRead decides this when the live query subscribes:

The same local run after the fix: about 280 re-queries in all (the NodeType catalog still re-reads when a NodeType is written, which is a relevant change). Per-node write cost stayed between 1 and 5 ms from the first package to the last, where it had grown from 3 ms to 136 ms. The whole run took 2 min 36 s instead of 6 min 23 s, and every package and type verdict was identical.

Regression test

LiveQueryIgnoresOtherNodeTypesTest (MeshWeaver.Hosting.Test) gained two cases:

What this does not claim