From e541ee2ef6ab20d136dd2a65050fbc8e6cbf5103 Mon Sep 17 00:00:00 2001 From: Brad McCormack Date: Sat, 1 Aug 2026 15:02:55 +1000 Subject: [PATCH] fix(mnemopi): align SQLite page size with OS page size Add the initial page-size detection and regression coverage for file-backed Mnemopi databases. --- packages/mnemopi/CHANGELOG.md | 3 ++ packages/mnemopi/src/db.ts | 25 +++++++++- packages/mnemopi/test/db-page-size.test.ts | 54 ++++++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 packages/mnemopi/test/db-page-size.test.ts diff --git a/packages/mnemopi/CHANGELOG.md b/packages/mnemopi/CHANGELOG.md index 3608b7e55..f2e6aa2a0 100644 --- a/packages/mnemopi/CHANGELOG.md +++ b/packages/mnemopi/CHANGELOG.md @@ -7,6 +7,9 @@ ### Fixed - Stripped `…` reasoning blocks from remote LLM output in `cleanOutput`, so reasoning-model responses no longer leak into consolidated memories or corrupt fact extraction (the reasoning wrapper previously survived parsing and every stored fact became reasoning prose). ([#7231](https://github.com/can1357/oh-my-pi/issues/7231)) +### Fixed + +- Fixed SQLite databases being created with a hardcoded 4 KB page size regardless of the OS page size. On systems with larger pages (e.g. Apple Silicon / Asahi Linux at 16 KB), every SQLite page read touches 4 OS pages, causing 4x I/O amplification. Mnemopi now detects the OS page size at startup via `getconf PAGE_SIZE` and sets `PRAGMA page_size` before any tables are created, eliminating the write amplification on non-4K-page systems. ## [17.2.2] - 2026-07-31 diff --git a/packages/mnemopi/src/db.ts b/packages/mnemopi/src/db.ts index 2ce63b94f..653f935fb 100644 --- a/packages/mnemopi/src/db.ts +++ b/packages/mnemopi/src/db.ts @@ -3,6 +3,24 @@ import { mkdirSync } from "node:fs"; import { dirname } from "node:path"; import { dbPath } from "./config"; +let _osPageSize: number | undefined; + +function osPageSize(): number { + if (_osPageSize !== undefined) return _osPageSize; + try { + const proc = Bun.spawnSync(["getconf", "PAGE_SIZE"], { stdout: "pipe" }); + if (proc.exitCode === 0) { + const size = Number.parseInt(proc.stdout.toString().trim(), 10); + if (size >= 512 && size <= 65536 && (size & (size - 1)) === 0) { + _osPageSize = size; + return size; + } + } + } catch { /* fall through */ } + _osPageSize = 4096; + return 4096; +} + export type DatabasePath = string | ":memory:"; export interface OpenDatabaseOptions { @@ -33,11 +51,14 @@ export function openDatabase(path: DatabasePath = dbPath(), options: OpenDatabas if (options.loadExtension !== undefined) loadExtensions(db, options.loadExtension); return db; } - export function enablePragmas(db: Database, path?: DatabasePath): void { db.exec("PRAGMA foreign_keys=ON"); db.exec("PRAGMA busy_timeout=5000"); - if (path !== ":memory:") db.exec("PRAGMA journal_mode=WAL"); + if (path !== ":memory:") { + const pageSize = osPageSize(); + if (pageSize !== 4096) db.exec(`PRAGMA page_size=${pageSize}`); + db.exec("PRAGMA journal_mode=WAL"); + } } export function loadExtensions(db: Database, extensions: string | readonly string[]): void { diff --git a/packages/mnemopi/test/db-page-size.test.ts b/packages/mnemopi/test/db-page-size.test.ts new file mode 100644 index 000000000..b8d52b177 --- /dev/null +++ b/packages/mnemopi/test/db-page-size.test.ts @@ -0,0 +1,54 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { openDatabase, closeQuietly } from "../src/db"; + +function expectedOsPageSize(): number { + try { + const proc = Bun.spawnSync(["getconf", "PAGE_SIZE"], { stdout: "pipe" }); + if (proc.exitCode === 0) { + const size = Number.parseInt(proc.stdout.toString().trim(), 10); + if (size >= 512 && size <= 65536 && (size & (size - 1)) === 0) return size; + } + } catch { /* fall through */ } + return 4096; +} + +const roots: string[] = []; + +afterEach(() => { + for (;;) { + const root = roots.pop(); + if (root === undefined) break; + rmSync(root, { recursive: true, force: true }); + } +}); + +describe("db page size", () => { + it("creates a new database with page size matching the OS page size", () => { + const root = mkdtempSync(join(tmpdir(), "mnemopi-page-size-")); + roots.push(root); + const path = join(root, "test.db"); + + const db = openDatabase(path); + try { + const pageSize = db.query("PRAGMA page_size").get() as { page_size: number }; + expect(pageSize.page_size).toBe(expectedOsPageSize()); + } finally { + closeQuietly(db); + } + }); + + it("does not set page_size on in-memory databases", () => { + const db = openDatabase(":memory:"); + try { + const pageSize = db.query("PRAGMA page_size").get() as { page_size: number }; + // In-memory DBs keep the SQLite default (4096) since there is no + // persistent file whose I/O would benefit from OS page alignment. + expect(pageSize.page_size).toBe(4096); + } finally { + closeQuietly(db); + } + }); +});