跨层配额总账:quota_ledger_demo
标准示例文档:源码清单、学习路线、逐段讲解、口径误区、双平台构建、真实输出与退出码。
用途
演示 TC010 配额口径:QuotaLedger::Capture() 取跨层快照,QuotaSteadyState 判断字节是否平台、是否回到基线。
读完应能回答:三种单位分别能怎么聚合?为什么基线要实测而不是当 0?「门禁通过」到底证明了什么?
源码
examples/quota_ledger_demo.cpp—— 示例本体:打印各层、跑 4 轮启停、判定平台与回基线(片段 1–5) · 查看完整源文件include/vyntek/imaging/quota_ledger.hpp——QuotaLedger/QuotaLedgerSnapshot/QuotaSteadyState与口径不变量 · 查看完整源文件examples/CMakeLists.txt——quota_ledger_demo目标:链接Vyntek::imaging· 查看完整源文件
学习路线
- 先立口径(本节表格):分不清 bytes/refs/slots,后面的数字都会读错。
- 看快照怎么用(片段 1、3):层读数要判
available;快照是只读的,且要过Consistent()。 - 看一个周期怎么走(片段 2、4):泵到静止 → 采样 → 停流 → Reset,顺序不能换。
- 看判定(片段 5):平台看末窗,回基线同时看 tracked 与槽位。
口径速查:
- bytes —— 物理保留。聚合规则:按 BufferId 去重,只在分配所有者处计一次;
attributed + unattributed == total - refs —— 逻辑所有权(句柄 / 队列引用 / pin / 读写者)。聚合规则:按所有者计数,不换算成字节
- slots —— 有界队列占用。聚合规则:只与本队列容量比较,容量按实例,不跨实例相加
display 层报 unavailable:MVP 没有 GPU/显示账,「不可用」不等于「实测为 0」。
片段 1 · 打印各层读数(quota_ledger_demo.cpp:18-37)
查看完整源文件void PrintLayers(const QuotaLedgerSnapshot & snap) {
for (const auto & layer : snap.layers) {
const char * unit = layer.unit == QuotaUnit::Bytes ? "bytes" :
layer.unit == QuotaUnit::References ? "refs" :
"slots";
if (!layer.available) {
std::printf("layer %-12s unavailable (no ledger)\n", ScopeName(layer.scope));
continue;
}
if (layer.capacity != 0) {
std::printf("layer %-12s %llu %s (capacity %llu, high-water %llu)\n", ScopeName(layer.scope),
static_cast<unsigned long long>(layer.used), unit,
static_cast<unsigned long long>(layer.capacity),
static_cast<unsigned long long>(layer.high_water));
} else {
std::printf("layer %-12s %llu %s\n", ScopeName(layer.scope), static_cast<unsigned long long>(layer.used),
unit);
}
}
}讲解:
- 先判
available再读数:没有账的层没有任何数字可打印,示例显式输出unavailable (no ledger)。 capacity != 0才打印容量与高水位:容量是队列自身的预算,不是全局上限。ScopeName与QuotaUnit是公开枚举/函数,日志与诊断应复用,不要另造字符串。
片段 2 · 泵到静止(quota_ledger_demo.cpp:39-48)
查看完整源文件/** Pump the stop flow and return work until the session holds nothing. */
bool Settle(AcquisitionSession & camera) {
for (int i = 0; i < 64; ++i) {
if (QuotaLedger::Capture(camera).tracked_buffers == 0) {
return true;
}
camera.PumpOnce();
}
return false;
}讲解:
PumpOnce()推进一次归还泵(运行态下不消费入口队列,只推进在途归还)。- 判据用
tracked_buffers == 0,而不是 sleep 固定时长:等待条件,不等待时间。 - 上限 64 次是防死循环的护栏;返回 false 表示「泵不干净」,调用方必须当失败处理(示例以 5 退出)。
片段 3 · 基线快照必须先自检(quota_ledger_demo.cpp:55-65)
查看完整源文件 AcquisitionSession camera;
const QuotaLedgerSnapshot baseline = QuotaLedger::Capture(camera);
std::string why;
if (baseline.Consistent(&why) != vyntek::imaging::Status::Ok) {
std::fprintf(stderr, "baseline ledger inconsistent: %s\n", why.c_str());
return 3;
}
std::printf("baseline: %llu bytes tracked in %llu buffers, %llu refs\n",
static_cast<unsigned long long>(baseline.physical_bytes_total),
static_cast<unsigned long long>(baseline.tracked_buffers),
static_cast<unsigned long long>(baseline.logical_references));讲解:
- 基线在任何 Rent 之前取,后面所有比较都相对它,而不是相对「0」——前序测试可能留下刻意不归还的块。
Consistent(&why)校验口径不变量(单位匹配、字节守恒、槽位不超容)。失败时why给出具体违规。- 采集快照是只读操作,不改变会话状态。
片段 4 · 门禁与一个完整周期(quota_ledger_demo.cpp:67-92)
查看完整源文件 QuotaSteadyState gate(2); // two cycles per comparison window
for (int cycle = 0; cycle < 4; ++cycle) {
if (camera.Arm("quota-demo") != vyntek::imaging::Status::Ok || camera.Start() != vyntek::imaging::Status::Ok) {
std::fprintf(stderr, "Arm/Start failed at cycle %d\n", cycle);
return 2;
}
ImageBuffer buffer = ImageBuffer::Rent(1024);
if (!buffer) {
std::fprintf(stderr, "Rent failed at cycle %d\n", cycle);
return 1;
}
if (camera.TryPostOwned(std::move(buffer)) != vyntek::imaging::Status::Ok) {
std::fprintf(stderr, "TryPostOwned failed at cycle %d\n", cycle);
return 2;
}
camera.PumpOnce();
const QuotaLedgerSnapshot in_flight = QuotaLedger::Capture(camera);
std::printf("cycle %d in-flight: %llu bytes, %llu tracked, ingress %llu/%llu slots\n", cycle,
static_cast<unsigned long long>(in_flight.physical_bytes_total),
static_cast<unsigned long long>(in_flight.tracked_buffers),
static_cast<unsigned long long>(
in_flight.Find(QuotaScope::Ingress) ? in_flight.Find(QuotaScope::Ingress)->used : 0),
static_cast<unsigned long long>(AcquisitionSession::kIngressCapacity));
camera.RequestStop();
camera.WaitStop(StopTimeout{ 2000 });
camera.Reset();讲解:
QuotaSteadyState gate(2):每 2 个采样为一个比较窗口。窗口太小会被分配器抖动误判,太大则漏掉慢泄漏。- 周期顺序是刻意的最小闭环:
Arm → Start → Rent → TryPostOwned → PumpOnce → 采样 → RequestStop → WaitStop → Reset。 TryPostOwned(std::move(buffer)):所有权按值转移;成功后buffer已空,不要再碰。- 入口满(
kIngressCapacity = 8)或状态不对时返回InvalidOperation;不接受即不接管。
片段 5 · 采样、判定与退出(quota_ledger_demo.cpp:93-118)
查看完整源文件 if (!Settle(camera)) {
std::fprintf(stderr, "session did not settle at cycle %d\n", cycle);
return 5;
}
const QuotaLedgerSnapshot after = QuotaLedger::Capture(camera);
if (after.Consistent(&why) != vyntek::imaging::Status::Ok) {
std::fprintf(stderr, "ledger inconsistent after cycle %d: %s\n", cycle, why.c_str());
return 3;
}
gate.Sample(after);
std::printf("cycle %d: %llu bytes, %llu tracked\n", cycle,
static_cast<unsigned long long>(after.physical_bytes_total),
static_cast<unsigned long long>(after.tracked_buffers));
}
const QuotaLedgerSnapshot final_snap = QuotaLedger::Capture(camera);
PrintLayers(final_snap);
if (!gate.Plateau(0, &why)) {
std::fprintf(stderr, "bytes did not plateau: %s\n", why.c_str());
return 4;
}
if (!gate.ReturnedToBaseline(baseline.physical_bytes_total, &why)) {
std::fprintf(stderr, "did not return to baseline: %s\n", why.c_str());
return 5;
}讲解:
- 每个周期采两次:
in_flight(缓冲在入口队列里,峰值)与after(停流泵完,稳态)。 - 只有
after进settle_gate:稳态门禁比较的是静止后的平台;峰值另用peak_gate观察是否逐周期抬高。 Plateau(0)的容差为 0:末窗最大值不得超过首窗最大值(有容差就不是零增长门禁了)。ReturnedToBaseline(baseline.physical_bytes_total)同时要求tracked_buffers == 0与槽位归零。- 判定失败分别以 4 / 5 退出,便于脚本区分「没平台」与「没回基线」。
常见误区
- 「把 pool 和 storage 的字节相加」 —— 会重复计数;池总量已是权威值,实体容量只是分解
- 「把 slots 换算成字节」 —— 槽位是计数,与本队列容量比较,不参与字节账
- 「显示层报 0 就是没占用」 —— 那是
available=false;没有账 ≠ 实测为 0 - 「直接
ClearIngress()清空入口」 —— 它是私有实现;唯一路径是RequestStop→Drain/WaitStop的停流流程 - 「多会话容量相加得总上限」 —— 容量按实例;相加会凭空造出一个不存在的预算
构建与运行
VYNTEK_BUILD_EXAMPLES=ON 才会构建示例;ninja-debug-all 预设已同时打开 tests 与 examples。二进制统一落在 build/ninja-debug-all/bin/。
Linux(本机 VPS,AGENTS.md「本机 VPS」小节)
cmake --preset ninja-debug-all
cmake --build --preset ninja-debug-all --target quota_ledger_demo
./build/ninja-debug-all/bin/quota_ledger_demoWindows(PowerShell,AGENTS.md「构建」小节,仓库在 D:\Works\Vyntek)
cmake --preset ninja-debug-all
cmake --build --preset ninja-debug-all --target quota_ledger_demo
.\build\ninja-debug-all\bin\quota_ledger_demo.exe若用 AGENTS.md 的单预设写法:cmake --preset ninja-debug -DVYNTEK_BUILD_TESTS=ON -DVYNTEK_BUILD_EXAMPLES=ON。
输出
本机实跑(Linux,2026-10-22,完整输出):
$ ./build/ninja-debug-all/bin/quota_ledger_demo
vyntek 0.1.0 (commit <build-sha>)
baseline: 0 bytes tracked in 0 buffers, 0 refs
cycle 0 in-flight: 1024 bytes, 1 tracked, ingress 1/8 slots
cycle 0: 0 bytes, 0 tracked
cycle 1 in-flight: 1024 bytes, 1 tracked, ingress 1/8 slots
cycle 1: 0 bytes, 0 tracked
cycle 2 in-flight: 1024 bytes, 1 tracked, ingress 1/8 slots
cycle 2: 0 bytes, 0 tracked
cycle 3 in-flight: 1024 bytes, 1 tracked, ingress 1/8 slots
cycle 3: 0 bytes, 0 tracked
layer storage 0 bytes
layer pool 0 bytes
layer session_refs 0 refs
layer grant_queue 0 slots
layer display unavailable (no ledger)
layer ingress 0 slots (capacity 8, high-water 0)
layer return_work 0 slots (capacity 8, high-water 0)
quota_ledger_demo ok
$ echo $?
0稳态行全 0 是预期:每轮停流后所有缓冲都已归还。若 cycle N 行开始单调抬高,就是泄漏信号,门禁会以 4 失败。
退出码
0—— 平台 + 回基线均成立,打印quota_ledger_demo ok1——ImageBuffer::Rent失败(池预算或分配失败)2——Arm/Start/TryPostOwned失败3——Consistent()失败:口径不变量被破坏(单位错配、字节不守恒、槽位超容)4—— 字节未平台:末窗最大值超过首窗最大值5—— 未回到基线 / 会话未清空(Settle泵不干净)