19 预算是内存不是时间
✅ 已验证 · 探针 probes/probe_stroke_budget.py · probe_throughput_tree.py
先量出来的数
单层空白 8192×12288 文档(probe_stroke_budget.py):
| 操作 | 吞吐 |
|---|---|
paintLine(长约 300px) | 450–850 笔/秒,size 8/32/120/400 无差别 |
paintPath(8 点曲线) | 310–390 笔/秒 |
setForeGroundColor + paintLine,不 pump | 605 笔/秒 |
set_color()(带 pump(1))+ paintLine | 71 笔/秒——8.5 倍的税 |
成本与笔刷大小无关:大笔和小笔一样贵。换色换笔径不需要 pump()(连画 24 个色块不跑 event loop,读回来正好 24 种),Session.brush() 就是这条快路径。
真实图层树上的数
上面的数字骗了我一次。真实作业有 307 个带 clip 的节点,第一次跑:
20000 笔 187 笔/秒
60000 笔 121
100000 笔 99 ← 还在掉, Krita RSS 已经 18.6 GB
probe_throughput_tree.py 在同构的树上分开量:
| 画法 | 笔/秒 | 每 12000 笔的 RSS 增量 |
|---|---|---|
单层 · stroke() 逐段 paintLine | 140 | +2.8 GB |
clip 树 · stroke() 逐段 paintLine | 130 | +3.9 GB |
clip 树 · path() 一次 paintPath | 297 | +2.8 GB |
同上 + Document.setBatchmode(True) | 291 | +2.3 GB |
三个结论:
- 图层树只贵 7%(140 → 130),不是主因
- 调用次数是主因:
paintPath一次调用比逐段paintLine快 2.3 倍 - 每次落笔调用约吃 233 KB 撤销栈,
setBatchmode无效,Python API 里没有清撤销栈的接口(只有Krita.setBatchmode/Document.setBatchmode,都不管撤销)
所以预算是内存
第一版规划了 241,162 笔。每笔平均 3.5 段,stroke() 逐段调 → 845,060 次调用 → 60 GB。本机 46 GB,会静默 OOM(Krita 被 OOM killer 干掉,一句话都不留)。
预算:46 GB − 基础 5 GB − 余量 ≈ 11 万次调用。
三件事把调用数压下来:
| 做法 | 效果 |
|---|---|
不收尖的笔走 path()(一次 paintPath) | 调用数 = 笔数,而不是段数 |
| 落盘前 RDP 简化点列 | detail 从 3.4 段/笔降到约 1.2 |
strand 点列砍到 ≤ 9 点 | 从 34.8 段/笔降到 8 |
现在:80,669 笔 → 99,755 次调用 → 撤销栈约 23 GB,217 秒,371 笔/秒。
trace_prepare.py 落盘时会自己算调用次数和内存估计,超过 11 万次打警告。调 ART_DETAIL_T(越大越少笔)来控。
什么时候需要逐段 paintLine
只有需要收尖的笔——pressure 是逐段给的,paintPath 全程一个压力。发丝、排线、线稿(如果收尖)走 stroke(pts, pressures),其余全部 path(pts)。
离屏画布不是出路
Scratchpad 是文档之外的一块画布,本来指望它的落笔不进文档撤销栈。实测它上面
根本没有程序化落笔的 API,只有 fill* 和 loadScratchpadImage——paintEngine /
paintEvent / paintingActive 是 QWidget 自带的,别被名字骗了。省内存这条路不成立
(第 30 章)。
预算仍然只有一个办法:少调用。
旧的坑
layers.md 时代记过 undoStackLimit=3 这个 kritarc 配置。它对 paint* 的撤销栈无效——第一版跑到 18.6 GB 就是在这个配置下。kritarc 里额外的内存相关键实测会直接搞坏绘画,保持最小配置。