Skip to content

Commit 29b63bb

Browse files
committed
improve composable-cache performance
1 parent d07c9f1 commit 29b63bb

File tree

2 files changed

+48
-63
lines changed

2 files changed

+48
-63
lines changed
Lines changed: 45 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,24 @@
11
import type { ComposableCacheEntry, ComposableCacheHandler } from "types/cache";
2-
import type { CacheValue } from "types/overrides";
32
import { writeTags } from "utils/cache";
43
import { fromReadableStream, toReadableStream } from "utils/stream";
54
import { debug } from "./logger";
65

7-
const pendingWritePromiseMap = new Map<
8-
string,
9-
Promise<CacheValue<"composable">>
10-
>();
6+
const pendingWritePromiseMap = new Map<string, Promise<ComposableCacheEntry>>();
117

128
export default {
139
async get(cacheKey: string) {
1410
try {
15-
// We first check if we have a pending write for this cache key
16-
// If we do, we return the pending promise instead of fetching the cache
17-
if (pendingWritePromiseMap.has(cacheKey)) {
18-
const stored = pendingWritePromiseMap.get(cacheKey);
19-
if (stored) {
20-
return stored.then((entry) => ({
21-
...entry,
22-
value: toReadableStream(entry.value),
23-
}));
24-
}
25-
}
11+
const stored = pendingWritePromiseMap.get(cacheKey);
12+
if (stored) return stored;
13+
2614
const result = await globalThis.incrementalCache.get(
2715
cacheKey,
2816
"composable",
2917
);
30-
if (!result?.value?.value) {
31-
return undefined;
32-
}
18+
if (!result?.value?.value) return undefined;
3319

3420
debug("composable cache result", result);
3521

36-
// We need to check if the tags associated with this entry has been revalidated
3722
if (
3823
globalThis.tagCache.mode === "nextMode" &&
3924
result.value.tags.length > 0
@@ -69,73 +54,77 @@ export default {
6954
},
7055

7156
async set(cacheKey: string, pendingEntry: Promise<ComposableCacheEntry>) {
72-
const promiseEntry = pendingEntry.then(async (entry) => ({
73-
...entry,
74-
value: await fromReadableStream(entry.value),
75-
}));
76-
pendingWritePromiseMap.set(cacheKey, promiseEntry);
57+
const teedPromise = pendingEntry.then((entry) => {
58+
// Optimization: We avoid consuming and stringifying the stream here,
59+
// because it creates double copies just to be discarded when this function
60+
// ends. This avoids unnecessary memory usage, and reduces GC pressure.
61+
const [stream1, stream2] = entry.value.tee();
62+
return [
63+
{ ...entry, value: stream1 },
64+
{ ...entry, value: stream2 },
65+
] as const;
66+
});
7767

78-
const entry = await promiseEntry.finally(() => {
68+
pendingWritePromiseMap.set(
69+
cacheKey,
70+
teedPromise.then(([entry]) => entry),
71+
);
72+
73+
const [, entryForStorage] = await teedPromise.finally(() => {
7974
pendingWritePromiseMap.delete(cacheKey);
8075
});
76+
8177
await globalThis.incrementalCache.set(
8278
cacheKey,
8379
{
84-
...entry,
85-
value: entry.value,
80+
...entryForStorage,
81+
value: await fromReadableStream(entryForStorage.value),
8682
},
8783
"composable",
8884
);
85+
8986
if (globalThis.tagCache.mode === "original") {
9087
const storedTags = await globalThis.tagCache.getByPath(cacheKey);
91-
const tagsToWrite = entry.tags.filter((tag) => !storedTags.includes(tag));
88+
const tagsToWrite = [];
89+
for (const tag of entryForStorage.tags) {
90+
if (!storedTags.includes(tag)) {
91+
tagsToWrite.push({ tag, path: cacheKey });
92+
}
93+
}
9294
if (tagsToWrite.length > 0) {
93-
await writeTags(tagsToWrite.map((tag) => ({ tag, path: cacheKey })));
95+
await writeTags(tagsToWrite);
9496
}
9597
}
9698
},
9799

98-
async refreshTags() {
99-
// We don't do anything for now, do we want to do something here ???
100-
return;
101-
},
100+
async refreshTags() {},
101+
102102
async getExpiration(...tags: string[]) {
103-
if (globalThis.tagCache.mode === "nextMode") {
104-
return globalThis.tagCache.getLastRevalidated(tags);
105-
}
106-
// We always return 0 here, original tag cache are handled directly in the get part
107-
// TODO: We need to test this more, i'm not entirely sure that this is working as expected
108-
return 0;
103+
return globalThis.tagCache.mode === "nextMode"
104+
? globalThis.tagCache.getLastRevalidated(tags)
105+
: 0;
109106
},
107+
110108
async expireTags(...tags: string[]) {
111109
if (globalThis.tagCache.mode === "nextMode") {
112110
return writeTags(tags);
113111
}
112+
114113
const tagCache = globalThis.tagCache;
115114
const revalidatedAt = Date.now();
116-
// For the original mode, we have more work to do here.
117-
// We need to find all paths linked to to these tags
118115
const pathsToUpdate = await Promise.all(
119116
tags.map(async (tag) => {
120117
const paths = await tagCache.getByTag(tag);
121-
return paths.map((path) => ({
122-
path,
123-
tag,
124-
revalidatedAt,
125-
}));
118+
return paths.map((path) => ({ path, tag, revalidatedAt }));
126119
}),
127120
);
128-
// We need to deduplicate paths, we use a set for that
129-
const setToWrite = new Set<{ path: string; tag: string }>();
121+
122+
const dedupeMap = new Map();
130123
for (const entry of pathsToUpdate.flat()) {
131-
setToWrite.add(entry);
124+
dedupeMap.set(`${entry.path}|${entry.tag}`, entry);
132125
}
133-
await writeTags(Array.from(setToWrite));
126+
await writeTags(Array.from(dedupeMap.values()));
134127
},
135128

136-
// This one is necessary for older versions of next
137-
async receiveExpiredTags(...tags: string[]) {
138-
// This function does absolutely nothing
139-
return;
140-
},
129+
async receiveExpiredTags() {},
141130
} satisfies ComposableCacheHandler;

packages/open-next/src/utils/stream.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,9 @@ export async function fromReadableStream(
2020
return Buffer.from(chunks[0]).toString(base64 ? "base64" : "utf8");
2121
}
2222

23-
// Pre-allocate buffer with exact size to avoid reallocation
24-
const buffer = Buffer.alloc(totalLength);
25-
let offset = 0;
26-
for (const chunk of chunks) {
27-
buffer.set(chunk, offset);
28-
offset += chunk.length;
29-
}
23+
// Use Buffer.concat which is more efficient than manual allocation and copy
24+
// It handles the allocation and copy in optimized native code
25+
const buffer = Buffer.concat(chunks, totalLength);
3026

3127
return buffer.toString(base64 ? "base64" : "utf8");
3228
}

0 commit comments

Comments
 (0)