perf(images): measure raster headers from a probe and memoize repo icon validation

`getRepos()` re-sanitizes every repo on every call, and an uploaded/file repo
icon costs a full base64 decode of its data URI each time. Three fixes:

- `writeQuartet` destructured a mutable array, which sends V8 through the
  iterator protocol once per four input characters; index reads plus a length
  counter produce identical bytes.
- `decodeBase64Prefix` decoded the whole payload despite only the first bytes
  being needed. `exceedsRasterImagePreviewLimits` now probes 64 bytes and
  widens x16 until the header measures, and only re-runs the original
  full-payload decode when the verdict would suppress a preview.
- `sanitizeRepoIcon`'s src validation is memoized per icon source with a
  bounded FIFO map, reusing the `memoizeTitleClassification` idiom (now a
  shared `memoizeByStringKey`).
This commit is contained in:
Neil
2026-09-03 20:39:56 -07:00
parent 63aee7f1ee
commit 2817ff479e
6 changed files with 553 additions and 43 deletions
+29
View File
@@ -0,0 +1,29 @@
/**
* FIFO-bounded memo for a pure `(key: string) => T`.
*
* Insertion-ordered eviction: the oldest key is the one whose input has been superseded longest, so
* it is the least likely to be asked for again. Entries hold a reference to a string the caller
* already retains, so a live entry costs the map slot and nothing else.
*/
export function memoizeByStringKey<T>(
compute: (key: string) => T,
maxEntries: number
): (key: string) => T {
// Boxed values so `undefined`/`null` results are still cache hits.
const cache = new Map<string, { value: T }>()
return (key: string): T => {
const cached = cache.get(key)
if (cached) {
return cached.value
}
const value = compute(key)
if (cache.size >= maxEntries) {
const oldest = cache.keys().next()
if (!oldest.done) {
cache.delete(oldest.value)
}
}
cache.set(key, { value })
return value
}
}
@@ -0,0 +1,345 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { decodeBase64Prefix, exceedsRasterImagePreviewLimits } from './raster-image-base64-preview'
import type * as RasterImageDimensionsModule from './raster-image-dimensions'
import { readRasterImageDimensions } from './raster-image-dimensions'
import {
isKnownRasterImageMimeType,
isRasterImagePreviewDimensions,
RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
} from './raster-image-preview-limits'
/** The pre-change verdict: one full-payload decode, then one dimension read. */
function unprobedExceeds(content: string, mimeType: string | undefined): boolean {
if (!isKnownRasterImageMimeType(mimeType)) {
return false
}
const prefix = referenceDecodeBase64Prefix(content, RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES)
if (!prefix) {
return false
}
const dimensions = readRasterImageDimensions(prefix)
return dimensions !== null && !isRasterImagePreviewDimensions(dimensions)
}
// Why a module mock: the byte length handed to the dimension reader is the direct measure of how
// much of the payload the preview check decoded, and it is the only observable difference between
// the early-stopping probe and the full-payload decode it replaces.
const { dimensionReadLengths } = vi.hoisted(() => ({ dimensionReadLengths: [] as number[] }))
vi.mock('./raster-image-dimensions', async (importOriginal) => {
const actual = await importOriginal<typeof RasterImageDimensionsModule>()
return {
...actual,
readRasterImageDimensions: (bytes: Uint8Array) => {
dimensionReadLengths.push(bytes.byteLength)
return actual.readRasterImageDimensions(bytes)
}
}
})
// ── Reference decoder: the pre-change implementation, verbatim ──────────────────────────────────
const BASE64_PADDING = -2
const INVALID_BASE64 = -1
function base64Value(code: number): number {
if (code >= 65 && code <= 90) {
return code - 65
}
if (code >= 97 && code <= 122) {
return code - 71
}
if (code >= 48 && code <= 57) {
return code + 4
}
if (code === 43) {
return 62
}
if (code === 47) {
return 63
}
if (code === 61) {
return BASE64_PADDING
}
return INVALID_BASE64
}
function isWhitespace(code: number): boolean {
return code === 9 || code === 10 || code === 12 || code === 13 || code === 32
}
function referenceWriteQuartet(
output: Uint8Array,
offset: number,
quartet: readonly number[]
): { bytesWritten: number; padded: boolean } | null {
const [a, b, c, d] = quartet
if (a === undefined || b === undefined || a < 0 || b < 0) {
return null
}
if (c === BASE64_PADDING) {
if (d !== BASE64_PADDING) {
return null
}
if (offset < output.length) {
output[offset] = (a << 2) | (b >> 4)
}
return { bytesWritten: Math.min(1, output.length - offset), padded: true }
}
if (c === undefined || c < 0) {
return null
}
if (offset < output.length) {
output[offset] = (a << 2) | (b >> 4)
}
if (offset + 1 < output.length) {
output[offset + 1] = ((b & 15) << 4) | (c >> 2)
}
if (d === BASE64_PADDING) {
return { bytesWritten: Math.min(2, output.length - offset), padded: true }
}
if (d === undefined || d < 0) {
return null
}
if (offset + 2 < output.length) {
output[offset + 2] = ((c & 3) << 6) | d
}
return { bytesWritten: Math.min(3, output.length - offset), padded: false }
}
function referenceDecodeBase64Prefix(content: string, maxBytes: number): Uint8Array | null {
const capacity = Math.min(maxBytes, Math.ceil(content.length / 4) * 3)
const output = new Uint8Array(capacity)
const quartet: number[] = []
let outputLength = 0
let padded = false
for (let index = 0; index < content.length && outputLength < capacity; index += 1) {
const code = content.charCodeAt(index)
if (isWhitespace(code)) {
continue
}
if (padded) {
return null
}
const value = base64Value(code)
if (value === INVALID_BASE64) {
return null
}
quartet.push(value)
if (quartet.length !== 4) {
continue
}
const decoded = referenceWriteQuartet(output, outputLength, quartet)
if (!decoded) {
return null
}
outputLength += decoded.bytesWritten
padded = decoded.padded
quartet.length = 0
}
if (!padded && outputLength < capacity && quartet.length > 0) {
if (quartet.length === 1 || quartet.includes(BASE64_PADDING)) {
return null
}
while (quartet.length < 4) {
quartet.push(BASE64_PADDING)
}
const decoded = referenceWriteQuartet(output, outputLength, quartet)
if (!decoded) {
return null
}
outputLength += decoded.bytesWritten
}
return output.subarray(0, outputLength)
}
// ── Fixtures ───────────────────────────────────────────────────────────────────────────────────
function pngBytes(totalBytes: number, width: number, height: number): Buffer {
const bytes = Buffer.alloc(Math.max(totalBytes, 24))
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(bytes)
bytes.writeUInt32BE(13, 8)
bytes.write('IHDR', 12, 'ascii')
bytes.writeUInt32BE(width, 16)
bytes.writeUInt32BE(height, 20)
for (let index = 24; index < bytes.length; index += 1) {
bytes[index] = (index * 31 + 7) & 0xff
}
return bytes
}
/** SOI, `metadataBytes` of APP2 padding (real cameras chain many segments), then SOF0. */
function jpegBytes(
metadataBytes: number,
width: number,
height: number,
trailingBytes = 4096
): Buffer {
const parts: Buffer[] = [Buffer.from([0xff, 0xd8])]
for (let written = 0; written < metadataBytes;) {
const size = Math.min(65_533, metadataBytes - written)
const header = Buffer.alloc(4)
header.writeUInt16BE(0xffe2)
header.writeUInt16BE(size + 2, 2)
parts.push(header, Buffer.alloc(size))
written += size
}
const sof = Buffer.alloc(11)
sof.writeUInt16BE(0xffc0)
sof.writeUInt16BE(8, 2)
sof[4] = 8
sof.writeUInt16BE(height, 5)
sof.writeUInt16BE(width, 7)
parts.push(sof, Buffer.alloc(trailingBytes))
return Buffer.concat(parts)
}
function gifBytes(width: number, height: number): Buffer {
const gif = Buffer.alloc(64)
gif.write('GIF89a', 0, 'ascii')
gif.writeUInt16LE(width, 6)
gif.writeUInt16LE(height, 8)
return gif
}
function webpBytes(width: number, height: number): Buffer {
const webp = Buffer.alloc(64)
webp.write('RIFF', 0, 'ascii')
webp.writeUInt32LE(50, 4)
webp.write('WEBP', 8, 'ascii')
webp.write('VP8X', 12, 'ascii')
webp.writeUInt32LE(10, 16)
webp.writeUIntLE(width - 1, 24, 3)
webp.writeUIntLE(height - 1, 27, 3)
return webp
}
const DECODE_FIXTURES: { label: string; content: string }[] = [
{ label: 'png', content: pngBytes(24, 512, 512).toString('base64') },
{ label: 'png padded once', content: pngBytes(26, 512, 512).toString('base64') },
{ label: 'png padded twice', content: pngBytes(25, 512, 512).toString('base64') },
{ label: 'png 70 KiB', content: pngBytes(70_000, 512, 512).toString('base64') },
{ label: 'jpeg', content: jpegBytes(0, 640, 480).toString('base64') },
{ label: 'jpeg 70 KiB exif', content: jpegBytes(70_000, 4000, 3000).toString('base64') },
{ label: 'gif', content: gifBytes(320, 240).toString('base64') },
{ label: 'webp', content: webpBytes(800, 600).toString('base64') },
{ label: 'empty', content: '' },
{ label: 'one character', content: 'A' },
{ label: 'two characters', content: 'AB' },
{ label: 'three characters', content: 'ABC' },
{ label: 'invalid character', content: 'AB*D' },
{ label: 'invalid tail', content: `${pngBytes(24, 4, 4).toString('base64')}!!!` },
{ label: 'padding mid-payload', content: 'AAAA=AAA' },
{ label: 'lone padding in tail', content: 'AAAAAB=' },
{ label: 'single padding', content: 'AAAAAA==' },
{ label: 'double padding', content: 'AAAAAAA=' },
{ label: 'stray padding after padding', content: 'AAAA====' },
{
label: 'line-wrapped png',
content: pngBytes(70_000, 512, 512)
.toString('base64')
.replace(/(.{76})/g, '$1\r\n')
},
{ label: 'leading and trailing whitespace', content: `\n\t ${'AAAA'} \r\n` },
{ label: 'truncated png header', content: pngBytes(24, 512, 512).toString('base64').slice(0, 18) }
]
const DECODE_CAPS = [
0,
1,
2,
3,
4,
23,
24,
25,
63,
64,
65,
1024,
RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
]
describe('decodeBase64Prefix', () => {
it('decodes byte-for-byte identically to the reference implementation', () => {
for (const { label, content } of DECODE_FIXTURES) {
for (const maxBytes of DECODE_CAPS) {
const expected = referenceDecodeBase64Prefix(content, maxBytes)
const actual = decodeBase64Prefix(content, maxBytes)
const detail = `${label} @ maxBytes=${maxBytes}`
if (expected === null) {
expect(actual, detail).toBeNull()
continue
}
expect(actual, detail).not.toBeNull()
expect(Array.from(actual!), detail).toEqual(Array.from(expected))
}
}
})
it('stops at the byte cap instead of decoding the whole payload', () => {
const content = pngBytes(70_000, 512, 512).toString('base64')
expect(decodeBase64Prefix(content, 32)?.byteLength).toBe(32)
})
})
describe('exceedsRasterImagePreviewLimits', () => {
beforeEach(() => {
dimensionReadLengths.length = 0
})
it('measures a large image from its first bytes, not its last', () => {
// Regression guard: before the probe this decoded all 5 MiB before reading 24 bytes of IHDR.
const content = pngBytes(5 * 1024 * 1024, 512, 512).toString('base64')
expect(exceedsRasterImagePreviewLimits(content, 'image/png')).toBe(false)
expect(dimensionReadLengths).toEqual([64])
})
it('widens the probe until a JPEG SOF past its metadata is reachable', () => {
const content = jpegBytes(70_000, 4000, 3000, 3_000_000).toString('base64')
expect(exceedsRasterImagePreviewLimits(content, 'image/jpeg')).toBe(false)
expect(dimensionReadLengths).toEqual([64, 1024, 16_384, 262_144])
// Far below the ~3 MiB the payload decodes to, and below the 8 MiB fallback cap.
expect(dimensionReadLengths.at(-1)!).toBeLessThan(RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES)
})
it('re-reads the whole payload before suppressing an over-limit image', () => {
const content = pngBytes(70_000, 32_769, 1).toString('base64')
expect(exceedsRasterImagePreviewLimits(content, 'image/png')).toBe(true)
// The header answers at 64 bytes, but a suppression verdict is only taken from the same
// full-payload decode the unprobed implementation used, so invalid base64 past the header
// still demotes the answer to "could not measure".
expect(dimensionReadLengths).toEqual([64, 70_000])
})
it('keeps rendering an over-limit header whose payload is not valid base64', () => {
const content = `${pngBytes(70_000, 32_769, 1).toString('base64')}!!!`
expect(exceedsRasterImagePreviewLimits(content, 'image/png')).toBe(false)
})
it('agrees with the unprobed implementation on every fixture and mime type', () => {
const mimeTypes = [
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'image/svg+xml',
undefined
]
const fixtures = [
...DECODE_FIXTURES,
{ label: 'over-limit png', content: pngBytes(24, 32_769, 1).toString('base64') },
{ label: 'over-limit pixels png', content: pngBytes(24, 8192, 8192).toString('base64') },
{ label: 'over-limit gif', content: gifBytes(65_535, 65_535).toString('base64') },
{ label: 'over-limit jpeg', content: jpegBytes(70_000, 40_000, 40_000).toString('base64') },
{ label: 'over-limit webp', content: webpBytes(40_000, 40_000).toString('base64') }
]
for (const { label, content } of fixtures) {
for (const mimeType of mimeTypes) {
expect(exceedsRasterImagePreviewLimits(content, mimeType), `${label} / ${mimeType}`).toBe(
unprobedExceeds(content, mimeType)
)
}
}
})
})
+62 -21
View File
@@ -34,13 +34,20 @@ function isWhitespace(code: number): boolean {
return code === 9 || code === 10 || code === 12 || code === 13 || code === 32
}
/** `quartetLength` under 4 is a final short group; the missing slots decode as `=` padding. */
function writeQuartet(
output: Uint8Array,
offset: number,
quartet: readonly number[]
quartet: readonly number[],
quartetLength: number
): { bytesWritten: number; padded: boolean } | null {
const [a, b, c, d] = quartet
if (a === undefined || b === undefined || a < 0 || b < 0) {
// Index reads, not `const [a, b, c, d] = quartet`: destructuring an array runs the iterator
// protocol (Symbol.iterator plus four `.next()` calls) once per four input characters.
const a = quartet[0]
const b = quartet[1]
const c = quartetLength > 2 ? quartet[2] : BASE64_PADDING
const d = quartetLength > 3 ? quartet[3] : BASE64_PADDING
if (quartetLength < 2 || a === undefined || b === undefined || a < 0 || b < 0) {
return null
}
if (c === BASE64_PADDING) {
@@ -73,10 +80,13 @@ function writeQuartet(
return { bytesWritten: Math.min(3, output.length - offset), padded: false }
}
function decodeBase64Prefix(content: string, maxBytes: number): Uint8Array | null {
/** Exported so the decode can be compared byte-for-byte against a reference implementation. */
export function decodeBase64Prefix(content: string, maxBytes: number): Uint8Array | null {
const capacity = Math.min(maxBytes, Math.ceil(content.length / 4) * 3)
const output = new Uint8Array(capacity)
const quartet: number[] = []
// Fixed four slots plus a counter, never resized: `quartet.length = 0` deoptimizes the array.
const quartet = [0, 0, 0, 0]
let quartetLength = 0
let outputLength = 0
let padded = false
@@ -92,27 +102,27 @@ function decodeBase64Prefix(content: string, maxBytes: number): Uint8Array | nul
if (value === INVALID_BASE64) {
return null
}
quartet.push(value)
if (quartet.length !== 4) {
quartet[quartetLength] = value
quartetLength += 1
if (quartetLength !== 4) {
continue
}
const decoded = writeQuartet(output, outputLength, quartet)
const decoded = writeQuartet(output, outputLength, quartet, 4)
if (!decoded) {
return null
}
outputLength += decoded.bytesWritten
padded = decoded.padded
quartet.length = 0
quartetLength = 0
}
if (!padded && outputLength < capacity && quartet.length > 0) {
if (quartet.length === 1 || quartet.includes(BASE64_PADDING)) {
return null
if (!padded && outputLength < capacity && quartetLength > 0) {
for (let index = 0; index < quartetLength; index += 1) {
if (quartet[index] === BASE64_PADDING) {
return null
}
}
while (quartet.length < 4) {
quartet.push(BASE64_PADDING)
}
const decoded = writeQuartet(output, outputLength, quartet)
const decoded = writeQuartet(output, outputLength, quartet, quartetLength)
if (!decoded) {
return null
}
@@ -121,6 +131,13 @@ function decodeBase64Prefix(content: string, maxBytes: number): Uint8Array | nul
return output.subarray(0, outputLength)
}
// First probe: past every fixed-offset header (PNG 24, GIF 10, WebP 30, BMP 26) and a JFIF-only
// JPEG's SOF, so an icon or screenshot is measured from its first bytes instead of its last.
const RASTER_IMAGE_HEADER_PROBE_BYTES = 64
// Growth per miss. JPEG SOF sits past however much EXIF/ICC/MPF the camera wrote, so the probe
// widens geometrically: total decoded stays within ~1.07x of the bytes the header actually needed.
const RASTER_IMAGE_HEADER_PROBE_GROWTH = 16
/**
* Whether the encoded dimensions are known to exceed the preview limits.
*
@@ -135,10 +152,34 @@ export function exceedsRasterImagePreviewLimits(
if (!isKnownRasterImageMimeType(mimeType)) {
return false
}
const prefix = decodeBase64Prefix(content, RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES)
if (!prefix) {
return false
let probeBytes = RASTER_IMAGE_HEADER_PROBE_BYTES
for (;;) {
const prefix = decodeBase64Prefix(content, probeBytes)
// A short probe only ever fails where the whole payload would: it walks a strict prefix of the
// same characters through the same state machine.
if (!prefix) {
return false
}
// Shorter than asked for means the payload ran out, so a wider probe cannot add bytes.
const exhausted =
prefix.byteLength < probeBytes || probeBytes >= RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
const dimensions = readRasterImageDimensions(prefix)
if (dimensions !== null) {
const withinLimits = isRasterImagePreviewDimensions(dimensions)
if (withinLimits || exhausted) {
return !withinLimits
}
// About to suppress: redo the decode over the whole payload so the verdict stays the one the
// full read gives, including its rejection of base64 that turns invalid past the header.
probeBytes = RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
continue
}
if (exhausted) {
return false
}
probeBytes = Math.min(
probeBytes * RASTER_IMAGE_HEADER_PROBE_GROWTH,
RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
)
}
const dimensions = readRasterImageDimensions(prefix)
return dimensions !== null && !isRasterImagePreviewDimensions(dimensions)
}
+75 -1
View File
@@ -1,6 +1,22 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import type * as ImageDataUriModule from './image-data-uri'
import { githubAvatarIcon, githubAvatarSlug, sanitizeRepoIcon } from './repo-icon'
// Why a module mock: `validateRasterImageDataUri` is the leaf that base64-decodes an inline icon's
// header, so counting its invocations is the direct measure of what re-hydrating a repo costs.
const { dataUriValidations } = vi.hoisted(() => ({ dataUriValidations: { count: 0 } }))
vi.mock('./image-data-uri', async (importOriginal) => {
const actual = await importOriginal<typeof ImageDataUriModule>()
return {
...actual,
validateRasterImageDataUri: (dataUri: string) => {
dataUriValidations.count += 1
return actual.validateRasterImageDataUri(dataUri)
}
}
})
const PNG_1X1_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='
const WEBP_1X1_BASE64 = 'UklGRhoAAABXRUJQVlA4IA4AAAAwAQCdASoBAAEAAQIlSkwAAA=='
@@ -188,3 +204,61 @@ describe('githubAvatarSlug', () => {
expect(githubAvatarSlug(null, undefined)).toBeNull()
})
})
describe('repo icon source validation memo', () => {
const HYDRATIONS = 25
const MAX_MEMOIZED_ICON_SOURCES = 64
function uploadIcon(width: number): { type: 'image'; src: string; source: 'upload' } {
return { type: 'image', src: `data:image/png;base64,${pngBase64(width, 1)}`, source: 'upload' }
}
it('validates each distinct icon src once across repeated hydrations', () => {
const icons = [uploadIcon(2), uploadIcon(3), uploadIcon(4)]
// Warm the memo the way the first hydration would, then measure steady state.
for (const icon of icons) {
sanitizeRepoIcon(icon)
}
dataUriValidations.count = 0
for (let hydration = 0; hydration < HYDRATIONS; hydration += 1) {
for (const icon of icons) {
expect(sanitizeRepoIcon(icon)).toEqual(icon)
}
}
// Unmemoized this is HYDRATIONS x icons full base64 header decodes; memoized an unchanged
// persisted icon costs nothing.
expect(dataUriValidations.count).toBe(0)
})
it('re-validates as soon as the src changes', () => {
dataUriValidations.count = 0
expect(sanitizeRepoIcon(uploadIcon(11))).toEqual(uploadIcon(11))
expect(sanitizeRepoIcon(uploadIcon(12))).toEqual(uploadIcon(12))
expect(dataUriValidations.count).toBe(2)
})
it('keeps the verdict specific to the icon source', () => {
const src = `data:image/webp;base64,${WEBP_1X1_BASE64}`
expect(sanitizeRepoIcon({ type: 'image', src, source: 'file' })).toEqual({
type: 'image',
src,
source: 'file'
})
// WebP is a `file` icon only; sharing one cache across sources would accept it as an upload.
expect(sanitizeRepoIcon({ type: 'image', src, source: 'upload' })).toBeUndefined()
})
it('evicts oldest entries instead of growing without bound', () => {
const overflow = MAX_MEMOIZED_ICON_SOURCES + 6
for (let index = 1; index <= overflow; index += 1) {
sanitizeRepoIcon(uploadIcon(1000 + index))
}
dataUriValidations.count = 0
sanitizeRepoIcon(uploadIcon(1000 + overflow))
expect(dataUriValidations.count).toBe(0)
sanitizeRepoIcon(uploadIcon(1001))
expect(dataUriValidations.count).toBe(1)
})
})
+39 -2
View File
@@ -1,3 +1,4 @@
import { memoizeByStringKey } from './bounded-string-key-memo'
import { validateRasterImageDataUri } from './image-data-uri'
export type RepoIconImageSource = 'upload' | 'file' | 'favicon' | 'github'
@@ -79,7 +80,14 @@ function normalizeGitHubAvatarHost(rawHost?: string): string {
}
}
function isSupportedImageSrc(src: string, source: RepoIconImageSource): boolean {
/**
* Cap: comfortably above the icon working set of a heavy multi-repo user, small enough that a
* session of icon edits cannot grow the map without limit. Keys are the same `src` strings the
* persisted repo list already holds, so a live entry costs the map slot and nothing else.
*/
const MAX_MEMOIZED_ICON_SOURCES = 64
function computeIsSupportedImageSrc(src: string, source: RepoIconImageSource): boolean {
if (source === 'upload') {
return (
/^data:image\/png;base64,[A-Za-z0-9+/=\s]+$/i.test(src) &&
@@ -112,6 +120,35 @@ function isSupportedImageSrc(src: string, source: RepoIconImageSource): boolean
return url.hostname === 'www.google.com' && url.pathname === '/s2/favicons'
}
/**
* Why: repo icons are immutable persisted strings, so a given `src` always validates the same way
* and there is no invalidation window — a changed icon is simply a new key. `getRepos()` re-hydrates
* every repo on every call, and validating one inline data URI means scanning it twice with a regex
* and base64-decoding its header.
*
* One memo per source rather than a composite key, because V8 caches a string's hash in the string
* itself: keying on `src` makes a 400 KB data URI an O(1) lookup, while `${source}\0${src}` would
* rebuild and rehash the whole thing on every call.
*/
const memoizedIsSupportedImageSrc: Record<RepoIconImageSource, (src: string) => boolean> = {
upload: memoizeByStringKey(
(src) => computeIsSupportedImageSrc(src, 'upload'),
MAX_MEMOIZED_ICON_SOURCES
),
file: memoizeByStringKey(
(src) => computeIsSupportedImageSrc(src, 'file'),
MAX_MEMOIZED_ICON_SOURCES
),
favicon: memoizeByStringKey(
(src) => computeIsSupportedImageSrc(src, 'favicon'),
MAX_MEMOIZED_ICON_SOURCES
),
github: memoizeByStringKey(
(src) => computeIsSupportedImageSrc(src, 'github'),
MAX_MEMOIZED_ICON_SOURCES
)
}
export function sanitizeRepoIcon(value: unknown): RepoIcon | null | undefined {
if (value === undefined) {
return undefined
@@ -146,7 +183,7 @@ export function sanitizeRepoIcon(value: unknown): RepoIcon | null | undefined {
if (!isRepoIconImageSource(source) || src.length > MAX_REPO_ICON_DATA_URL_LENGTH) {
return undefined
}
if (!isSupportedImageSrc(src, source)) {
if (!memoizedIsSupportedImageSrc[source](src)) {
return undefined
}
const label = typeof candidate.label === 'string' ? candidate.label.trim().slice(0, 80) : ''
@@ -1,3 +1,5 @@
import { memoizeByStringKey } from './bounded-string-key-memo'
/**
* Bounded memo for pure `(title: string) => T` terminal-title classifiers.
*
@@ -21,23 +23,5 @@ const MAX_MEMOIZED_TITLES = 1024
export function memoizeTitleClassification<T>(
classify: (title: string) => T
): (title: string) => T {
// Boxed values so `undefined`/`null` verdicts are still cache hits.
const cache = new Map<string, { value: T }>()
return (title: string): T => {
const cached = cache.get(title)
if (cached) {
return cached.value
}
const value = classify(title)
// Insertion-ordered FIFO eviction: a pane's superseded title frames are the
// oldest keys and the least likely to be asked for again.
if (cache.size >= MAX_MEMOIZED_TITLES) {
const oldest = cache.keys().next()
if (!oldest.done) {
cache.delete(oldest.value)
}
}
cache.set(title, { value })
return value
}
return memoizeByStringKey(classify, MAX_MEMOIZED_TITLES)
}