simplify cache LRU logic; use count and not size

This commit is contained in:
Brandon Liu
2022-11-18 22:16:47 +08:00
parent 9fd1d39ab5
commit 6a1af0f4ca
2 changed files with 19 additions and 48 deletions

View File

@@ -131,8 +131,6 @@ export interface Entry {
runLength: number; runLength: number;
} }
const ENTRY_SIZE_BYTES = 32;
export enum Compression { export enum Compression {
Unknown = 0, Unknown = 0,
None = 1, None = 1,
@@ -472,7 +470,7 @@ async function getHeaderAndRoot(
const rootDir = deserializeIndex( const rootDir = deserializeIndex(
await decompress(rootDirData, header.internalCompression) await decompress(rootDirData, header.internalCompression)
); );
return [header, [dirKey, ENTRY_SIZE_BYTES * rootDir.length, rootDir]]; return [header, [dirKey, rootDir.length, rootDir]];
} }
return [header, undefined]; return [header, undefined];
@@ -502,26 +500,23 @@ async function getDirectory(
interface ResolvedValue { interface ResolvedValue {
lastUsed: number; lastUsed: number;
size: number;
data: Header | Entry[] | ArrayBuffer; data: Header | Entry[] | ArrayBuffer;
} }
export class ResolvedValueCache { export class ResolvedValueCache {
cache: Map<string, ResolvedValue>; cache: Map<string, ResolvedValue>;
sizeBytes: number; maxCacheEntries: number;
maxSizeBytes: number;
counter: number; counter: number;
prefetch: boolean; prefetch: boolean;
decompress: DecompressFunc; decompress: DecompressFunc;
constructor( constructor(
maxSizeBytes = 64000000, maxCacheEntries = 100,
prefetch = true, prefetch = true,
decompress: DecompressFunc = fflateDecompress decompress: DecompressFunc = fflateDecompress
) { ) {
this.cache = new Map<string, ResolvedValue>(); this.cache = new Map<string, ResolvedValue>();
this.sizeBytes = 0; this.maxCacheEntries = maxCacheEntries;
this.maxSizeBytes = maxSizeBytes;
this.counter = 1; this.counter = 1;
this.prefetch = prefetch; this.prefetch = prefetch;
this.decompress = decompress; this.decompress = decompress;
@@ -544,7 +539,6 @@ export class ResolvedValueCache {
if (res[1]) { if (res[1]) {
this.cache.set(res[1][0], { this.cache.set(res[1][0], {
lastUsed: this.counter++, lastUsed: this.counter++,
size: res[1][1],
data: res[1][2], data: res[1][2],
}); });
} }
@@ -552,9 +546,7 @@ export class ResolvedValueCache {
this.cache.set(cacheKey, { this.cache.set(cacheKey, {
lastUsed: this.counter++, lastUsed: this.counter++,
data: res[0], data: res[0],
size: HEADER_SIZE_BYTES,
}); });
this.sizeBytes += HEADER_SIZE_BYTES;
this.prune(); this.prune();
return res[0]; return res[0];
} }
@@ -583,9 +575,7 @@ export class ResolvedValueCache {
this.cache.set(cacheKey, { this.cache.set(cacheKey, {
lastUsed: this.counter++, lastUsed: this.counter++,
data: directory, data: directory,
size: ENTRY_SIZE_BYTES * directory.length,
}); });
this.sizeBytes += ENTRY_SIZE_BYTES * directory.length;
this.prune(); this.prune();
return directory; return directory;
} }
@@ -612,15 +602,13 @@ export class ResolvedValueCache {
this.cache.set(cacheKey, { this.cache.set(cacheKey, {
lastUsed: this.counter++, lastUsed: this.counter++,
data: resp.data, data: resp.data,
size: resp.data.byteLength,
}); });
this.sizeBytes += resp.data.byteLength;
this.prune(); this.prune();
return resp.data; return resp.data;
} }
prune() { prune() {
while (this.sizeBytes > this.maxSizeBytes) { if (this.cache.size > this.maxCacheEntries) {
let minUsed = Infinity; let minUsed = Infinity;
let minKey = undefined; let minKey = undefined;
this.cache.forEach((cache_value: ResolvedValue, key: string) => { this.cache.forEach((cache_value: ResolvedValue, key: string) => {
@@ -630,7 +618,6 @@ export class ResolvedValueCache {
} }
}); });
if (minKey) { if (minKey) {
this.sizeBytes -= this.cache.get(minKey)!.size;
this.cache.delete(minKey); this.cache.delete(minKey);
} }
} }
@@ -644,7 +631,6 @@ export class ResolvedValueCache {
interface SharedPromiseCacheValue { interface SharedPromiseCacheValue {
lastUsed: number; lastUsed: number;
size: number; // 0 if the promise has not resolved
data: Promise<Header | Entry[] | ArrayBuffer>; data: Promise<Header | Entry[] | ArrayBuffer>;
} }
@@ -654,20 +640,18 @@ interface SharedPromiseCacheValue {
// (estimates) the maximum size of the cache. // (estimates) the maximum size of the cache.
export class SharedPromiseCache { export class SharedPromiseCache {
cache: Map<string, SharedPromiseCacheValue>; cache: Map<string, SharedPromiseCacheValue>;
sizeBytes: number; maxCacheEntries: number;
maxSizeBytes: number;
counter: number; counter: number;
prefetch: boolean; prefetch: boolean;
decompress: DecompressFunc; decompress: DecompressFunc;
constructor( constructor(
maxSizeBytes = 64000000, maxCacheEntries = 100,
prefetch = true, prefetch = true,
decompress: DecompressFunc = fflateDecompress decompress: DecompressFunc = fflateDecompress
) { ) {
this.cache = new Map<string, SharedPromiseCacheValue>(); this.cache = new Map<string, SharedPromiseCacheValue>();
this.sizeBytes = 0; this.maxCacheEntries = maxCacheEntries;
this.maxSizeBytes = maxSizeBytes;
this.counter = 1; this.counter = 1;
this.prefetch = prefetch; this.prefetch = prefetch;
this.decompress = decompress; this.decompress = decompress;
@@ -684,14 +668,9 @@ export class SharedPromiseCache {
const p = new Promise<Header>((resolve, reject) => { const p = new Promise<Header>((resolve, reject) => {
getHeaderAndRoot(source, this.decompress, this.prefetch, current_etag) getHeaderAndRoot(source, this.decompress, this.prefetch, current_etag)
.then((res) => { .then((res) => {
if (this.cache.has(cacheKey)) {
this.cache.get(cacheKey)!.size = HEADER_SIZE_BYTES;
this.sizeBytes += HEADER_SIZE_BYTES;
}
if (res[1]) { if (res[1]) {
this.cache.set(res[1][0], { this.cache.set(res[1][0], {
lastUsed: this.counter++, lastUsed: this.counter++,
size: res[1][1],
data: Promise.resolve(res[1][2]), data: Promise.resolve(res[1][2]),
}); });
} }
@@ -702,7 +681,7 @@ export class SharedPromiseCache {
reject(e); reject(e);
}); });
}); });
this.cache.set(cacheKey, { lastUsed: this.counter++, data: p, size: 0 }); this.cache.set(cacheKey, { lastUsed: this.counter++, data: p });
return p; return p;
} }
@@ -724,18 +703,13 @@ export class SharedPromiseCache {
getDirectory(source, this.decompress, offset, length, header) getDirectory(source, this.decompress, offset, length, header)
.then((directory) => { .then((directory) => {
resolve(directory); resolve(directory);
if (this.cache.has(cacheKey)) {
this.cache.get(cacheKey)!.size =
ENTRY_SIZE_BYTES * directory.length;
this.sizeBytes += ENTRY_SIZE_BYTES * directory.length;
}
this.prune(); this.prune();
}) })
.catch((e) => { .catch((e) => {
reject(e); reject(e);
}); });
}); });
this.cache.set(cacheKey, { lastUsed: this.counter++, data: p, size: 0 }); this.cache.set(cacheKey, { lastUsed: this.counter++, data: p });
return p; return p;
} }
@@ -763,8 +737,6 @@ export class SharedPromiseCache {
} }
resolve(resp.data); resolve(resp.data);
if (this.cache.has(cacheKey)) { if (this.cache.has(cacheKey)) {
this.cache.get(cacheKey)!.size = resp.data.byteLength;
this.sizeBytes += resp.data.byteLength;
} }
this.prune(); this.prune();
}) })
@@ -772,12 +744,12 @@ export class SharedPromiseCache {
reject(e); reject(e);
}); });
}); });
this.cache.set(cacheKey, { lastUsed: this.counter++, data: p, size: 0 }); this.cache.set(cacheKey, { lastUsed: this.counter++, data: p });
return p; return p;
} }
prune() { prune() {
while (this.sizeBytes > this.maxSizeBytes) { if (this.cache.size >= this.maxCacheEntries) {
let minUsed = Infinity; let minUsed = Infinity;
let minKey = undefined; let minKey = undefined;
this.cache.forEach( this.cache.forEach(
@@ -789,7 +761,6 @@ export class SharedPromiseCache {
} }
); );
if (minKey) { if (minKey) {
this.sizeBytes -= this.cache.get(minKey)!.size;
this.cache.delete(minKey); this.cache.delete(minKey);
} }
} }

View File

@@ -237,7 +237,6 @@ test("cache getDirectory", async (assertion) => {
for (const v of cache.cache.values()) { for (const v of cache.cache.values()) {
assertion.ok(v.lastUsed > 0); assertion.ok(v.lastUsed > 0);
assertion.ok(v.size > 0);
} }
}); });
@@ -324,14 +323,15 @@ test("soft failure on etag weirdness", async (assertion) => {
}); });
test("cache pruning by byte size", async (assertion) => { test("cache pruning by byte size", async (assertion) => {
const cache = new SharedPromiseCache(1000, false); const cache = new SharedPromiseCache(2, false);
cache.cache.set("0", { lastUsed: 0, data: Promise.resolve([]), size: 400 }); cache.cache.set("0", { lastUsed: 0, data: Promise.resolve([]) });
cache.cache.set("1", { lastUsed: 1, data: Promise.resolve([]), size: 200 }); cache.cache.set("1", { lastUsed: 1, data: Promise.resolve([]) });
cache.cache.set("2", { lastUsed: 2, data: Promise.resolve([]), size: 900 }); cache.cache.set("2", { lastUsed: 2, data: Promise.resolve([]) });
cache.sizeBytes = 900 + 200 + 400;
cache.prune(); cache.prune();
assertion.eq(cache.cache.size, 1); assertion.eq(cache.cache.size, 2);
assertion.ok(cache.cache.get("2")); assertion.ok(cache.cache.get("2"));
assertion.ok(cache.cache.get("1"));
assertion.notOk(cache.cache.get("0"));
}); });
test("pmtiles get metadata", async (assertion) => { test("pmtiles get metadata", async (assertion) => {