Skip to content

Commit a7dfc84

Browse files
authored
Compress pinned REST cache in memory (~30-50 MB savings) (#61306)
1 parent 0c6004d commit a7dfc84

2 files changed

Lines changed: 69 additions & 7 deletions

File tree

src/rest/lib/index.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs, { promises as fsPromises } from 'fs'
22
import path from 'path'
33

44
import QuickLRU from 'quick-lru'
5-
import { brotliDecompress } from 'zlib'
5+
import { brotliDecompress, deflateSync, inflateSync } from 'zlib'
66
import { promisify } from 'util'
77
import { getAutomatedPageMiniTocItems, type MiniTocItem } from '@/frame/lib/get-mini-toc-items'
88
import { allVersions, getOpenApiVersion } from '@/versions/lib/all-versions'
@@ -60,7 +60,7 @@ const restOperationData = new Map<
6060
// they account for >90% of traffic and each version needs ~100 slots alone.
6161
// All other versions (ghes) go into a bounded LRU cache.
6262
const PINNED_OPEN_API_VERSIONS = new Set(['fpt', 'ghec'])
63-
export const pinnedCache = new Map<string, RestOperationCategory>() // @internal
63+
export const pinnedCache = new Map<string, Buffer>() // @internal — stores deflate-compressed JSON
6464
const LRU_MAX_SIZE = Math.max(1, parseInt(process.env.REST_SCHEMA_LRU_SIZE ?? '', 10) || 96)
6565
export const lruCache = new QuickLRU<string, RestOperationCategory>({ maxSize: LRU_MAX_SIZE }) // @internal
6666

@@ -106,20 +106,39 @@ export default async function getRest(
106106
const openapiSchemaName = apiVersion ? `${openApiVersion}-${apiVersion}` : `${openApiVersion}`
107107
const lruKey = `${openApiVersion}:${apiDate}:${category}`
108108

109-
const cache = PINNED_OPEN_API_VERSIONS.has(openApiVersion) ? pinnedCache : lruCache
109+
const isPinned = PINNED_OPEN_API_VERSIONS.has(openApiVersion)
110110

111-
if (!cache.has(lruKey)) {
111+
// Pinned cache: store deflate-compressed JSON Buffers to save ~100–500 MB heap.
112+
// LRU cache: store parsed objects (bounded size, low traffic).
113+
if (isPinned) {
114+
if (pinnedCache.has(lruKey)) {
115+
return JSON.parse(inflateSync(pinnedCache.get(lruKey)!).toString()) as RestOperationCategory
116+
}
117+
const basePath = path.join(REST_DATA_DIR, openapiSchemaName, `${category}.json`)
118+
if (!inflight.has(lruKey)) {
119+
inflight.set(
120+
lruKey,
121+
loadCategoryFile(basePath).finally(() => inflight.delete(lruKey)),
122+
)
123+
}
124+
const data = await inflight.get(lruKey)!
125+
pinnedCache.set(lruKey, deflateSync(Buffer.from(JSON.stringify(data))))
126+
return data
127+
} else {
128+
if (lruCache.has(lruKey)) {
129+
return lruCache.get(lruKey)!
130+
}
112131
const basePath = path.join(REST_DATA_DIR, openapiSchemaName, `${category}.json`)
113132
if (!inflight.has(lruKey)) {
114133
inflight.set(
115134
lruKey,
116135
loadCategoryFile(basePath).finally(() => inflight.delete(lruKey)),
117136
)
118137
}
119-
cache.set(lruKey, await inflight.get(lruKey)!)
138+
const data = await inflight.get(lruKey)!
139+
lruCache.set(lruKey, data)
140+
return data
120141
}
121-
122-
return cache.get(lruKey)!
123142
}
124143

125144
// Read asynchronously to avoid blocking the event loop on a cache miss.

src/rest/tests/lib-index.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,49 @@ describe('two-tier cache routing', () => {
176176
})
177177
})
178178

179+
// ---------------------------------------------------------------------------
180+
// 1b. Pinned cache compression
181+
// ---------------------------------------------------------------------------
182+
183+
describe('pinned cache compression', () => {
184+
test('pinnedCache stores a Buffer (compressed), not a parsed object', async () => {
185+
vi.mocked(fsMock.promises.readFile)
186+
.mockRejectedValueOnce(enoent())
187+
.mockResolvedValueOnce(FAKE_JSON as unknown as Buffer)
188+
189+
await getRest('free-pro-team@latest', undefined, 'actions')
190+
191+
const stored = [...pinnedCache.values()][0]
192+
expect(Buffer.isBuffer(stored)).toBe(true)
193+
})
194+
195+
test('getRest returns correct parsed data from compressed pinnedCache on cache hit', async () => {
196+
vi.mocked(fsMock.promises.readFile)
197+
.mockRejectedValueOnce(enoent())
198+
.mockResolvedValueOnce(FAKE_JSON as unknown as Buffer)
199+
200+
// First call populates the cache
201+
const first = await getRest('free-pro-team@latest', undefined, 'actions')
202+
// Second call reads from compressed cache
203+
const second = await getRest('free-pro-team@latest', undefined, 'actions')
204+
205+
expect(first).toEqual(FAKE_DATA)
206+
expect(second).toEqual(FAKE_DATA)
207+
})
208+
209+
test('lruCache stores parsed objects, not Buffers', async () => {
210+
vi.mocked(fsMock.promises.readFile)
211+
.mockRejectedValueOnce(enoent())
212+
.mockResolvedValueOnce(FAKE_JSON as unknown as Buffer)
213+
214+
await getRest('enterprise-server@3.10', undefined, 'actions')
215+
216+
const stored = lruCache.get([...(lruCache as unknown as Map<string, unknown>).keys()][0])
217+
expect(Buffer.isBuffer(stored)).toBe(false)
218+
expect(stored).toEqual(FAKE_DATA)
219+
})
220+
})
221+
179222
// ---------------------------------------------------------------------------
180223
// 2. In-flight deduplication
181224
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)