The sync/ Hub Population
A 26 h production replica held 9 398 MessageHub instances, of which 6 925 were sync/ hubs
at RunLevel=Started β about 2.7 GB of a 3.5 GB live heap (#3432). This page is what the code says
about where they come from and what is supposed to take them away. It is a narrowing, not a
diagnosis: the sections marked π¨ NOT ESTABLISHED are still open.
π¨ The cause was established on 2026-09-10, by experiment on a live replica: the READ path minted one
sync/ hub per GetDataRequest. See The Read Path Minted a Hub Per Read,
which also carries the live census that decomposes the population into its holders β and re-scopes the
evicted-stream mechanism to 1 parked stream in 475.
1. A sync/ hub is not a population β it is one field of a stream
SynchronizationStream<T>'s constructor mints exactly one, unconditionally, for every stream:
// src/MeshWeaver.Data/Serialization/SynchronizationStream.cs
var syncHub = Host.RunLevel > MessageHubRunLevel.Started
? null
: Host.GetHostedHub(
SynchronizationAddress.Create(ClientId), ConfigureSynchronizationHub, HostedHubCreation.Always);
β¦
Hub = syncHub;
syncHub.RegisterForDisposal(_ => ReleaseHub());
And one cross-hub subscription builds two streams, hence two hubs:
| side | who builds it | where |
|---|---|---|
| client | JsonSynchronizationStream.CreateExternalClient, Host = the subscribing hub |
JsonSynchronizationStream.cs |
| owner | Workspace.SubscribeToClient β CreateSynchronizationStream β ReduceStream(β¦ WithClientId(request.StreamId)), Host = the owner's hub |
Workspace.cs Β· WorkspaceStreams.cs |
Both carry the same ClientId, so the two hubs share an address string while living under
different parents.
This settles the first of #3432's open questions. The dump's own arithmetic agrees:
6 925 Started sync/ + 1 495 Dead sync/ = 8 420
SynchronizationStream total = 8 461 (0.5 % apart)
So there is no separate "sync hub leak" to explain. The question is entirely "why are there 8 461
live SynchronizationStreams", and each one costs its own Autofac ILifetimeScope and
TypeRegistry (~390 KB, of which ~173 KB is framework metadata duplicated per hub).
2. What retires one β three paths, and only one of them applies here
π¨ This table is about the STREAM's lifetime. The registry-level account β which collection roots a
sync/ hub, what takes it out, and which (RunLevel, IsDisposing) states can persist there β is
Β§8 below, written against the code after the retire-and-replace change, and it is the half that
says which of these hubs cannot be removed versus is removed only when X happens.
| path | where | which population it reaches |
|---|---|---|
| the stream is disposed | SynchronizationStream.Dispose() disposes its own Hub |
the 11 |
the parent hub is torn down (circuit ends, DisposeRequest, recycle) |
HostedHubsCollection β the RegisterForDisposal(_ => ReleaseHub()) hook (#3427) |
the 1 485 measured Dead |
| the cache releases the path | MeshNodeStreamCache idle sweep β TryReleaseUnwatched β DetachUpstreams β TearDownEntry β stream.Dispose() |
the Started population β and nothing else does |
The middle row is #3321 step 3, and it is why the Dead bucket exists at all. It does not touch a
hub whose parent is alive. So for a Started sync/ hub under a live parent β which is this
issue's entire 6 925 β the idle sweep is the only reaper in the process.
MeshNodeStreamHandle.DetachUpstreams() is reachable from nowhere else, and it is also the only
thing that collects the change-feed-parked predecessors described below.
3. The sweep's predicate, and why traffic defeats it
// src/MeshWeaver.Hosting/MeshNodeStreamCache.cs β Entry
public bool TryMarkIdleEvicted(TimeSpan idleWindow)
{
lock (gate)
{
if (evicted || subscribers > 0
|| Environment.TickCount64 - lastActiveAt < (long)idleWindow.TotalMilliseconds)
return false;
evicted = true;
return true;
}
}
Window 10 minutes, swept every minute (MeshNodeStreamCacheOptions). Two conjuncts, and the option's
own summary states what refreshes the second: "Every GetStream subscription, unsubscription and
Update on the path refreshes the window."
π¨ The subscribers count is NOT the Rx-subscriber trap Workspace warns about. That warning
("the reduce chain subscribes to the stream ITSELF, so an evicted stream measures 2β3 subscribers and
never reaches zero β implemented, measured and reverted") is about a different, abandoned mechanism
at the workspace layer. Entry.subscribers is a purpose-built refcount moved only by
TryAddSubscriber/RemoveSubscriber from the cache's own SharedView, and the internal hydration
subscription bypasses it. It genuinely can reach zero. Anyone reasoning about #3432 from the
Workspace comment alone will mis-attribute this.
What actually holds the sweep off is the other conjunct.
4. The change-feed orphan β the write both creates it and postpones its collection
π¨ Since #1174 this runs only for change
events no mirror can be held to (a delete, a recreate, a version-less recycle broadcast); a versioned
Updated keeps the stream β see Live Mirrors and the Change Feed.
The measurement below predates that.
Workspace.EvictForPath runs on every change-feed event for an owner path and removes every
identity's cached stream for that owner:
_evictedRemoteStreams[removed.Value] = 0;
ReclaimIfUnheld(removed.Value); // disposes ONLY if no declared lease remains
The mesh-node cache's hydration is a declared holder for the entry's whole life β that is
deliberate, and MeshNodeStreamHandle.Subscribe says so: "The lease lives exactly as long as this
subscription. The shared mesh-node cache's hydration comes through here, so its entry IS the declared
holder of the path's upstream for the entry's whole life β which is what makes every OTHER
(write-scoped) stream for the same path reclaimable on eviction."
So on the first write to a cached path:
- the entry's stream S1 is removed from
_remoteStreamCacheand parked; ReclaimIfUnheld(S1)finds the hydration's lease and does not dispose it;- nothing re-points the hydration β the sweep "only ever CLOSES idle entries; it never re-subscribes anything (the 2026-06-08 rule)" β so the entry keeps reading S1 forever;
- the next reader or writer misses the cache and builds S2, which becomes the cached one.
From then on the path holds two live streams β four sync/ hubs, ~1.5 MB β where the design
intends one. Write-scoped streams still churn correctly (an unleased evicted stream is reclaimed at
once), so the accumulation is a floor of one orphaned pair per cached, written path, not growth
per write.
π¨ And the same act does both halves: the Update that fires the change-feed event which orphans
S1 is also an Update that Touch()es the entry, pushing the 10-minute window out. A path written
at least once every ten minutes can never be released, so its orphan can never be collected. The
only reaper is switched off by the traffic that feeds it.
That is the answer to "is the retirement path missing, or present-but-not-firing?" β present, and structurally unable to fire on exactly the paths that generate the garbage.
5. Bounding
Neither cache has memory-pressure eviction or any other TTL:
Workspace._remoteStreamCacheβ a plainConcurrentDictionarykeyed by(Owner, Reference, Identity), so a path read under N identities holds N streams.MeshNodeStreamCache._streamsβ a plainConcurrentDictionarykeyed by path. (TheMemoryCachein the same file is the write-queue's, not this one's.)
So the live-stream count is bounded only by (distinct paths Γ identities) touched since process start, with the per-path floor above added on top.
6. π¨ What is NOT established
π¨ Partly superseded. The Evicted-Stream Retention closes the
"what holds them" half for one named mechanism, with a controlled two-arm test rather than a dump: a
change-feed eviction parks a remote stream, and ReclaimIfUnheld returns without disposing whenever
the stream carries no lease entry at all β so every unleased call site (including
LayoutExtensions.GetControlStream, i.e. every rendered layout area) retains one stream, and two
sync/ hubs, per change event. It also rules out static state and timers as roots, and replaces the
referrer walk below with two field reads on the singleton Workspace. What stays open is the
magnitude, not the mechanism.
- Whether the 8 461 are garbage. A portal with thousands of open Blazor circuits, each holding live layout-area streams, is supposed to look like this. Sections 1β5 explain the shape of the population, not that it is waste.
- The split between the per-path floor of Β§4 and legitimately-live streams. That is the number that decides whether #3432 is a leak with a fix or the per-hub DI cost the issue's own third outcome row describes.
- Whether #3427 moved this number. Still not re-measured; no deployment carried
f41f8bdaat the time of the last attempt.
7. The discriminator β a ratio, from data the existing plan already collects
#3432's plan calls for a ClrMD referrer walk, which is the expensive half and needs a dump that
(measured) restarts the replica. Β§1 makes a much cheaper reading decisive, and it comes off
dumpheap -stat, which that plan already takes:
count
MeshNodeStreamCache+EntryalongsideSynchronizationStream.
| reading | what it means | what to do |
|---|---|---|
| streams β 2 Γ entries | the Β§4 floor dominates: one change-feed orphan plus one live stream per cached path | a fix with a named code path β release the entry's lease on eviction (or re-point the hydration) so ReclaimIfUnheld collects S1. Halves the population without touching anyone's live view |
| streams β 1 Γ entries | no orphaning; the population is one stream per cached path | the leak framing is falsified β this is per-hub DI cost (#3432's third outcome row); close it and open the metadata-sharing issue |
| streams β« 2 Γ entries | something outside the mesh-node cache mints them | look at the identity dimension of _remoteStreamCache's key (Β§5) before anything else |
No referrer walk, no field traversal, no second dump β one extra line of a histogram the plan already produces. π¨ It does not replace the referrer walk for the third row; it tells you whether you need one.
8. The registry account: what roots a sync/ hub, and what takes it out
Sections 1β7 are about the STREAM. This section is about the HUB, read off
HostedHubsCollection and MessageHub as they stand today. It answers the question the dump's
histogram poses β what can a Started sync/ hub be? β by construction rather than by inference,
and it distinguishes cannot be removed from is removed only when X happens.
8.1 One root, and it is the parent's registry
A sync/{clientId} hub is a HOSTED hub. There is exactly one creation site (Β§1), and every hosted
hub enters its parent's registry through one method:
// src/MeshWeaver.Messaging.Hub/HostedHubsCollection.cs β Track
private void Track(IMessageHub hub)
{
messageHubs[hub.Address] = hub;
hub.RegisterForDisposal(h =>
messageHubs.TryRemove(new KeyValuePair<Address, IMessageHub>(h.Address, h)));
}
messageHubs is an instance ConcurrentDictionary<Address, IMessageHub> on the collection, whose
lifetime is the parent hub's. There is a second root beside it, retiring, a
reference-keyed dictionary of hubs taken out from under their address but still tearing down; Hubs
enumerates both, which is why a census walking Hubs sees a retired hub too.
8.2 Three removals, all of them self-driven
| # | what removes the entry | when it runs |
|---|---|---|
| 1 | Track's own registrant |
in the hub's ShutDown phase β DisposeImpl is reached from nowhere else, and it is what walks the composite the registrant sits in |
| 2 | RetireCorpse |
only from a lookup for that exact address, only with HostedHubCreation.Always, only at RunLevel >= ShutDown, only while the collection itself is not disposing. Moves the hub to retiring |
| 3 | the retiring entry's own subscription |
on that hub's DisposalCompleted, i.e. at Dead |
Nothing else removes a hosted hub from its parent's registry. There is no sweep, no idle eviction,
no cap, and nothing anywhere asks "is this hub still needed". Removal 1 is the hub disposing
itself; removals 2 and 3 only move a hub that is already disposing. So the registry is a pure
consequence of who calls Dispose(), and for a sync/ hub that is: the stream's own
Dispose(), the parent's teardown, or a routed DisposeRequest β Β§2's table.
π¨ Removal 2 is lookup-triggered, not self-triggered. Retire-and-replace made the hand-out safe;
it did not make the registry self-cleaning. A hub wedged at ShutDown that no Always lookup ever
asks for again stays in messageHubs for the parent's life. That is #4883's wedge showing through,
deliberately left there.
8.3 The (RunLevel, IsDisposing) cross-tab β which cells can persist
IsDisposing is disposalStarted, set as the FIRST statement of Dispose(), before it posts
ShutdownRequest(Quiescing). The RunLevel only moves when the action block dequeues that request.
| cell | what it is | can it persist? |
|---|---|---|
Started, false |
nobody has asked this hub to die | Yes, indefinitely β by construction, not by defect. Removal 1 is the only route out and nothing has started it. Whether the hub is garbage is entirely a question about the stream that owns it |
Started, true |
Dispose() ran; the turn was never dequeued |
Yes, unboundedly. Invisible to a RunLevel-only histogram β it reads identically to the row above |
Quiescing, true |
draining accepted work, off the block | Bounded by the quiesce budget only if the block can dequeue the next phase |
DisposeHostedHubs, true |
joined on the hosted subtree | Yes, unboundedly β the join carries no deadline by design, so one child that never completes holds it forever |
HostedHubsDisposed, true |
β | No: empty by construction. The level is declared and never assigned; the phase sequence goes DisposeHostedHubs β ShutDown |
ShutDown, true |
past the flip, not yet past the registrant walk | Microseconds normally; unbounded if the ShutDown turn wedges, and then only a lookup takes it out (Β§8.2) |
Dead, true |
terminal | Should not appear in messageHubs at all: Dead is assigned strictly after DisposeImpl has run removal 1. Present β the removal did not run |
anything, in retiring |
retired predecessor | Until DisposalCompleted β so a wedged teardown keeps it, visible in Hubs and in the owner's disposal join |
8.4 A second retention class the histogram cannot see: the registrant composite
A hub's registered cleanups live in one CompositeDisposable:
// src/MeshWeaver.Messaging.Hub/MessageHub.cs
private readonly CompositeDisposable disposables = new();
public IMessageHub RegisterForDisposal(IDisposable disposable)
{
disposables.Add(GuardRegistrant(disposable));
return this;
}
A plain registration is append-only. CompositeDisposable.Add never prunes, and nothing removes
an entry added through RegisterForDisposal, so such a registrant is held β with everything its
closure captured β for that hub's whole life. The ONE way out before the hub dies is a DETACHABLE
registration (RegisterForDisposalDetachable, or SubscribeHeldUntilTerminal built on it): its handle
removes the entry without disposing it, and it is to be used only once the registrant's disposal can
no longer do anything (see the table below). A PLAIN registrant whose own subject is SHORTER-lived than the hub is therefore a monotone
root, and it retains an object graph rather than a hub, which is why no MessageHub histogram can
see it.
MessageHub.DisposalRegistrantCount is the reading that can, and it is printed in both disposal
diagnostics as Registrants=. A hub whose registrant count climbs with the traffic it has served
is retaining one object graph per unit of that traffic.
Two classes, both measured in MeshWeaver.Data.Test:
| site | growth | status |
|---|---|---|
JsonSynchronizationStream.CreateExternalClient registered the owner-protocol subscription on the SUBSCRIBING hub as well as on the stream |
+1 per remote stream ever opened, holding the whole SynchronizationStream graph (measured: floor 4 β 6 over two streams, still 6 after both were disposed) |
Fixed. The duplicate bought nothing, and by TWO routes rather than one: the stream's own composite rides its sync/ sub-hub (a hosted hub of the subscribing hub, torn down in its DisposeHostedHubs phase), AND Workspace.Dispose disposes every cached remote stream β which its own comment says exists to release exactly this SubscribeRequest callback. A hub walks its OWN registrants only later, in ShutDown, so the duplicate was the third route and the last to fire. π¨ The second route is a correction owed to running the negative control: with the sub-hub hook removed the coverage test still passes, so it asserts the OUTCOME and not a route. Pinned by StreamRegistrantsLeaveTheSubscribingHubTest |
the per-request handlers in DataExtensions and MeshDataSource register one cleanup per request on the OWNING hub β HandleGetDataRequest, HandleSubscribeRequest, HandleDataChangeRequest, ApplyJsonMergePatchAndUpdate, ApplyMeshNodePatchInTurn, RegisterOwnerDisposingNack, HandleSaveMeshNode, the unified-reference handlers |
+1 or +2 per request served, measured over ten answered requests each from a floor of 3: DataChangeRequest β 23, PatchDataRequest (generic path) β 23, one-shot GetDataRequest β 13, UpdateUnifiedReferenceRequest β 13 |
Fixed. A registrant now leaves the hub at the moment its own disposal can no longer do anything, which changes no behaviour: a subscription at its TERMINAL (hub.SubscribeHeldUntilTerminal(source, subscribe)), a teardown NACK once its once-only answer gate is CLAIMED (IMessageHub.RegisterForDisposalDetachable β the handle removes the registrant WITHOUT disposing it, one shared flag deciding against a racing teardown). A request still in flight when the hub goes down is torn down and NACKed exactly as before. Pinned by PerRequestRegistrantsLeaveTheOwnerHubTest (red 4 of 4 on the unfixed build with the readings above; green on the fix) and PatchAckTotalityTest's flush-leg assertion |
π¨ Still held by design, and NOT a leak this fix touches: a GetDataRequest over a LIVE workspace
reference keeps shipping every change to its requester, so its subscription β and its registrant β
live until the owner goes down. Whether anything should end such a read earlier (the requester's own
Observe completes on the first answer) is a separate question, not measured here. A read that ends
EMPTY on a healthy hub also stays registered on purpose: its caller is still owed the teardown NACK.
9. The 2026-10-11 census: every sync/ hub is the fixed complement of a live owner
Three in-process, read-only censuses on images carrying every fix this page names (core
e438ab89e7, image 3.1.10517). They walk the live hosted-hub tree by field reads and resolve
nothing from DI. This is the reading #3432 was closed on, and it is the issue's own third outcome
row: the sync/ population is per-hub cost, not retention of sync/ hubs. No sync/ hub was
found outliving or lacking its owner. That moves the lifetime question to the owners and does not
answer it: whether the node hubs themselves retire once idle is NOT established here (end of this
section), so this reading does not say the owners are all needed.
| replica | uptime | hubs | sync/ |
node hubs | last-GC heap |
|---|---|---|---|---|---|
instance A, pod β¦5c74fb9855-d9wfp |
26.7 min | 276 | 228 | 48 | 974 MiB (GC.GetTotalMemory) |
instance B, pod β¦688f5555d6-pzvwj |
30.9 min | 670 | 569 | 101 | 1 303 MiB |
instance B, pod β¦688f5555d6-8f2g5 (the FIRST replica of its generation to boot) |
32.9 min | 4 795 | 3 736 | 1 059 | 5 563 MiB, of which 1 779 MiB fragmented |
Attribution, instance A (the v7.2 census, identity exact β a stream counts only when its
ClientId equals the hub's Address.Id): sync=228 attributed=228, every other bucket zero. All
228 read Started / <not started> except one Starting. Each is one of the owner hub's own
data-source reduces (MeshNodeReference, CollectionReference, CollectionsReference on
host=X, owner=ds/X) or one mesh-node cache hydration (host=cache/β¦, 68 of them). The duplicate
discriminator reads streams=228 distinctPairs=197 excess=31, and every duplicate group reads
refsEqual=YES hash=SAME, the configured (deliberately uncached) branch. No group reads
refsEqual=NO: the reference that could not be a cache key (ContentCollectionReference, 24 of the
80 excess on 2026-09-16) is gone.
Shape, instance B, the busy replica β sync/ hubs per parent:
sync/ children |
parents | hubs |
|---|---|---|
| 1 | 1 | 1 |
| 3 | 696 | 2 088 |
| 4 | 284 | 1 136 |
| 5β12 | 65 | 354 |
| 157 | 1 (cache/β¦, one per cached path) |
157 |
980 of 1 047 parents hold exactly three or four. A hub that accumulated streams per read, per write
or per subscriber would stand out of that histogram, and the only parent above twelve is the cache
hub, whose count is its entry count. So the number of sync/ hubs is bounded per live node hub β
three or four for 980 of the 1 046 non-cache parents, at most twelve for any β plus the cache
entries. The 66 parents outside three-or-four (one with a single child, 65 with five to twelve,
354 hubs) were NOT attributed stream by stream on this replica; the per-stream attribution was run
on instance A, where the whole excess over the base set (31 of 228) was the configured, deliberately
uncached branch. That is the likely reading of the 65 here and it is an inference, not a
measurement.
π¨ The working hypothesis is that the heap follows the number of live node hubs, and that on the
first replica of a generation the boot sets that number. It is a correlation across two replicas,
not a root attribution. Of the 1 059 node hubs on 8f2g5 thirty minutes after it started, 930 fall
in buckets that are not request traffic: 403 are two-segment addresses (NodeType definitions, the
bake), 128 are partition roots, 115 are β¦/_Policy, 115 β¦/_Entitlements, 43 β¦/_GitSync, 82
Admin/Build/β¦, 44 MeshWeaver/samples/β¦. The other 129 were not classified. Its sibling, booted
ninety seconds later and serving the same instance, holds 101 node hubs. The two readings differ
by 958 node hubs and 4 260 MiB of last-GC heap, about 4.4 MiB per node hub (its own scope, type
registry, workspace and its three or four sync/ hubs would be in that figure). That fan-out is the candidate for the multi-GiB
[HEAPSTEP] series every roll prints in its first minutes: the sampled allocation of those steps
is System.String, System.Byte[], Entry[System.String][] and Autofac's
IResolvePipelineBuilder handlers and MiddlewareDeclaration, which is what hub construction
allocates. [HEAPSTEP] names allocated types, not retained roots, and this section has neither a
quiet-process root attribution nor a before/after on one process, so the per-hub figure and the
link to the heap step are to be confirmed, not cited.
π¨ NOT established: whether those boot-activated hubs retire once idle. The replica was replaced
by the next roll 20 minutes after the reading (the instance rolled twice inside one hour), so no
second reading on the same process exists, and at 33 minutes an idle activation is only just past
the keep-alive window plus the collection age. The 2026-09-25 reading on a 30 h process (post-GC
floor level at about 3.3 GiB from hour 18) is the newest evidence that the population plateaus. A
replica that lives a day is the instrument; HubCensusByOwner (a Code node in the operator's own
partition, the script behind the tables above) is the reading to repeat on it.
The standing census names the satellite shape (#6632)
meshweaver.hubs.live (PlatformMetrics) is tagged kind (the address type), satellite and
runlevel. satellite is the LAST address segment that starts with _ β _DocumentPart,
_Activity, _Thread β or none; never an id. The 2026-10-04/05 OOM burst was ~5,000
Admin/content/_Documents/β¦/_DocumentPart/{index} hubs, which the kind tag alone folded into
kind=Admin; with the second tag the next burst names its address shape while it happens. The bound
is the meter's own, and it holds for the meter's LIFETIME: at most MaxSatelliteValues (32) satellite
values are ever named. A value is admitted the first time it is among the most populous of a scrape
while a slot is free and keeps its name afterwards; every value never admitted folds into _other.
A per-scrape top-N would bound one scrape only β values rotating through the top would each leave a
series behind in the metric store. Nothing enforces that a _ segment is a declared satellite, so
cardinality is not left to the deployment
(PlatformMetricsTest.SatelliteValues_StayBounded_AcrossScrapesThatRotateTheirValues).
10. Which boot step builds each class of node hub (#6623)
The rule: a boot step that only needs a node's CONTENT reads it without activating that node's
per-node hub. GetMeshNodeStream(path) is the authoritative LIVE read, and it gets there by
subscribing at the node's own address, which builds the hub. For a one-shot read of many nodes the
store answers with the DURABLE snapshot: exactly what the next activation would load.
StoredNodeReads.ReadStoredNode / ReadStoredNodes (MeshWeaver.Mesh.Contract) is that read:
storage first, the served static node second. It is the same order ReadNodeAuthoritative has
always used for the partition bootstrap, and the access-rule gate's ReadSubjectNode now delegates
to it.
π¨ The durable snapshot is not the live record. Where an owner is live, the row trails it by
one 200 ms persistence-sampler window plus the write, and with no bound after a store write that
FAILED: MeshDataSource.WriteSampledNode logs a faulted sampled save and does not retry it, and
PendingOwnSave counts the accepted request as saved, so the owner stays ahead of its row until its
next change is written (and a recycle loses the acknowledged state β an owner-side durability
defect, separate from this read). So a caller may use this read only where its decision is safe on
a trailing row, and says why. The two GitSync sweeps are: BundleKeyedHold.Decide holds only where
it REPRODUCES the definition's fingerprint from the current Code nodes, so a trailing fingerprint
abstains, and a row trailing only in provenance either misses a hold (the documented StaleAdopted
direction) or holds a type's sources in place (nothing written or deleted) until the row
catches up or the shelf releases the hold; SyncedPartitionDrift.Measure either
misses drift (abstain) or reports drift that is gone, whose action re-imports at the commit the
source already claims and moves nothing backwards (NodeTypeDefinitionReads carries the full
argument).
The step β call β class mapping from reading the code. The census counts address shapes only, so each attribution below is an inference from code, not a per-address measurement:
| class (census bucket) | boot step | the call that builds the hub | needs the hub? | status |
|---|---|---|---|---|
two-segment {Space}/{Type} (403) |
GitSync import of every synced Space: BundleKeyedHoldReading.Judge; the seal-arrival pass: SealedPublicationSyncReconciler.LiveDefinitions |
GetMeshNodeStream(typePath).Take(1) for every NodeType the listing names |
no, content only | rerouted to NodeTypeDefinitionReads.ReadDefinitions (one batched store read) |
| two-segment (part) | any live instance hub's enrichment: NodeTypeEnrichmentHelpers watches GetMeshNodeStream(nodeType) |
a live subscription | yes, it watches for a new build (stale-build recycle) | left; bounded by the number of live instance hubs |
| two-segment (part) | the bake sweep DynamicTypePreWarmer.WarmDynamicTypes |
none when every type is baked (AlreadyBaked is never activated; the enumeration is a query) |
β | already non-activating |
| partition roots (128) | PackageInstaller.WarmInstalledRoots |
GetMeshNodeStream(root), deliberately |
yes, the activation runs the gating pass that writes the cover grants | left |
| partition roots | StaticRepoImporter.Run: EnsureRoot / ReadClaimedRoots |
GetMeshNodeStream(partition) |
the same run then upserts the root, which activates it anyway | left (no gain) |
_Activity (66) |
StaticRepoImporter: lock / attempt / manifest writes; ReadNodeManifest / ReadContentManifest |
CreateOrUpdateNodeRequest, GetMeshNode |
the writes do; the manifest reads follow the previous import's own write and need the owner's unflushed state | left |
_GitSync (43) |
GitHubSyncService stamping LastSync⦠on the config |
GetMeshNodeStream(config).Update |
yes, it writes | left |
Admin/Build/β¦ (82) |
BuildProtocolDriver: chunk claims, chunk _Activity create/finish |
UpdateBuildAsHolder, CreateNode, FinishActivity |
yes, they write | left |
_Policy (115), _Entitlements (115) |
not attributed in core: the installer's policy reads already go through IStorageAdapter.Read; _Entitlements is written by the Store plugin |
β | β | open, to attribute in MeshWeaver.Plugins |
Pinned by NodeTypeSweepActivatesNoHubTest (MeshWeaver.Documentation.Test). It creates twelve
NodeTypes, runs the sweep's read and watches HostedHubsCollection.HubAdded: no hub is built.
The negative control runs the replaced GetMeshNodeStream read over the same types with the same
instrument and sees twelve, one per type.
Not established: that the 403 two-segment hubs on 8f2g5 were built by these two sweeps rather
than by enrichment watches or by plugin-side code. A per-address census on the first replica of the
next generation answers that. The same goes for whether boot-activated hubs retire once idle (Β§9).
Related
- Stream Liveness and the Hub Reference β #3321 step 3, the
parent-teardown hook that reclaimed the
Deadhalf - MeshNode Stream Cache β the entry, the refcount and the idle sweep
- Portal Heap Is Hubs β the five dumps, the measurement hazard, and the ClrMD recipe
- The Evicted-Stream Retention β the named retainer, the controlled experiment that established it, and the two-field-read measurement that closes #3432