Skip to content

MILAB-6303: tree-sync introspection metrics + stop-marker follow-up fix#1760

Draft
xnacly wants to merge 6 commits into
mainfrom
MILAB-6303_tree-sync-metrics
Draft

MILAB-6303: tree-sync introspection metrics + stop-marker follow-up fix#1760
xnacly wants to merge 6 commits into
mainfrom
MILAB-6303_tree-sync-metrics

Conversation

@xnacly

@xnacly xnacly commented Jul 21, 2026

Copy link
Copy Markdown
Member

Tree-sync instrumentation and a stop-marker follow-up fix, backing the MILAB-6303 investigation (Desktop App on slow / distant networks). Spec: milaboratory/text#190.

What this adds

Full introspection of the tree-sync load via MI_LOG_TREE_STAT, computed by reusing the state machine's existing changed flag (negligible per-resource cost):

  • Cross-cycle duplication: new / changed / unchanged resources, wasted downlink bytes, stable-metadata re-streams, BFS fetches spent on unchanged resources.
  • Per-path detail: streaming (rounds, resource frames, stop-marker frames, stop-markers that triggered a follow-up, traverse-stopped) and BFS (resources actually requested, not-found).
  • What changed each cycle: fields added / removed / value-changed, kv changes, ready flips, lock transitions, errors attached, duplicates resolved, resources marked final.

This measured that 52-76% of every polling cycle re-fetches unchanged resources (trees changed by only 2-9 resources over 3 min) on a heavy project.

Fix

The resourceTree stop-marker follow-up now propagates traverseStopRules, so the retry stops at the same boundaries instead of traversing the stop-marked subtrees unbounded. It also asserts that one follow-up round resolves every stop marker (throws if a deeper stop-marker chain remains, which would mean the retry must become a loop).

Status

Draft. Type-check clean; pl-tree unit tests pass (the one skipped failure needs a live PL_ADDRESS). No end-to-end validation of the stop-rule change under a deep tree yet; the assertion is the tripwire for that.

xnacly added 2 commits July 20, 2026 15:47
Extend the MI_LOG_TREE_STAT counters with per-cycle re-fetch
classification computed in updateFromResourceData: new, changed and
unchanged resources, wasted downlink bytes, BFS fetches spent on
unchanged resources, and changes that re-streamed stable metadata.
Quantifies how much of each poll re-fetches resources the client
already holds. Reuses the existing changed flag, no extra work per
resource.
Expand MI_LOG_TREE_STAT into full per-path introspection: streaming
detail (rounds, resource/stop-marker frames, follow-up seeds,
traverse-stopped), BFS detail (resources requested, not found), and a
per-cycle change breakdown (fields added/removed/changed, kv, ready
flips, locks, errors, duplicates resolved, resources marked final),
alongside the existing cross-cycle duplication counters.

Fix the resourceTree stop-marker follow-up to propagate traverseStopRules
so the retry stops at the same boundaries instead of traversing the
stop-marked subtrees unbounded. Assert that one follow-up round resolves
every stop marker (throws if a deeper stop-marker chain remains, which
would mean the retry must become a loop).
@changeset-bot

changeset-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f329cc3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@milaboratories/pl-tree Patch
@milaboratories/pl-drivers Patch
@milaboratories/pl-middle-layer Patch
@platforma-sdk/test Patch
@platforma-sdk/pl-cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@notion-workspace

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request instruments tree-sync statistics (MI_LOG_TREE_STAT) to provide detailed introspection on cross-cycle duplication, per-path details, and cycle changes, while also fixing stop-marker follow-up propagation. The review comments correctly identify a critical issue where adding, modifying, or deleting KV entries does not set the changed flag to true. This omission causes incorrect state versioning, inaccurate introspection metrics, and prevents proper final state evaluation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +759 to +766
if (stat) stat.kvChanged++;
} else if (Buffer.compare(current, kv.value) !== 0) {
resource.kv.set(kv.key, kv.value);
notEmpty(resource.kvChangedPerKey).markChanged(
kv.key,
`kv changed for ${resourceIdToString(resource.id)}: ${kv.key}`,
);
if (stat) stat.kvChanged++;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When KV entries are added or modified, the changed flag is not set to true. This leads to several issues:

  1. Incorrect State Versioning: resource.dataVersion is not updated to resource.version, meaning external observers or caching layers won't detect the KV changes.
  2. Incorrect Introspection Metrics: The resource is incorrectly counted under resourcesUnchanged instead of resourcesChanged in the newly added ResourceUpdateStat metrics, which also incorrectly inflates bytesUnchanged and bfsRequestsWasted.
  3. Final State Evaluation: Bumping changed ensures that isFinalPredicate is re-evaluated, which is important if the finality of a resource depends on its KV values.

We should set changed = true when KV entries are added or modified.

            changed = true;
            if (stat) stat.kvChanged++;
          } else if (Buffer.compare(current, kv.value) !== 0) {
            resource.kv.set(kv.key, kv.value);
            notEmpty(resource.kvChangedPerKey).markChanged(
              kv.key,
              `kv changed for ${resourceIdToString(resource.id)}: ${kv.key}`,
            );
            changed = true;
            if (stat) stat.kvChanged++;

key,
`kv deleted for ${resourceIdToString(resource!.id)}: ${key}`,
);
if (stat) stat.kvChanged++;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similarly to KV additions and modifications, when a KV entry is deleted, the changed flag should be set to true to ensure proper state versioning, final state evaluation, and accurate introspection metrics.

              changed = true;
              if (stat) stat.kvChanged++;

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
1422 4 1418 13
View the top 3 failed test(s) by shortest run time
src/synchronized_tree.test.ts > termination test
Stack Traces | 0.00268s run time
RpcError: No connection established. Last error: null
 ❯ Object.callback ../../../node_modules/.pnpm/@protobuf-ts+grpc-transport@2.11.1_@grpc+grpc-js@1.13.4/node_modules/@.../build/commonjs/grpc-transport.js:37:27
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client.ts:360:25
 ❯ ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/call-interface.ts:149:26
 ❯ Object.onReceiveStatus ...../src/core/ll_client.ts:503:15
 ❯ InterceptingListenerImpl.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/call-interface.ts:145:18
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client-interceptors.ts:458:33
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client-interceptors.ts:419:47
 ❯ ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/resolving-call.ts:169:23

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { code: 'UNAVAILABLE', meta: {}, methodName: 'Ping', serviceName: 'MiLaboratories.PL.API.Platform' }
src/synchronized_tree.test.ts > synchronized tree test with KV
Stack Traces | 0.00457s run time
RpcError: No connection established. Last error: null
 ❯ Object.callback ../../../node_modules/.pnpm/@protobuf-ts+grpc-transport@2.11.1_@grpc+grpc-js@1.13.4/node_modules/@.../build/commonjs/grpc-transport.js:37:27
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client.ts:360:25
 ❯ ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/call-interface.ts:149:26
 ❯ Object.onReceiveStatus ...../src/core/ll_client.ts:503:15
 ❯ InterceptingListenerImpl.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/call-interface.ts:145:18
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client-interceptors.ts:458:33
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client-interceptors.ts:419:47
 ❯ ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/resolving-call.ts:169:23

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { code: 'UNAVAILABLE', meta: {}, methodName: 'Ping', serviceName: 'MiLaboratories.PL.API.Platform' }
src/sync.test.ts > load resources
Stack Traces | 0.0329s run time
RpcError: No connection established. Last error: Error: connect ECONNREFUSED 127.0.0.1:6345
 ❯ Object.callback ../../../node_modules/.pnpm/@protobuf-ts+grpc-transport@2.11.1_@grpc+grpc-js@1.13.4/node_modules/@.../build/commonjs/grpc-transport.js:37:27
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client.ts:360:25
 ❯ ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/call-interface.ts:149:26
 ❯ Object.onReceiveStatus ...../src/core/ll_client.ts:503:15
 ❯ InterceptingListenerImpl.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/call-interface.ts:145:18
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client-interceptors.ts:458:33
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client-interceptors.ts:419:47
 ❯ ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/resolving-call.ts:169:23

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { code: 'UNAVAILABLE', meta: {}, methodName: 'Ping', serviceName: 'MiLaboratories.PL.API.Platform' }
src/synchronized_tree.test.ts > simple synchronized tree test
Stack Traces | 0.0372s run time
RpcError: No connection established. Last error: Error: connect ECONNREFUSED 127.0.0.1:6345
 ❯ Object.callback ../../../node_modules/.pnpm/@protobuf-ts+grpc-transport@2.11.1_@grpc+grpc-js@1.13.4/node_modules/@.../build/commonjs/grpc-transport.js:37:27
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client.ts:360:25
 ❯ ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/call-interface.ts:149:26
 ❯ Object.onReceiveStatus ...../src/core/ll_client.ts:503:15
 ❯ InterceptingListenerImpl.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/call-interface.ts:145:18
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client-interceptors.ts:458:33
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client-interceptors.ts:419:47
 ❯ ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/resolving-call.ts:169:23

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { code: 'UNAVAILABLE', meta: {}, methodName: 'Ping', serviceName: 'MiLaboratories.PL.API.Platform' }

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

xnacly added 4 commits July 22, 2026 10:16
KV mutations previously did not set the changed flag, so they never
bumped dataVersion, skipped isFinalPredicate re-evaluation, and were
miscounted as unchanged re-fetches in the tree-sync stats. Set changed
on KV add, modify, and delete so versioning, finality, and the
duplication metrics are correct. Addresses PR review.
Comment out the throw on unresolved stop-marker seeds after the follow-up
round while the deeper-chain case is under investigation. Keep the
traverseStopRules propagation and leave re-enable instructions inline.
…hains

Propagating traverseStopRules to the follow-up bounded the retry but left
stop-marker chains deeper than one level unresolved: a loaded resource
referenced a deeper stop-marked resource that was never fetched, so
updateFromResourceData threw "orphan resource" on open. Loop the
follow-up (each round with the same stop rules, deduping fetched ids)
until no stop-marker seeds remain, so everything referenced is loaded.
Replaces the disabled one-shot assertion.
Reapplying traverseStopRules on the follow-up re-flagged the very
resources being retried as stop markers (the rule is finality-based and
they are server-final), so their state never loaded and resources
referencing them threw "orphan resource" on open. Fetch follow-up seeds
plainly; keep the loop only to resolve any residual stop markers. Update
the changeset accordingly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant