Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d09a7db01 |
+14
-51
@@ -58,13 +58,6 @@ const THUMBNAIL_EXT = ".jpg";
|
|||||||
|
|
||||||
type Kind = "original" | "thumbnail";
|
type Kind = "original" | "thumbnail";
|
||||||
|
|
||||||
// An original write in progress, with the IDs of the concurrent original writes
|
|
||||||
// it overlaps (recorded both ways as writes begin, cleared when the write ends).
|
|
||||||
interface OriginalWrite {
|
|
||||||
fileID: number;
|
|
||||||
overlaps: Set<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The priority a caller attaches to a thumbnail prefetch. The pool has two
|
// The priority a caller attaches to a thumbnail prefetch. The pool has two
|
||||||
// tiers, so this three-value surface collapses onto them: only a currently
|
// tiers, so this three-value surface collapses onto them: only a currently
|
||||||
// visible thumbnail preempts (on-demand); "ahead" prefetch and speculative
|
// visible thumbnail preempts (on-demand); "ahead" prefetch and speculative
|
||||||
@@ -255,13 +248,6 @@ export class ContentCache implements PhotoContent, ThumbnailsAPI {
|
|||||||
// Serializes limit enforcement so concurrent original writes never race on
|
// Serializes limit enforcement so concurrent original writes never race on
|
||||||
// the map or delete each other's just-freed room.
|
// the map or delete each other's just-freed room.
|
||||||
private enforcing: Promise<void> = Promise.resolve();
|
private enforcing: Promise<void> = Promise.resolve();
|
||||||
// Original writes in progress. Writes for different files run concurrently
|
|
||||||
// (the content pool), so an eviction pass must never delete a file whose
|
|
||||||
// fetch has not yet returned. Each entry records the IDs of the concurrent
|
|
||||||
// original writes it overlaps — noted both ways as writes begin — and a
|
|
||||||
// write's eviction pass spares them all. Bounded by the pool's concurrency,
|
|
||||||
// so eviction is never deferred beyond the active working set.
|
|
||||||
private readonly inFlightOriginals = new Set<OriginalWrite>();
|
|
||||||
|
|
||||||
constructor(opts: ContentCacheOptions) {
|
constructor(opts: ContentCacheOptions) {
|
||||||
this.pools = opts.pools;
|
this.pools = opts.pools;
|
||||||
@@ -455,14 +441,6 @@ export class ContentCache implements PhotoContent, ThumbnailsAPI {
|
|||||||
// to completion.
|
// to completion.
|
||||||
if (signal?.aborted) throw new AbortDrop();
|
if (signal?.aborted) throw new AbortDrop();
|
||||||
|
|
||||||
// Register this original among those in flight, linking it with
|
|
||||||
// every sibling already writing so neither evicts the other's
|
|
||||||
// file. Non-null iff this is an original.
|
|
||||||
const write =
|
|
||||||
kind === "original"
|
|
||||||
? this.beginOriginalWrite(fileID)
|
|
||||||
: null;
|
|
||||||
try {
|
|
||||||
await this.download(file, dest, kind, opts?.onByte);
|
await this.download(file, dest, kind, opts?.onByte);
|
||||||
await chmod(dest, FILE_MODE);
|
await chmod(dest, FILE_MODE);
|
||||||
const size = (await stat(dest)).size;
|
const size = (await stat(dest)).size;
|
||||||
@@ -473,14 +451,13 @@ export class ContentCache implements PhotoContent, ThumbnailsAPI {
|
|||||||
}
|
}
|
||||||
known.set(fileID, dest);
|
known.set(fileID, dest);
|
||||||
// A fresh original may have crossed the limit; make room by
|
// A fresh original may have crossed the limit; make room by
|
||||||
// evicting least-recently-used originals. An over-budget
|
// evicting least-recently-used originals. The file just written
|
||||||
// fetch keeps the file it returns, and no overlapping
|
// is excluded from the candidates so an on-demand fetch that is
|
||||||
// sibling is evicted. Thumbnails are never bounded.
|
// itself over budget proceeds over-limit and returns a path that
|
||||||
if (write) await this.enforceOriginalsLimit(write);
|
// exists on disk. Thumbnails are never bounded or evicted.
|
||||||
|
if (kind === "original")
|
||||||
|
await this.enforceOriginalsLimit(fileID);
|
||||||
return { path: dest, bytes: size, cached: false };
|
return { path: dest, bytes: size, cached: false };
|
||||||
} finally {
|
|
||||||
if (write) this.inFlightOriginals.delete(write);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ priority, key: fileID },
|
{ priority, key: fileID },
|
||||||
);
|
);
|
||||||
@@ -565,26 +542,13 @@ export class ContentCache implements PhotoContent, ThumbnailsAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record a starting original write among those in flight, linking it with
|
// Evict least-recently-used originals until usage fits the limit. Pinned
|
||||||
// every sibling already writing so neither can evict the other's file.
|
// originals are skipped, as is `justWrittenID` (the original this write just
|
||||||
private beginOriginalWrite(fileID: number): OriginalWrite {
|
// stored, so an over-budget on-demand fetch never evicts the file it is
|
||||||
const write: OriginalWrite = { fileID, overlaps: new Set() };
|
// about to return). When only such originals remain the cache stays
|
||||||
for (const other of this.inFlightOriginals) {
|
// over-limit until the protected set shrinks; the skipped file becomes
|
||||||
write.overlaps.add(other.fileID);
|
// eligible on a later write.
|
||||||
other.overlaps.add(fileID);
|
private enforceOriginalsLimit(justWrittenID: number): Promise<void> {
|
||||||
}
|
|
||||||
this.inFlightOriginals.add(write);
|
|
||||||
return write;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evict least-recently-used originals until usage fits the limit. Skipped:
|
|
||||||
// pinned originals, the file `write` just stored, and every original whose
|
|
||||||
// write overlaps it (`write.overlaps`). The last two spare any fetch whose
|
|
||||||
// lifetime overlaps this one, so concurrent over-budget fetches all keep the
|
|
||||||
// paths they return; when only such originals remain the cache stays
|
|
||||||
// over-limit until they settle, and a later, non-overlapping write finds
|
|
||||||
// them eligible again.
|
|
||||||
private enforceOriginalsLimit(write: OriginalWrite): Promise<void> {
|
|
||||||
return this.serializeEnforce(async () => {
|
return this.serializeEnforce(async () => {
|
||||||
const { entries, used } = await this.measureOriginals();
|
const { entries, used } = await this.measureOriginals();
|
||||||
const limit = await this.originalsLimit(used);
|
const limit = await this.originalsLimit(used);
|
||||||
@@ -593,8 +557,7 @@ export class ContentCache implements PhotoContent, ThumbnailsAPI {
|
|||||||
const evictable = entries
|
const evictable = entries
|
||||||
.filter(
|
.filter(
|
||||||
(e) =>
|
(e) =>
|
||||||
e.fileID !== write.fileID &&
|
e.fileID !== justWrittenID &&
|
||||||
!write.overlaps.has(e.fileID) &&
|
|
||||||
!this.isPinned(e.fileID),
|
!this.isPinned(e.fileID),
|
||||||
)
|
)
|
||||||
.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
||||||
|
|||||||
@@ -70,40 +70,6 @@ class SizedSource implements ContentSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A source whose original() writes 10 bytes but blocks on a gate before
|
|
||||||
// returning, so two fetches can be held in flight together. `bothStarted`
|
|
||||||
// resolves once both originals have entered, and `release()` lets them finish.
|
|
||||||
class GatedSource implements ContentSource {
|
|
||||||
private openGate!: () => void;
|
|
||||||
private readonly gate = new Promise<void>((r) => (this.openGate = r));
|
|
||||||
private inFlight = 0;
|
|
||||||
private reachedTwo!: () => void;
|
|
||||||
readonly bothStarted = new Promise<void>((r) => (this.reachedTwo = r));
|
|
||||||
|
|
||||||
async original(args: {
|
|
||||||
file: EnteFile;
|
|
||||||
destination: string;
|
|
||||||
}): Promise<{ bytesWritten: number }> {
|
|
||||||
this.inFlight += 1;
|
|
||||||
if (this.inFlight === 2) this.reachedTwo();
|
|
||||||
await this.gate;
|
|
||||||
await writeFile(args.destination, Buffer.alloc(10, 1));
|
|
||||||
return { bytesWritten: 10 };
|
|
||||||
}
|
|
||||||
|
|
||||||
async thumbnail(args: {
|
|
||||||
file: EnteFile;
|
|
||||||
destination: string;
|
|
||||||
}): Promise<{ bytesWritten: number }> {
|
|
||||||
await writeFile(args.destination, Buffer.alloc(10, 1));
|
|
||||||
return { bytesWritten: 10 };
|
|
||||||
}
|
|
||||||
|
|
||||||
release(): void {
|
|
||||||
this.openGate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let root: string;
|
let root: string;
|
||||||
let cacheDir: string;
|
let cacheDir: string;
|
||||||
|
|
||||||
@@ -311,35 +277,6 @@ describe("originals eviction", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps both originals when two over-budget fetches race", async () => {
|
|
||||||
// A 15-byte cap holds one 10-byte original but not two. Two fetches are
|
|
||||||
// held in flight together; when both stored files cross the limit,
|
|
||||||
// neither eviction pass may delete the sibling whose path has not yet
|
|
||||||
// been returned, so both survive over-limit.
|
|
||||||
const source = new GatedSource();
|
|
||||||
const { cache } = buildCache({
|
|
||||||
source,
|
|
||||||
statfs: abundantFree,
|
|
||||||
cacheOriginalsMaxBytes: 15,
|
|
||||||
freeBelowBytes: 0,
|
|
||||||
});
|
|
||||||
await cache.open();
|
|
||||||
|
|
||||||
const p1 = cache.original(1);
|
|
||||||
const p2 = cache.original(2);
|
|
||||||
await source.bothStarted; // both downloads are in flight before either stores
|
|
||||||
source.release();
|
|
||||||
const [r1, r2] = await Promise.all([p1, p2]);
|
|
||||||
|
|
||||||
// Both returned paths exist on disk even though together they exceed the
|
|
||||||
// cap; nothing was evicted out from under a fetch still in progress.
|
|
||||||
expect(existsSync(r1.path)).toBe(true);
|
|
||||||
expect(existsSync(r2.path)).toBe(true);
|
|
||||||
expect(existsSync(originalPath(1))).toBe(true);
|
|
||||||
expect(existsSync(originalPath(2))).toBe(true);
|
|
||||||
expect(cache.originalsStatus().usedBytes).toBe(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("never counts or evicts thumbnails", async () => {
|
it("never counts or evicts thumbnails", async () => {
|
||||||
const { cache } = buildCache({
|
const { cache } = buildCache({
|
||||||
statfs: abundantFree,
|
statfs: abundantFree,
|
||||||
|
|||||||
Reference in New Issue
Block a user