Vyntek

跨层配额总账: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 · 查看完整源文件

学习路线

  1. 先立口径(本节表格):分不清 bytes/refs/slots,后面的数字都会读错。
  2. 看快照怎么用(片段 1、3):层读数要判 available;快照是只读的,且要过 Consistent()。
  3. 看一个周期怎么走(片段 2、4):泵到静止 → 采样 → 停流 → Reset,顺序不能换。
  4. 看判定(片段 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_demo

Windows(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 ok
  • 1 —— ImageBuffer::Rent 失败(池预算或分配失败)
  • 2 —— Arm / Start / TryPostOwned 失败
  • 3 —— Consistent() 失败:口径不变量被破坏(单位错配、字节不守恒、槽位超容)
  • 4 —— 字节未平台:末窗最大值超过首窗最大值
  • 5 —— 未回到基线 / 会话未清空(Settle 泵不干净)

On this page