# 索引性能验证 开发版和基准必须使用相同的 Go 编译参数。开发构建使用 `-gcflags="pvfine/...=-l"`,仅关闭项目代码的内联;SQLite 和标准库保持优化。 原来的 `-gcflags=all=-l` 会同时关闭纯 Go SQLite 及其依赖的内联,不能与 默认 `go test` 的耗时直接比较。 ## 真实归档强制重建 在仓库根目录运行,`PVF_TESTFILE` 可替换为待测文件: ```sh PVF_MEMORY_TEST=1 PVF_REBUILD_TEST=1 PVF_TESTFILE="$PWD/testdata/90CN.pvf" \ go test -gcflags='pvfine/...=-l' ./services \ -run '^TestLargeArchiveMemory$' -count=1 -v ``` 若要复现旧开发版的性能问题,仅将编译参数替换为 `-gcflags=all=-l`。 两种测试应顺序运行,避免相互竞争 CPU 和磁盘。测试末尾的 GC 用于单独测量 存活堆;`forced_rebuild` 不包含这次 GC。 2026-09-21 在同机、同一份 1,057,873 文件归档和同一份索引实现上的各一次结果: | 指标 | 原开发参数 `all=-l` | 修正参数 `pvfine/...=-l` | | --- | ---: | ---: | | 强制重建总耗时 | 54.34 s | 10.41 s | | 语义加载 | 11.525 s | 3.790 s | | 文件补齐 | 19.424 s | 2.499 s | | 创建搜索索引 | 18.907 s | 2.211 s | | GC 后存活堆 | 437.5 MiB | 437.5 MiB | | 测试进程累计堆分配 | 32,906.6 MiB | 4,521.7 MiB | 这组结果用于定位编译参数造成的性能差异,不代表三次冷/热缓存验收、 桌面 WebView 物理内存测量或所有平台的性能保证。累计分配不是峰值或物理内存。 修改 Taskfile 后需重新构建并重启开发应用;仅刷新前端不会替换 Go 进程。 可用 `go version -m bin/pvfine` 检查二进制中的 `-gcflags`。 强制重建仍重新解析语义并构建候选数据库,不通过缓存命中跳过重建。 ## 110US 全量重建优化(2026-10-13) 归档包含 4,311,296 个文件。用户提供的开发应用日志中,强制重建耗时 202.29 秒。优化后使用同一份本地归档,按开发配置关闭项目代码内联, 顺序运行三次;每次创建新的 Core、打开已有完整文件索引缓存,再执行强制 重建。重建仍读取脚本元数据,不通过语义缓存跳过重建;操作系统缓存未清空。 ```sh PVF_MEMORY_TEST=1 PVF_REBUILD_TEST=1 PVF_TESTFILE="$PWD/testdata/110US.pvf" \ go test -gcflags='pvfine/...=-l' ./services \ -run '^TestLargeArchiveMemory$' -count=3 -timeout=15m -v ``` 三次强制重建:**72.09 / 72.11 / 72.47 秒**,中位数 **72.11 秒**。 | 阶段 | 用户原日志 | 优化后三次中位数 | | --- | ---: | ---: | | 候选库准备(复制文件投影) | 10.05 s | 16.36 s | | 清理旧语义数据 | 28.50 s | 0.00 s | | 语义加载及关联写入 | 115.99 s | 31.51 s | | 建立文件关联索引 | 2.10 s | 1.24 s | | 普通文件补齐 | 21.85 s | 0.00 s | | 建立搜索及依赖索引 | 23.49 s | 22.36 s | 旧路径先复制整库,再删除旧语义及普通文件重复记录;新候选库只复制 `meta/files/dirs` 并创建空语义表。未登记文件直接从 `files` 查询,以两个有序 数据流分页,省去 3,674,085 条重复记录。语义依赖按表编号缓存路径、按文件 去重,每 128 条使用预编译 SQL 批量写入;全量加载后统一建立依赖索引。 增量刷新复用相同依赖写入器,但保留已有索引。 三次均得到 638,532 条登记记录、637,211 条 visuals、979 个跳过条目, 与用户日志的对应计数一致,元数据解析错误均为零。新增回归覆盖虚拟普通 文件搜索、跨数据流分页、多列表登记、取消/恢复登记、WAL 中已提交文件投影、 批量尾部依赖,以及关闭期间后台重试;全量 Go 测试和相关竞态测试通过。 发布仍校验归档代际与修订,完整候选事务提交后才替换活动索引。 相较用户日志,中位耗时约减少 64%。原数据为应用日志,新数据为服务层测试, 没有对原二进制做相同缓存条件下的三次重跑,因此这是日志对照,不是严格 控制变量的提速倍数。重建计时不含归档打开、最后的 GC 和 WebView。 新 `create-indexes` 阶段也包含依赖索引建立,阶段内部工作量与旧路径不同。 剩余主要成本是文件投影复制、字符串依赖写入和索引排序。 文件索引 schema 升级为 5,并使用新的缓存身份;旧缓存需要首次建库,之后 可复用。重新构建并重启 Go 应用进程才能启用实现;仅刷新前端不会生效。 ## SQLite 语义增量刷新 2026-10-13,新增 `BenchmarkSQLiteSemanticRefresh`,在合成归档中创建 100,000 个装备脚本及一个登记列表。初次建库不计时;每次改变一个脚本名称, 对比完整语义构建与增量刷新。两种模式使用同一个测试可执行文件和默认 Go 编译参数,各运行三次,每次测量一次刷新。 ```sh go test ./services -run '^$' -bench '^BenchmarkSQLiteSemanticRefresh$' \ -benchtime=1x -count=3 ``` | 模式 | 三次耗时 | 中位数 | | --- | --- | --- | | 完整语义构建 | 1003.4 / 984.4 / 982.6 ms | 984.4 ms | | 增量刷新 | 184.0 / 179.7 / 179.1 ms | 179.7 ms | 这组数据约为 5.5 倍差异。测量包含增量暂存克隆、登记读取和 SQL 发布, 不包含初次建库、实际编辑动作、前端桥接和文件投影更新。合成脚本的未修改 内容为空,不能视为真实百万文件归档或完整用户操作的耗时保证。 普通修改保留未受影响的语义行和数据库索引;首次建库、强制重建和修复仍 允许全量构建。删除仍需同步文件编号映射,并不是常数时间操作。 ## 高级字符串搜索(SQLite) 高级字符串搜索统一使用磁盘反向索引,小归档也不再构建服务层的全量 `StringPoolIndex`。字符串池值只存一份,引用表保存真实池偏移、文件编号、 出现次数、token 类型和文件表字段。普通子串仍使用 Go 的 Unicode 小写规则, 正则仍使用 Go regexp。查询只扫描池记录,不重复解压归档;按路径、文件编号 排序的结果文件编号另存会话库,详情按当前页关联读取。 - 首次使用才建库。构建分批提交,解码时只保留当前块和当前文件的引用。 - 干净归档以加载内容的 SHA-256 和索引版本作为缓存键,缓存命中执行完整性检查。 索引放在用户缓存目录 `pvfine/advanced-search`,历史缓存预算 2 GiB。 - v4 缓存使用不可变文件及会话硬链接,历史缓存淘汰不会删除活动会话使用的索引。 不支持硬链接的文件系统仍可使用会话索引,但不能跨打开复用。 - 未保存归档只使用会话库,不发布干净缓存。编辑、结构变化和版本切换使查询 结果及游标失效,已完成的引用底库保留。下一次搜索仅解析新增/修改文件, 通过私有覆盖表和编号映射屏蔽旧引用;引用分片不重写。池字符串只追加新增 部分,已有占位符重新解析。池前缀改变时才回退到完整构建。 - 首次更新只复制元数据库,不复制引用分片;保存后追平的覆盖结果可发布 新缓存。可变元数据库使用 backup 快照,引用分片继续使用不可变硬链接。 - 查询会话最多 8 个,结果库合计预算 256 MiB,淘汰会删除实际文件;单次查询 超过可用预算会明确报错。游标包含查询身份,旧游标不能用于新查询或新归档。 - 取消按钮、归档关闭和编辑会取消旧任务并异步释放连接及会话目录;干净缓存保留。 - 这次迁移针对字符串模式。二进制搜索沿用现有实现。 复现: ```sh PVF_MEMORY_TEST=1 PVF_ADVANCED_TEST=1 PVF_TESTFILE="$PWD/testdata/90CN.pvf" \ go test -gcflags='pvfine/...=-l' ./services \ -run '^TestLargeArchiveMemory$' -count=1 -v ``` 2026-09-21,同机顺序运行、每场景一次,查询 `npc`,每页 200 个文件: | 指标 | 测量值 | | --- | ---: | | 池字符串数 | 1,643,770 | | 文件与池偏移引用数 | 26,804,667 | | 首次建库 | 76.7 s | | 反向索引文件 | 1,162,321,920 字节(约 1.08 GiB) | | 已有缓存的完整性检查及打开 | 6.9 s | | 已有缓存的首次查询(包含检查) | 7.6 s | | 首次建库之后,普通查询生成结果 | 约 0.8 s | | Go 正则查询生成结果 | 约 1.1 s | | 首次建库后连续 10 页,端到端耗时范围 | 8–71 ms | | 高级搜索后 GC 存活堆 | 约 364 MiB | 这些是服务层真实归档测量,未包含 WebView 物理内存,不是跨机器性能承诺。 终端日志使用 `[pvfine:advanced]`,仅记录条数、容量和耗时,不记录池内容或查询文本。 ## 元数据提取与按需图标合批(2026-10-14) `ScriptMetadata` 保留配对/非配对 section、嵌套层级和文档顺序语义, 只收集名称、图标、场景图、稀有度和角色名称所需的值。名称只保存首个有效 字符串,图标只保存首个有效相邻路径/帧编号对,其余目标最多保存首个 token; 无关 section 的值不解码。每次提取按偏移复用标签解析结果,不建立跨编辑的 标签缓存,也不修改字符串池。 Apple M4 Max、默认 Go 编译参数下,同一合成脚本分别在修改前后运行三次: ```sh go test ./internal/pvf -run '^$' -bench '^BenchmarkScriptMetadataLargeScript$' \ -benchmem -count=3 ``` | 指标 | 修改前 | 修改后 | | --- | ---: | ---: | | 单次耗时(三次中位数) | 220,775 ns | 42,124 ns | | 单次分配字节(三次中位数) | 769,279 B | 28,208 B | | 单次分配次数 | 814 | 23 | 样本含 100 个无关 section,每个含 40 个字符串及 40 个整数,并带名称、 图标、稀有度字段。建样本不计时,测量为重复读取内存脚本的热路径,不包含 真实归档打开、分块解压、本地化首次加载、SQLite 写入或 WebView。因此约 5.2 倍的提取差异不能解释为真实归档整体重建的提速倍数。 图标仍按实际请求加载。前端等待 8 ms 合并请求,每批最多 128 帧、最多 两个桥接批次同时执行。后端按 NPK 分组、按 IMG 排序,同批每个 NPK 只打开 一次,每个 IMG 只解析一次帧表;整个服务最多同时处理四个 NPK 解码任务。 每个任务只保留当前 IMG 帧表。重复路径别名按索引中的实际成员路径去重, 错误按帧返回,资源代际或快照变化后丢弃旧结果。旧 `GetImage` 入口复用相同 并发限制和缓存,绑定由 Wails 重新生成。 合成回归覆盖两个 NPK、三个 IMG、多帧、重复别名及无效引用的混合请求, 验证首次请求恰好打开两个 NPK、解析三个帧表,已缓存帧的重复请求不执行 IO。 另覆盖损坏成员隔离、过期代际、读取中切换代际、跨请求并发上限,以及前端 合批、排队、缓存、重试和迟到响应失效。这些是行为与读取次数验证,尚未测量 真实 NPK 目录下的滚动延迟或物理内存变化。 ## 高级字符串搜索:worker 解析工作区复用(2026-10-14) 引用建库的每个 worker 独占一个 `StringReferenceWorkspace`,复用偏移去重 map、引用数组及 token 类型、文件字段小切片。返回结果为借用切片,对应 SQLite 分片 writer 必须同步消费;写入完成后 channel 只传文件信息和引用 数量,避免下一文件覆盖尚未读取的结果。原有 `StringReferenceInput.References` 继续返回独立结果,扫描器及增量调用的结果生命周期不变。 工作区超过 32,768 条引用容量时,在消费后立即释放;这个限制只控制后续保留 的临时容量,不截断大文件引用。正常工作区清理逻辑状态后复用,错误、取消 及 worker 退出仍遵循原来的任务取消协议,不复用可能被其他输入持有的解压块。 在 Apple M4 Max、36 GiB 内存、macOS 26.6、Go 1.25.8 darwin/arm64 上, 使用 4,311,296 文件的 110US 归档及开发编译参数顺序测试。实施前与复用原型 各运行三轮;正式实现再复测一轮,均绕过索引缓存,未清空操作系统缓存。 | 指标 | 实施前(三轮中位数) | 正式实现(一次复测) | | --- | ---: | ---: | | 完整高级字符串索引建库 | 103.68 s | 94.14 s | | 累计分配 | 78.75 GiB | 57.55 GiB | | 分配次数 | 1,025,812,242 | 756,516,298 | | 文件与字符串偏移引用数 | 122,196,685 | 122,196,685 | 累计分配减少约 21.2 GiB(26.9%),分配次数减少约 26.3%。实施前耗时为 110.61 / 103.68 / 96.30 秒,复用原型为 95.26 / 96.79 / 98.32 秒;原型测试 之后补跑实施前代码得到 95.45 秒。因此耗时差异仍可能来自缓存及环境波动, 本次优化不承诺稳定的完整建库提速。累计分配不是峰值、存活堆或 RSS,不能 据此声称应用物理内存下降 26.9%。 此前独立解压及引用扫描探针在同一真实归档上交替运行现有/原型各三轮: 中位耗时 16.07 / 14.18 秒,累计分配 40.97 / 19.77 GiB,GC 次数 24 / 11。 该探针包含解压、解析、排序及计数,不含 SQLite、归档打开或文件路径投影, 不能把扫描提速直接视为完整建库提速。 真实归档逐文件校验全部引用的偏移、出现次数、token 类型顺序和字段标记, 与独立结果解析一致。新增回归覆盖同偏移多字段、类型顺序、尾部不完整 token、 跨文件复用、大容量回收、解析中取消、同步 writer 消费及阻塞 writer 取消。 全量 Go 测试、相关竞态检查及前端构建通过。 真实归档一致性验证(未设置 `PVF_REFERENCE_TEST=1` 时跳过): ```sh PVF_REFERENCE_TEST=1 PVF_TESTFILE="$PWD/testdata/110US.pvf" \ go test -gcflags='pvfine/...=-l' ./internal/pvf \ -run '^TestStringReferenceWorkspaceRealArchive$' -count=1 -v -timeout=5m ``` 完整建库复测: ```sh PVF_TESTFILE="$PWD/testdata/110US.pvf" \ go test -gcflags='pvfine/...=-l' ./services -run '^$' \ -bench '^BenchmarkAdvancedIndexRealArchive$' -benchtime=1x -count=1 \ -benchmem -timeout=15m ``` 该基准只在内存中追加一个未引用的池字符串来阻止缓存复用,不保存归档。 建库计时不含归档打开、缓存恢复校验和关闭清理;缓存校验时间单独报告。