Files
90CNServer/pvf/docs/索引性能验证.md
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

256 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 索引性能验证
开发版和基准必须使用相同的 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
```
该基准只在内存中追加一个未引用的池字符串来阻止缓存复用,不保存归档。
建库计时不含归档打开、缓存恢复校验和关闭清理;缓存校验时间单独报告。