Skip to content

Commit 25d010a

Browse files
authored
Improve request graph cache reading (#9721)
* Make nodes per blob a method to allow overriding * Add a test for the stale cache issue * Update tests to cover more cases * Write the number of nodes in each blob to the cache * Undo erroneous variable move * Be extra mathsy
1 parent 4556b5c commit 25d010a

File tree

2 files changed

+84
-18
lines changed

2 files changed

+84
-18
lines changed

packages/core/core/src/RequestTracker.js

+38-18
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,10 @@ const keyFromEnvContentKey = (contentKey: ContentKey): string =>
267267
const keyFromOptionContentKey = (contentKey: ContentKey): string =>
268268
contentKey.slice('option:'.length);
269269

270+
// This constant is chosen by local profiling the time to serialise n nodes and tuning until an average time of ~50 ms per blob.
271+
// The goal is to free up the event loop periodically to allow interruption by the user.
272+
const NODES_PER_BLOB = 2 ** 14;
273+
270274
export class RequestGraph extends ContentGraph<
271275
RequestGraphNode,
272276
RequestGraphEdgeType,
@@ -283,6 +287,7 @@ export class RequestGraph extends ContentGraph<
283287
invalidateOnBuildNodeIds: Set<NodeId> = new Set();
284288
cachedRequestChunks: Set<number> = new Set();
285289
configKeyNodes: Map<ProjectPath, Set<NodeId>> = new Map();
290+
nodesPerBlob: number = NODES_PER_BLOB;
286291

287292
// $FlowFixMe[prop-missing]
288293
static deserialize(opts: RequestGraphOpts): RequestGraph {
@@ -1032,14 +1037,10 @@ export class RequestGraph extends ContentGraph<
10321037
}
10331038

10341039
removeCachedRequestChunkForNode(nodeId: number): void {
1035-
this.cachedRequestChunks.delete(Math.floor(nodeId / NODES_PER_BLOB));
1040+
this.cachedRequestChunks.delete(Math.floor(nodeId / this.nodesPerBlob));
10361041
}
10371042
}
10381043

1039-
// This constant is chosen by local profiling the time to serialise n nodes and tuning until an average time of ~50 ms per blob.
1040-
// The goal is to free up the event loop periodically to allow interruption by the user.
1041-
const NODES_PER_BLOB = 2 ** 14;
1042-
10431044
export default class RequestTracker {
10441045
graph: RequestGraph;
10451046
farm: WorkerFarm;
@@ -1427,17 +1428,30 @@ export default class RequestTracker {
14271428
}
14281429
}
14291430

1430-
for (let i = 0; i * NODES_PER_BLOB < cacheableNodes.length; i += 1) {
1431+
let nodeCountsPerBlob = [];
1432+
1433+
for (
1434+
let i = 0;
1435+
i * this.graph.nodesPerBlob < cacheableNodes.length;
1436+
i += 1
1437+
) {
1438+
let nodesStartIndex = i * this.graph.nodesPerBlob;
1439+
let nodesEndIndex = Math.min(
1440+
(i + 1) * this.graph.nodesPerBlob,
1441+
cacheableNodes.length,
1442+
);
1443+
1444+
nodeCountsPerBlob.push(nodesEndIndex - nodesStartIndex);
1445+
14311446
if (!this.graph.hasCachedRequestChunk(i)) {
14321447
// We assume the request graph nodes are immutable and won't change
1448+
let nodesToCache = cacheableNodes.slice(nodesStartIndex, nodesEndIndex);
1449+
14331450
queue
14341451
.add(() =>
14351452
serialiseAndSet(
14361453
getRequestGraphNodeKey(i, cacheKey),
1437-
cacheableNodes.slice(
1438-
i * NODES_PER_BLOB,
1439-
(i + 1) * NODES_PER_BLOB,
1440-
),
1454+
nodesToCache,
14411455
).then(() => {
14421456
// Succeeded in writing to disk, save that we have completed this chunk
14431457
this.graph.setCachedRequestChunk(i);
@@ -1455,6 +1469,7 @@ export default class RequestTracker {
14551469
// Set the request graph after the queue is flushed to avoid writing an invalid state
14561470
await serialiseAndSet(requestGraphKey, {
14571471
...serialisedGraph,
1472+
nodeCountsPerBlob,
14581473
nodes: undefined,
14591474
});
14601475

@@ -1523,19 +1538,24 @@ export async function readAndDeserializeRequestGraph(
15231538
return deserialize(buffer);
15241539
};
15251540

1526-
let i = 0;
1527-
let nodePromises = [];
1528-
while (await cache.hasLargeBlob(getRequestGraphNodeKey(i, cacheKey))) {
1529-
nodePromises.push(getAndDeserialize(getRequestGraphNodeKey(i, cacheKey)));
1530-
i += 1;
1531-
}
1532-
15331541
let serializedRequestGraph = await getAndDeserialize(requestGraphKey);
15341542

1543+
let nodePromises = serializedRequestGraph.nodeCountsPerBlob.map(
1544+
async (nodesCount, i) => {
1545+
let nodes = await getAndDeserialize(getRequestGraphNodeKey(i, cacheKey));
1546+
invariant.equal(
1547+
nodes.length,
1548+
nodesCount,
1549+
'RequestTracker node chunk: invalid node count',
1550+
);
1551+
return nodes;
1552+
},
1553+
);
1554+
15351555
return {
15361556
requestGraph: RequestGraph.deserialize({
15371557
...serializedRequestGraph,
1538-
nodes: (await Promise.all(nodePromises)).flatMap(nodeChunk => nodeChunk),
1558+
nodes: (await Promise.all(nodePromises)).flat(),
15391559
}),
15401560
// This is used inside parcel query for `.inspectCache`
15411561
bufferLength,

packages/core/core/test/RequestTracker.test.js

+46
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,52 @@ describe('RequestTracker', () => {
308308
assert.strictEqual(called, false);
309309
});
310310

311+
it('should ignore stale node chunks from cache', async () => {
312+
let tracker = new RequestTracker({farm, options});
313+
314+
// Set the nodes per blob low so we can ensure multiple files without
315+
// creating 17,000 nodes
316+
tracker.graph.nodesPerBlob = 2;
317+
318+
tracker.graph.addNode({type: 0, id: 'some-file-node-1'});
319+
tracker.graph.addNode({type: 0, id: 'some-file-node-2'});
320+
tracker.graph.addNode({type: 0, id: 'some-file-node-3'});
321+
tracker.graph.addNode({type: 0, id: 'some-file-node-4'});
322+
tracker.graph.addNode({type: 0, id: 'some-file-node-5'});
323+
324+
await tracker.writeToCache();
325+
326+
// Create a new request tracker that shouldn't look at the old cache files
327+
tracker = new RequestTracker({farm, options});
328+
assert.equal(tracker.graph.nodes.length, 0);
329+
330+
tracker.graph.addNode({type: 0, id: 'some-file-node-1'});
331+
await tracker.writeToCache();
332+
333+
// Init a request tracker that should only read the relevant cache files
334+
tracker = await RequestTracker.init({farm, options});
335+
assert.equal(tracker.graph.nodes.length, 1);
336+
});
337+
338+
it('should init with multiple node chunks', async () => {
339+
let tracker = new RequestTracker({farm, options});
340+
341+
// Set the nodes per blob low so we can ensure multiple files without
342+
// creating 17,000 nodes
343+
tracker.graph.nodesPerBlob = 2;
344+
345+
tracker.graph.addNode({type: 0, id: 'some-file-node-1'});
346+
tracker.graph.addNode({type: 0, id: 'some-file-node-2'});
347+
tracker.graph.addNode({type: 0, id: 'some-file-node-3'});
348+
tracker.graph.addNode({type: 0, id: 'some-file-node-4'});
349+
tracker.graph.addNode({type: 0, id: 'some-file-node-5'});
350+
351+
await tracker.writeToCache();
352+
353+
tracker = await RequestTracker.init({farm, options});
354+
assert.equal(tracker.graph.nodes.length, 5);
355+
});
356+
311357
it('should write new nodes to cache', async () => {
312358
let tracker = new RequestTracker({farm, options});
313359

0 commit comments

Comments
 (0)