Skip to content

Simplified summary process without summarizer nodes - #27953

Draft
Navin Agarwal (agarwal-navin) wants to merge 11 commits into
microsoft:mainfrom
agarwal-navin:feature/new-summary-api
Draft

Simplified summary process without summarizer nodes#27953
Navin Agarwal (agarwal-navin) wants to merge 11 commits into
microsoft:mainfrom
agarwal-navin:feature/new-summary-api

Conversation

@agarwal-navin

@agarwal-navin Navin Agarwal (agarwal-navin) commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

Design and rollout plan: summarizeV2.md, added in this PR. It covers the design in full, the staged rollout including the A/B cohort comparison, and the dashboard signals to watch. Start there if you want the reasoning behind the change; the summary below is the short version.

Summary state today is spread across a tree of SummarizerNodes. Every node keeps its own copy of "what did I
last summarize, and when", advanced through a multi-stage protocol (startSummarysummarize
validateSummarycompleteSummaryrefreshLatestSummary / clearSummary). That duplication is why nodes
can disagree with the runtime, why a failed or nacked summary has to be rolled back across the tree, and why
nodes get realized just to take part in the protocol.

This adds a second flow that keeps one piece of state on the container runtime -
latestSummarySequenceNumber - while each node tracks only when its own content last changed. Summarizing
becomes a single pass with one comparison per node and no per-node state machine, and a failed summary needs no
rollback because the decision input only advances on ack.

Nodes write into an ISummaryBuilder instead of returning a tree for the parent to merge. The builder derives
handle paths from the tree structure, so nodes no longer need to be told their own path - removing the
summaryPath leakage in IExperimentalIncrementalSummaryContext.

The new flow is off by default behind Fluid.ContainerRuntime.EnableSummarizeV2. The existing summarize
API is untouched, and a summary is produced entirely by one flow or the other - never a mix, since partial trees
built with different semantics would make the two impossible to compare.

Main pieces:

  • ISummaryBuilder / SummaryBuilder, and ISummarizable declaring generateSummary once (IChannel and
    IFluidDataStoreChannel pick it up via Partial<ISummarizable>, so it stays optional for external
    implementers).
  • ContainerRuntime.generateSummary, plus the gate in submitSummary which also skips the summarizer-node
    bookkeeping that no longer applies.
  • generateSummaryCore implemented across all 15 DDS packages, sharing one serialization routine with
    summarizeCore via ISummaryContentSink so the two cannot drift.
  • A layer-compatibility feature so a data store runtime that predates the API is summarized with summarize at
    the version boundary instead of failing.
  • Telemetry to compare the flows A/B across client cohorts: summarizeFlow on every summarizer event, plus
    realizedDataStoreCount, channelCount and reusedChannelCount on IGeneratedSummaryStats. See
    Dashboard signals.

On naming: generateSummary is named for what it does rather than being a suffixed variant of summarize,
so it does not need renaming when it replaces summarize at the end of the rollout - the old method is simply
removed. The generate verb matches the name the summarize pipeline already uses for this step in its telemetry.

Not ported yet: SharedTree's forest incremental summarization still depends on summaryPath, so the forest
is written in full under generateSummary. SharedTree's schema summarizer is ported, via an optional
Summarizable.canReuseSummary that lets an unchanged summarizable reuse its whole subtree. Both are called out
in the doc.

Reviewer Guidance

The review process is outlined on this wiki page.

Specific things I'd like opinions on:

  • generateSummary is an optional property, not a method, on SharedObject and FluidDataStoreRuntime. That
    keeps the addition purely additive (no type-test exceptions needed), but a property can't be overridden via
    super, so each delegates to an overridable generateSummaryCore. Is that trade the right one, or would you
    rather take the class-shape break and have a plain method?
  • The version boundary is decided by layer-compat details, not by probing for the method. A data store runtime
    that predates the API advertises no support and is summarized with summarize, so the two flows can coexist
    across a version boundary within one container. Worth a look at whether that check is in the right place.
  • The remaining end-to-end failures below are each understood, but several of them are tests asserting on
    summarizer-node internals that this flow removes. I'd like agreement on which should be deleted versus adapted
    before this leaves draft.

Validation

Suite Gate on Control (gate off)
Summarization e2e (local driver) 582 passing, 10 failing 592 passing, 0 failing
SharedTree unit 15061 passing unchanged
Runtime + DDS unit ~5,600 passing unchanged

Clean tsc --noEmit, lint, format and API reports across all touched packages.

The 10 remaining failures fall into four groups, all inventoried in
the doc:
version-boundary cases where a legacy data store or a DDS relying on IExperimentalIncrementalSummaryContext is
written in full; two tests asserting SummarizerNode refresh telemetry this flow never emits; two asserting a
NodeDidNotSummarize path that no longer exists; and two summary-handle-resolution tests. None is unexplained.

Adds ISummaryBuilder/SummaryBuilder and a separate summarize2 flow where the
container runtime owns the latest successful summary sequence number and child
nodes only track when they last changed. The existing summarize API is
unchanged; nodes that have not migrated fall back to it.
…coverage

- ISummarizable declares summarize2 once, with docs; IChannel and
  IFluidDataStoreChannel pick it up via Partial<ISummarizable>.
- SharedObject/FluidDataStoreRuntime expose summarize2 as optional properties, so
  the addition is purely additive and needs no type-test exceptions.
- Every DDS now implements summarizeCore2, sharing one serialization routine with
  summarizeCore via ISummaryContentSink.
- summarize2 is gated behind Fluid.ContainerRuntime.EnableSummarizeV2 and skips
  summarizer node bookkeeping when enabled.
- getMetadata takes the summary number instead of incrementing it.
- SummaryBuilder computes stats per node, so markUnreferenced is order-independent.
- Adds src/summary/summarizeV2.md describing the architecture and rollout.
The summarize2 flow fell back to the old summarize API for nodes that had not
implemented the new one, so a single summary could be built by both flows. Part
of the tree would then be produced with different semantics (no incremental
reuse, different realization and telemetry behavior), which makes comparing the
two flows meaningless.

Every participant must now implement the new API; if one does not, summarization
asserts instead of silently falling back. The addSummaryTreeToBuilder bridge is
removed as it existed only to copy old-flow output into the builder.

Also gives FluidDataStoreRuntime an overridable summarizeCore2 and implements it
in mixinSummaryHandler, which would otherwise have silently dropped its injected
summary content under summarize2.
The flows cannot be compared within a client, since only one runs per summary, so
they are compared across cohorts of clients instead. That needs every metric
tagged with the cohort and computed the same way for both flows.

- summarizeFlow is a persistent property on the summarizer logger, so it tags
  every summarizer event, including failures that never reach summary generation.
  It is also on IGeneratedSummaryStats.
- realizedDataStoreCount, channelCount and reusedChannelCount added to
  IGeneratedSummaryStats, derived from the generated summary tree or from context
  state common to both flows rather than from either flow's internals.
- A node that cannot participate in the new flow now throws a UsageError carrying
  its type and id, instead of a bare assert.

The feature gate key is shared between the runtime and the summarizer so the flow
that runs and the flow that is reported cannot disagree.
@github-actions github-actions Bot added area: tools area: runtime Runtime related issues area: dds Issues related to distributed data structures area: repo Repo related work area: website area: dds: sharedstring public api change Changes to a public API area: dds: tree changeset-present base: main PRs targeted against main branch labels Aug 13, 2026
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Hi! Thank you for opening this PR. Want me to review it?

Based on the diff (2478 lines, 64 files), I've queued these reviewers:

  • Correctness — logic errors, race conditions, lifecycle issues
  • Security — vulnerabilities, secret exposure, injection
  • API Compatibility — breaking changes, release tags, type design
  • Performance — algorithmic regressions, memory leaks
  • Testing — coverage gaps, hollow tests

How this works

  • Adjust the reviewer set by ticking/unticking boxes above. Reviewer toggles alone don't trigger anything.

  • Tick Start review below to dispatch the review fleet.

  • After review finishes, tick Start review again to request another run — it auto-resets after each dispatch.

  • This comment updates as new commits land; your reviewer selections are preserved.

  • Start review

IGeneratedSummaryStats gained the summarizeFlow and reuse-count fields after the
changeset was written.
Without summarizer nodes, refreshLatestSummaryAck always took the "summary not
tracked by this client" path, because summarizerNode.refreshLatestSummary has no
record of a summary the v2 flow produced. That path refetches the snapshot, waits
closeSummarizerDelayMs (5s) and then disposes the summarizer - after every
successful summary.

End to end this showed up as summaries taking ~5s instead of ~80ms, blowing the
2s test budget and hanging the summarize-retry tests. The runtime now decides
whether the ack is its own by remembering the handle it uploaded.

Found by enabling the flow by default and running the local-server e2e suite.
Found by enabling the flow by default and running the local-server e2e suite,
which went from 33 failures to 9.

Version boundary: a container can hold a data store runtime from a release that
predates summarize2. The DataStore layer now advertises a summarize2 layer-compat
feature and FluidDataStoreContext uses the old summarize API for a data store
whose runtime does not. Channels and shared objects fall back the same way. Such
a data store is written in full - its channels cannot be incremental either,
since their summarizer nodes are only usable when the summary was started through
the summarizer node tree (calling summarize with trackState there asserts 0x5df).

Reference state: a data store's unreferenced flag is part of its summary, so a
change in used routes has to count as a change even when content did not change.
The first observation in a session has no local baseline and uses the summarizer
node's hasUsedStateChanged(), whose reference used routes come from the base
snapshot; that is what catches a reference state which changed before this client
loaded.

Also fixes the GC sweep tests, which injected summary failures by monkey-patching
containerRuntime.summarize and so never failed under the new flow.
@github-actions github-actions Bot added the area: tests Tests to add, test infrastructure improvements, etc label Aug 13, 2026
The schema summarizer previously got its incremental reuse from
IExperimentalIncrementalSummaryContext.summaryPath, which the summarize2
flow does not provide, so the stored schema was rewritten on every
summary.

Add an optional canReuseSummary(latestSummarySequenceNumber) to
Summarizable and implement it on the schema summarizer by comparing
against the sequence number at which the stored schema last changed.
summarizeCore2 reuses the whole summarizable subtree via
createBuilderForChild(key).nodeDidNotChange(), which also preserves the
version metadata blob, so a summary keeps the format it was last written
with until the schema actually changes.

Also add a missing ISummaryTree import in dataStoreContext.ts that
incremental tsc had been masking, and update summarizeV2.md to record
that the schema summarizer is ported and to describe the remaining work
for the forest summarizer.
@agarwal-navin Navin Agarwal (agarwal-navin) changed the title Add builder-based summarization flow (summarize2) behind a feature gate summarizeV2 - Simplfied summary procss without summarizer nodes Aug 13, 2026
@agarwal-navin Navin Agarwal (agarwal-navin) changed the title summarizeV2 - Simplfied summary procss without summarizer nodes summarizeV2 - Simplified summary procss without summarizer nodes Aug 13, 2026
@agarwal-navin Navin Agarwal (agarwal-navin) changed the title summarizeV2 - Simplified summary procss without summarizer nodes summarize2 - Simplified summary procss without summarizer nodes Aug 13, 2026
@agarwal-navin Navin Agarwal (agarwal-navin) changed the title summarize2 - Simplified summary procss without summarizer nodes summarize2 - Simplified summary process without summarizer nodes Aug 13, 2026
summarize2 was suffixed only to avoid colliding with the existing summarize
during rollout, which meant it had to be renamed again once summarize was
removed. generateSummary does not collide, so it is a keeper: the rollout ends
by deleting summarize rather than by a second rename of released API.

The verb also matches what the summarize pipeline already calls this step in
its telemetry ("generateSummary" in summaryGenerator).

Renames summarizeCore2 to generateSummaryCore across the DDSes for the same
reason, and the layer compatibility feature to match.

Also removes unused imports in channelContext.ts and sharedObjectKernel.ts that
incremental TypeScript builds were masking - these fail a clean build.
@agarwal-navin Navin Agarwal (agarwal-navin) changed the title summarize2 - Simplified summary process without summarizer nodes Simplified summary process without summarizer nodes Aug 18, 2026
The recorded totals were stale and one entry pointed at the wrong test. Measured
against the local driver with the gate on, the numbers are 582 passing and 10
failing, not 583 and 9.

The summary-handle-resolution category is two tests rather than one, and they are
in tree.spec.ts, not summarizeWithOutOfOrderDataStoreRealization.spec.ts - the
test listed there is not among the failures. Record what they actually assert and
leave the resolution open, since a data store attached via op having no previous
summary for its subtree looks more like a missing never-summarized sequence number
on the attach path than something to relax the tests about.
@github-actions

Copy link
Copy Markdown
Contributor

Bundle size comparison

Base commit: 5da88704f2f1be6f7b4c2ec44fd497add9433cf7
Head commit: 18c16c33a79b77d0d933af9541194a005d6f2d92

Pending — Build - client packages is running. Results will appear here when the build completes.

@github-actions

Copy link
Copy Markdown
Contributor

🔗 Found some broken links! 💔

Run a link check locally to find them. See Checking for Broken Links for more information.

linkcheck output

1: starting server using command "npm run serve -- --no-open"
and when url "[ 'http://127.0.0.1:3000' ]" is responding with HTTP status code 200
running tests using command "npm run check-links"


> fluid-framework-website@0.0.0 serve
> docusaurus serve --no-open

[SUCCESS] Serving "build" directory at: http://localhost:3000/
[ELIFECYCLE] Command failed with exit code 1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: dds: sharedstring area: dds: tree area: dds Issues related to distributed data structures area: repo Repo related work area: runtime Runtime related issues area: tests Tests to add, test infrastructure improvements, etc area: tools area: website base: main PRs targeted against main branch changeset-present public api change Changes to a public API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant