Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ tick that writes more than the room left, such as one value larger than its
byte budget, can still cross that mark. The idle truncation empties the WAL
with no further write, on the Worker or on the main thread without one.

Space goes back to the disk once. When every one-time pass has finished and
A new journal is created in incremental auto-vacuum, so it gives free pages
back from its first compaction ticks. A journal created before that gets its
space back once. When every one-time pass has finished and
the trace was cut once, the next core start checks the journal before opening
it (`journal/vacuum.ts`): with free pages over a quarter of the file and over
256 MiB, and room on the volume for the live pages plus 512 MiB, it writes
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/journal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export class Journal {
this.stream = new StreamBuffer(this.db, this, this.onError);
this.processes = new ProcessRows(this.db);
try {
if ((this.db.query('PRAGMA page_count').get() as { page_count: number }).page_count === 0) this.db.exec('PRAGMA auto_vacuum = INCREMENTAL'); // Only a new file takes it: no `journal/vacuum.ts` ever.
this.db.exec('PRAGMA journal_mode = WAL');
this.db.exec('PRAGMA synchronous = NORMAL');
this.db.exec('PRAGMA busy_timeout = 5000');
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/journal/retention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ function dailyPass(journal: Journal, now: number, atKey: string, throughKey: str

/**
* Gives free pages back to the disk on a journal in incremental auto-vacuum
* (`journal/vacuum.ts` turns it on), `pages` per tick, until the free list is
* (set when the journal is created, or by `journal/vacuum.ts` for an older one), `pages` per tick, until the free list is
* back to `FREE_KEPT_BYTES`.
*/
function freePages(journal: Journal, pages: number): Step {
Expand Down
25 changes: 24 additions & 1 deletion packages/core/test/journal-vacuum.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, expect, spyOn, test } from 'bun:test';
import { Database } from 'bun:sqlite';
import * as fs from 'node:fs';
import { existsSync, mkdtempSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
Expand All @@ -16,9 +17,18 @@ const when = { share: 0.25, bytes: 4 * MiB };
const file = () => join(dir, 'journal.db');
const pragma = (journal: Journal, name: string) => Object.values(journal.db.query(`PRAGMA ${name}`).get() as Record<string, number>)[0]!;

/** A journal whose background passes finished, holding `freed` bytes of free pages and a kept message. */
/** A journal file made before new journals started in incremental auto-vacuum: its first page is written without it. */
function legacyJournalFile(): void {
const db = new Database(file(), { create: true });
db.exec('CREATE TABLE legacy (x); DROP TABLE legacy;');
db.close();
}

/** An older journal whose background passes finished, holding `freed` bytes of free pages and a kept message. */
function journalWithFreePages(freed: number, finished = true): void {
legacyJournalFile();
const journal = new Journal(file());
expect(pragma(journal, 'auto_vacuum')).toBe(0);
journal.putMessage({ id: 'msg_kept', threadId: 'thr_a', turnId: 'trn_a', role: 'assistant', parts: [{ type: 'text', text: 'kept' }], state: 'complete', createdAt: 1 });
const insert = journal.db.query("INSERT INTO events (thread_id, ts, type, version, payload) VALUES ('thr_b', 1, 'turn.finished', 1, ?)");
journal.db.transaction(() => { for (let n = 0; n < freed / 4096; n++) insert.run('x'.repeat(4000)); })();
Expand Down Expand Up @@ -69,6 +79,19 @@ test('once the passes are done, a start gives the free pages back and turns on i
}
});

test('a new journal starts in incremental auto-vacuum and never needs the VACUUM', () => {
const journal = new Journal(file());
try {
expect(pragma(journal, 'auto_vacuum')).toBe(2);
expect(journal.db.query('PRAGMA journal_mode').get()).toEqual({ journal_mode: 'wal' });
for (const key of ONE_TIME_PASSES) journal.setSetting(key, Number.MAX_SAFE_INTEGER);
journal.setSetting(PRUNED_AT, 1);
} finally {
journal.close();
}
expect(vacuumPlan(file(), { share: 0, bytes: 0 })).toBeNull();
});

test('no VACUUM before the passes finished or under the thresholds, and the journal is left as it was', () => {
journalWithFreePages(12 * MiB, false);
expect(vacuumPlan(file(), when)).toBeNull();
Expand Down
Loading