Skip to content

[PR] fix(storage): move auto_vacuum before journal_mode so that newly created index.db is truly INCREMENTAL | fix(storage): 把 auto_vacuum 挪到 journal_mode 之前,新建的 index.db 才真正是 INCREMENTAL - #6324

Open
BBleae wants to merge 1 commit into
ranxianglei:masterfrom
BBleae:2026-11-12_auto-vacuum-before-wal

Conversation

@BBleae

@BBleae BBleae commented Nov 12, 2026

Copy link
Copy Markdown

Fixes #6323

改了什么、为什么

storagePragmas() 原来先执行 PRAGMA journal_mode = WAL,最后才执行 PRAGMA auto_vacuum = INCREMENTAL。SQLite 只在数据库文件还是空的时候才认 auto_vacuum,而切到 WAL 时已经写了第 1 页。所以在全新的 index.db 上这条 pragma 被忽略:文件头里留着 auto_vacuum = 0,PRAGMA incremental_vacuum(gc({ vacuum: "incremental" })、bili storage gc --vacuum incremental、?vacuum=incremental)什么都回收不了。空闲页链表(freelist)只增不减,不跑离线的完整 VACUUM,文件就不会变小。

这不是 #3901 / #3463(PR #3911)的本意,也和 README 里「要在线安全地缩库用 --vacuum incremental」(#4612)对不上。

修法是把 auto_vacuum 那一行挪到列表最前面,别的都不变。kernel(kernel/**)没动。

只用 SQLite 的最小核对(同样两条 pragma,两种顺序各跑一遍;写入 100 个 100 KB 的 blob 再删掉,然后 PRAGMA incremental_vacuum):

wal-first:          auto_vacuum=0 page_count=2452 freelist_count=2450
auto_vacuum-first:  auto_vacuum=2 page_count=3    freelist_count=0

走 UnifiedStore(写入 20 个会话、全部删掉,再 gc({ vacuum: "incremental" })):

auto_vacuum 页数 前 → 后 之后的空闲页 文件大小 前 → 后
master 0 431 → 431 392 1 765 376 → 1 765 376
本 PR 2 432 → 40 0 1 769 472 → 163 840

持久化面(按 §7.4 标出)

面 变化 兼容性
sqlite 文件布局,新建的库 文件头里是 auto_vacuum = 2;文件带 pointer-map 页 这正是这条 pragma 一直想要的布局。是 SQLite 的普通特性,旧版本照样能读能写。
sqlite 文件布局,已有的库 无 见下文。
schema 版本、表、索引 无 —
mysql 后端 无 —
wire 协议 / 消息形状 无 —

已有的库不迁移

迄今创建的 index.db,只要还没跑过完整的 VACUUM,文件头里都是 auto_vacuum = 0。用本 PR 打开它不会改变这一点:在非空文件上,这条 pragma 只是给同一连接上的下一次完整 VACUUM 预设模式。所以对这些文件,--vacuum incremental 仍然什么都不做,直到跑一次完整 vacuum(bili storage gc --vacuum full / ?vacuum=full)把文件重写一遍;之后就是 auto_vacuum = 2,增量回收开始生效。tests/storage-auto-vacuum.test.ts 把这两半都钉住了。

我没有加自动转换:完整 VACUUM 是 O(库大小) 的独占重写,#4612 已经把它定为大库上的离线操作,并且在另一个存活进程持有该数据库时会拒绝启动。

想请维护者定:要不要提示已有安装的用户?下面两种我都可以在本 PR 或后续 PR 里加,看你们倾向哪种:

  1. storage gc --vacuum incremental 在 PRAGMA auto_vacuum 读出 0 时打印一行("this database predates incremental auto-vacuum; run one --vacuum full while no other instance is using it")。
  2. 在 README 存储一节(en / zh-CN / ja)--vacuum incremental 那条建议旁边加一句。

行为变化说明

行为 旧 新 原因
新建 index.db 的 PRAGMA auto_vacuum 0 (NONE) 2 (INCREMENTAL) 这条 pragma 现在在第 1 页存在之前执行
在新建的库上 gc({ vacuum: "incremental" }) 空操作 交还空闲页,文件变小 和文档一致
同一操作在旧版本创建、还没跑过完整 VACUUM 的库上 空操作 跑一次完整 VACUUM 之前仍是空操作 不迁移,见上文
新建库的写路径 — SQLite 维护 pointer-map 页(auto_vacuum 的标准开销) #3901 选的这个模式本来就带这项开销

改动

  • src/storage/schema.ts:PRAGMA auto_vacuum = INCREMENTAL 挪到 storagePragmas() 的第一位,加注释说明它为什么必须留在第一位、在已有文件上会怎样。
  • src/storage/store.ts:gc() 的文档注释原来说 auto_vacuum "handles freed pages incrementally",改成实际发生的情况(只改注释)。
  • tests/storage-auto-vacuum.test.ts:4 个用例。
    • pragma 顺序(auto_vacuum 在 journal_mode 之前);
    • 经 UnifiedStore.open 打开的新 index.db 读出 auto_vacuum = 2;
    • 写入 → 删除 → gc({ vacuum: "incremental" }) 之后空闲页链表清空,文件缩到不到一半;
    • 按旧方式创建的文件经过打开 + 增量 gc 仍是 0,跑一次 vacuum("full") 后变成 2。

验证

  • 前三个用例在 master 上失败、修复后通过;第四个两边都通过(它描述的是旧布局)。
  • npm run typecheck、check:unused-exports、check:test-import-graph:均无报错。
  • 本地跑主套件(tests/*.test.ts,Windows / Node 22,网络被沙箱隔离,--test-concurrency=4):11009 个测试,10904 通过,8 失败,30 取消,67 跳过。失败和取消的这 38 个里,31 个出自 6 个文件(负载下对时序敏感),这 6 个文件串行重跑全部通过。仍然失败的 7 个都和本机环境有关(本机安全软件拦截回环 TLS、本机不在 UTC、沙箱挡了 DNS 和外网),所在代码本 PR 都没碰;我没有在 master 上跑全量做对照。以推上去的 head 上的 CI 为准(§7.2)。
  • 没跑:mysql 后端(没动)、Linux / macOS(交给 CI)。

相关:#3463、#3901、#3911、#4612、#5599。

…eally gets INCREMENTAL

SQLite only honours PRAGMA auto_vacuum while the database file is still
empty. storagePragmas() switched to WAL first, which already writes
page 1, and issued auto_vacuum = INCREMENTAL last, so every newly created
index.db kept auto_vacuum = 0. PRAGMA incremental_vacuum
(gc({vacuum:"incremental"}), `bili storage gc --vacuum incremental`,
?vacuum=incremental) then reclaimed nothing: the freelist only grew and
the file never shrank without the offline full VACUUM.

- storagePragmas(): auto_vacuum is now the first pragma, with a comment
  on why it has to stay there.
- gc() doc comment: describes what incremental does and when it is a
  no-op (comment only).
- tests/storage-auto-vacuum.test.ts: pragma order; a new index.db reads
  auto_vacuum = 2; fill, delete, gc(incremental) empties the freelist and
  shrinks the file; a file created the old way stays at 0 until one full
  VACUUM converts it.

Existing databases are not migrated: on a non-empty file the pragma only
takes effect at the next full VACUUM.
Copilot AI balanced review requested due to automatic review settings November 12, 2026 00:04
@ranxianglei ranxianglei changed the title fix(storage): 把 auto_vacuum 挪到 journal_mode 之前,新建的 index.db 才真正是 INCREMENTAL [PR] fix(storage): move auto_vacuum before journal_mode so that newly created index.db is truly INCREMENTAL | fix(storage): 把 auto_vacuum 挪到 journal_mode 之前,新建的 index.db 才真正是 INCREMENTAL Nov 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request due to rate limiting. Please try again later. See Usage limits for GitHub Copilot for details.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants