From 6bd851e0db66c9a9191ff6067b4335bbaab0aab1 Mon Sep 17 00:00:00 2001 From: vrazuvaev Date: Fri, 10 Apr 2026 19:34:52 +0200 Subject: [PATCH 1/3] fix(apollo-forest-run): index covered operations --- .../src/__tests__/helpers/forest.ts | 1 + .../apollo-forest-run/src/cache/draftHelpers.ts | 16 ++++++++-------- packages/apollo-forest-run/src/cache/store.ts | 6 ++++++ packages/apollo-forest-run/src/forest/addTree.ts | 6 ++++++ packages/apollo-forest-run/src/forest/types.ts | 1 + 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/apollo-forest-run/src/__tests__/helpers/forest.ts b/packages/apollo-forest-run/src/__tests__/helpers/forest.ts index 4018d906c..a8d9824e3 100644 --- a/packages/apollo-forest-run/src/__tests__/helpers/forest.ts +++ b/packages/apollo-forest-run/src/__tests__/helpers/forest.ts @@ -36,6 +36,7 @@ export function createTestForest(): IndexedForest { operationsByNodes: new Map(), operationsWithErrors: new Set(), operationsByName: new Map(), + operationsByCoveredName: new Map(), operationsByPartitions: new Map(), deletedNodes: new Set(), }; diff --git a/packages/apollo-forest-run/src/cache/draftHelpers.ts b/packages/apollo-forest-run/src/cache/draftHelpers.ts index 438562c9f..fd37b3e40 100644 --- a/packages/apollo-forest-run/src/cache/draftHelpers.ts +++ b/packages/apollo-forest-run/src/cache/draftHelpers.ts @@ -248,15 +248,15 @@ function getCoveringOperationIds( let ids: Set | undefined; for (const layer of layers) { - // Forward: find ops that cover us (their covers list includes our name) + // Forward: find ops that cover us via pre-built index if (opName) { - for (const tree of layer.trees.values()) { - if ( - tree.operation.id !== operation.id && - tree.operation.covers.includes(opName) - ) { - if (!ids) ids = new Set(); - ids.add(tree.operation.id); + const coveringIds = layer.operationsByCoveredName.get(opName); + if (coveringIds) { + for (const id of coveringIds) { + if (id !== operation.id) { + if (!ids) ids = new Set(); + ids.add(id); + } } } } diff --git a/packages/apollo-forest-run/src/cache/store.ts b/packages/apollo-forest-run/src/cache/store.ts index eec06f1da..0a2e6c8a9 100644 --- a/packages/apollo-forest-run/src/cache/store.ts +++ b/packages/apollo-forest-run/src/cache/store.ts @@ -27,6 +27,7 @@ export function createStore(_: CacheEnv): Store { trees: new Map(), operationsByNodes: new Map>(), operationsByName: new Map(), + operationsByCoveredName: new Map(), operationsByPartitions: new Map(), operationsWithErrors: new Set(), extraRootIds: new Map(), @@ -193,6 +194,7 @@ export function createOptimisticLayer( operationsByNodes: new Map(), operationsWithErrors: new Set(), operationsByName: new Map(), + operationsByCoveredName: new Map(), operationsByPartitions: new Map(), extraRootIds: new Map(), readResults: new Map(), @@ -361,6 +363,9 @@ function removeDataTree( dataForest.readResults.delete(operation); dataForest.operationsWithErrors.delete(operation); dataForest.operationsByName.get(operation.name ?? "")?.delete(operation.id); + for (const coveredName of operation.covers) { + dataForest.operationsByCoveredName.get(coveredName)?.delete(operation.id); + } dataForest.operationsByPartitions.get(partition)?.delete(operation.id); optimisticReadResults.delete(operation); partialReadResults.delete(operation); @@ -380,6 +385,7 @@ export function resetStore(store: Store): void { dataForest.operationsByNodes.clear(); dataForest.operationsWithErrors.clear(); dataForest.operationsByName.clear(); + dataForest.operationsByCoveredName.clear(); dataForest.operationsWithDanglingRefs.clear(); dataForest.readResults.clear(); operations.clear(); diff --git a/packages/apollo-forest-run/src/forest/addTree.ts b/packages/apollo-forest-run/src/forest/addTree.ts index bd006bcdd..bffab2e73 100644 --- a/packages/apollo-forest-run/src/forest/addTree.ts +++ b/packages/apollo-forest-run/src/forest/addTree.ts @@ -50,6 +50,12 @@ function trackOperationName(forest: IndexedForest, tree: IndexedTree) { const name = tree.operation.name; if (!name) return; getOrCreate(forest.operationsByName, name, newSet).add(tree.operation.id); + + for (const coveredName of tree.operation.covers) { + getOrCreate(forest.operationsByCoveredName, coveredName, newSet).add( + tree.operation.id, + ); + } } function trackPartitions( diff --git a/packages/apollo-forest-run/src/forest/types.ts b/packages/apollo-forest-run/src/forest/types.ts index e124740cb..49b827195 100644 --- a/packages/apollo-forest-run/src/forest/types.ts +++ b/packages/apollo-forest-run/src/forest/types.ts @@ -125,6 +125,7 @@ export type IndexedForest = { operationsByNodes: Map>; // May contain false positives operationsWithErrors: Set; // May contain false positives operationsByName: Map>; // operationName → operation IDs + operationsByCoveredName: Map>; // coveredName → IDs of ops whose covers list includes it operationsByPartitions: Map>; // partition key => operation IDs deletedNodes: Set; }; From 4b46a2a1f67ff88a01bf3c514209645a492fa034 Mon Sep 17 00:00:00 2001 From: vrazuvaev Date: Fri, 10 Apr 2026 19:35:05 +0200 Subject: [PATCH 2/3] add benchmark --- .../src/scenarios.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/apollo-forest-run-benchmark/src/scenarios.ts b/packages/apollo-forest-run-benchmark/src/scenarios.ts index a7fbd9fb3..154d3d03d 100644 --- a/packages/apollo-forest-run-benchmark/src/scenarios.ts +++ b/packages/apollo-forest-run-benchmark/src/scenarios.ts @@ -1,5 +1,15 @@ import type { ForestRun } from "@graphitation/apollo-forest-run"; import type { Scenario, ScenarioContext } from "./types"; +import { parse } from "graphql"; + +// Pre-parse 50 unique named queries to simulate a real app with many active operations. +// Each has its own operation name and data, creating separate trees in the forest. +// This is the setup that exposes O(n) scans in getCoveringOperationIds. +const EXTRA_OPS_COUNT = 50; +const extraOperations = Array.from({ length: EXTRA_OPS_COUNT }, (_, i) => ({ + query: parse(`query BgQuery${i} { node${i}(id: "${i}") { id value } }`), + data: { [`node${i}`]: { __typename: `Node${i}`, id: `${i}`, value: i } }, +})); const addWatchers = ( watcherCount: number, @@ -404,4 +414,32 @@ export const scenarios = [ }; }, }, + { + name: "write-many-ops-one-field-change", + prepare: (ctx: ScenarioContext) => { + const { operations, CacheFactory, configuration, watcherCount } = ctx; + const cache = new CacheFactory(configuration); + + // Populate the cache with many unrelated operations (simulating real app) + for (const { query, data } of extraOperations) { + cache.writeQuery({ query, data }); + } + + // Write the main query, add watchers, then measure a write with a change. + // Each watcher's diff triggers readOperation → applyTransformations → + // createChunkMatcher → getCoveringOperationIds which scans all trees. + const { data, query } = operations["complex-nested"]; + cache.writeQuery({ query, data: data["complex-nested"] }); + addWatchers(watcherCount, cache, query); + + return { + run() { + return cache.writeQuery({ + query, + data: data["complex-nested-single-field-change"], + }); + }, + }; + }, + }, ] as const satisfies Scenario[]; From f6240c5ee992c13d9b1ab1d836be0326cb027f9e Mon Sep 17 00:00:00 2001 From: vrazuvaev Date: Fri, 10 Apr 2026 19:35:34 +0200 Subject: [PATCH 3/3] Change files --- ...lo-forest-run-bd962896-4b98-4c62-853f-2cf1f3b74811.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@graphitation-apollo-forest-run-bd962896-4b98-4c62-853f-2cf1f3b74811.json diff --git a/change/@graphitation-apollo-forest-run-bd962896-4b98-4c62-853f-2cf1f3b74811.json b/change/@graphitation-apollo-forest-run-bd962896-4b98-4c62-853f-2cf1f3b74811.json new file mode 100644 index 000000000..86a0f5ca5 --- /dev/null +++ b/change/@graphitation-apollo-forest-run-bd962896-4b98-4c62-853f-2cf1f3b74811.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix(apollo-forest-run): index covered operations", + "packageName": "@graphitation/apollo-forest-run", + "email": "vrazuvaev@microsoft.com_msteamsmdb", + "dependentChangeType": "patch" +}