Read-ahead per reader; compose passes READ_AHEAD and PERF_TRACE; report fixes

Each user gets an own read-ahead queue, served in turn, so two people reading
at once no longer cancel each other's chapter. The report rounds phase times
and measures the quiet time before a slow storage probe correctly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MaashkinoozheandClaude Opus 5.5 committed 2026-10-29 22:51:19 +01:00
1 parent 896726a51b
commit d7d3bf5b2b
6 files changed
+57 -34

No files matched your search

+28 -25
View File
@@ -5,49 +5,52 @@
* with spinning disks a cold page costs 10-100 ms (seconds while a disk wakes up); a cached one well under
* 5 ms. The data is discarded; only the cache effect matters.
*
* Only the newest chapter request counts: opening another chapter replaces whatever is still queued.
* One queue per reader (`key`, the user): opening another chapter replaces that reader's queue only, and
* the queues take turns so two people reading at once both get their pages ahead.
*/
const fs = require('fs');
class ReadAhead {
constructor({ concurrency = 2, remember = 4000, onBatch = () => {} } = {}) {
this.concurrency = concurrency; this.remember = remember; this.onBatch = onBatch;
this.queue = []; this.active = 0; this.recent = new Map(); this.batch = null; this.buffers = [];
this.batches = new Map(); this.turn = []; this.active = 0; this.recent = new Map(); this.buffers = [];
}
// `files` in reading order; `tag` names the batch in the trace.
warm(files, tag) {
const fresh = files.filter(f => !this.recent.has(f));
if (this.batch) this.finishBatch(true);
this.queue = fresh;
this.batch = { tag, files: fresh.length, skipped: files.length - fresh.length, bytes: 0, t0: performance.now(), done: 0 };
if (!fresh.length) this.finishBatch(false);
warm(files, tag, key = '') {
const old = this.batches.get(key);
if (old) this.finish(key, old, true);
const queue = files.filter(f => !this.recent.has(f));
const batch = { tag, queue, files: queue.length, skipped: files.length - queue.length, bytes: 0, done: 0, active: 0, t0: performance.now() };
this.batches.set(key, batch);
if (!this.turn.includes(key)) this.turn.unshift(key); // a fresh request goes next
if (!queue.length) this.finish(key, batch, false);
this.pump();
}
// Runs after the current batch (e.g. the next chapter) unless a newer warm() replaces it first.
then(files, tag) {
const after = () => this.warm(files, tag);
if (!this.batch) after(); else this.batch.next = after;
// Runs after the reader's current batch (e.g. the next chapter) unless a newer warm() replaces it first.
then(files, tag, key = '') {
const batch = this.batches.get(key);
if (!batch) this.warm(files, tag, key); else batch.next = () => this.warm(files, tag, key);
}
pump() {
while (this.active < this.concurrency && this.queue.length) {
const file = this.queue.shift(), batch = this.batch;
this.active++;
this.readFile(file).then(bytes => {
this.note(file);
if (batch === this.batch) { batch.bytes += bytes; batch.done++; }
}, () => {}).finally(() => {
this.active--;
// A replaced batch's reads may still be running; the current batch ends when all reads have.
if (this.batch && !this.queue.length && !this.active) this.finishBatch(false);
while (this.active < this.concurrency) {
const key = this.turn.find(k => this.batches.get(k)?.queue.length);
if (key === undefined) return;
this.turn.push(...this.turn.splice(this.turn.indexOf(key), 1)); // round robin between readers
const batch = this.batches.get(key), file = batch.queue.shift();
this.active++; batch.active++;
this.readFile(file).then(bytes => { this.note(file); batch.bytes += bytes; batch.done++; }, () => {}).finally(() => {
this.active--; batch.active--;
if (this.batches.get(key) === batch && !batch.queue.length && !batch.active) this.finish(key, batch, false);
this.pump();
});
}
}
finishBatch(replaced) {
const b = this.batch; this.batch = null;
if (!b) return;
finish(key, b, replaced) {
if (this.batches.get(key) === b) this.batches.delete(key);
if (!this.batches.has(key)) this.turn = this.turn.filter(k => k !== key);
b.queue = [];
this.onBatch({ tag: b.tag, files: b.files, done: b.done, skipped: b.skipped, bytes: b.bytes,
ms: Math.round(performance.now() - b.t0), replaced });
if (!replaced && b.next) b.next();