qubit-progress

Generic progress reporting abstractions for Qubit Rust libraries

Rust CI Coverage Crates.io Rust License

面向 Rust 生态的通用进度汇报抽象。

什么时候使用

当一个操作需要汇报进度,但又不希望进度 API 绑定到某个具体业务领域时,可以使用 qubit-progress

这个 crate 不是调度器、任务执行器或 UI 框架。它只定义进度事件模型和基础 reporter。

概览

Qubit Progress 将进度建模为不可变事件。一个进度事件本身包含:

除此之外,这个 crate 还提供:

ProgressReporter 是一个刻意保持轻量的 trait:若内置 reporter 不够用,业务系统可以按自身需求实现自定义 reporter,例如在图形界面上驱动进度条、在桌面应用的状态区刷新进度,或向 Web 前端转发进度更新。本 crate 不绑定任何 UI 或传输层;把事件接到具体框架或通道上由你的集成代码完成。

业务 crate 应保留自己的领域状态,并在汇报进度时转换成 ProgressEvent。领域错误、日志、指标和链路追踪应使用各自机制,不应附加到进度事件上。

安装

[dependencies]
qubit-progress = "0.4"

快速开始

use std::time::Duration;

use qubit_progress::{
    ProgressEvent,
    ProgressReporter,
    StdoutProgressReporter,
};

let reporter = StdoutProgressReporter::default();
let event = ProgressEvent::builder()
    .running()
    .total(4)
    .completed(2)
    .active(1)
    .stage_named("copy", "Copy files")
    .elapsed(Duration::from_secs(2))
    .build();

reporter.report(&event);

当调用方已经有完整的 counters 或 stage 元数据时,底层构造器仍然可用;普通进度汇报 代码优先使用 builder。

汇报一次操作的生命周期

当一个操作需要 started、周期性 running 和终态事件时,使用 ProgressProgress 负责记录耗时并对 running 回调做时间间隔节流;业务代码仍然维护自己的状态,并在汇报时转换成 ProgressCounters

use std::time::Duration;

use qubit_progress::{
    ProgressCounters,
    Progress,
    StdoutProgressReporter,
};

let reporter = StdoutProgressReporter::default();
let mut progress = Progress::new(&reporter, Duration::from_secs(5));

let started = progress.report_started(ProgressCounters::new(Some(3)));
assert!(started.elapsed().is_zero());

let mut completed = 0;
for _task in 0..3 {
    // ... 执行一个工作单元 ...
    completed += 1;
    let counters = ProgressCounters::new(Some(3)).with_completed_count(completed);
    let _running_event = progress.report_running_if_due(counters);
}

let final_counters = ProgressCounters::new(Some(3))
    .with_completed_count(3)
    .with_succeeded_count(3);
let finished = progress.report_finished(final_counters);
assert!(finished.elapsed() >= started.elapsed());

report_running_if_due 只有在真正发出事件时才返回 Some(event)。该方法不会阻塞等待 下一次汇报周期:未到期时会立即返回 None,到期时会同步调用 reporter 并返回刚发出的 事件(因此是否阻塞取决于 reporter 本身的实现)。实战中常见写法是:每完成一个工作 单元就调用一次;到汇报间隔时会自动发出事件,未到间隔时这次调用基本没有效果。如果 外部调度器或后台线程已经控制了汇报间隔,可以直接调用 report_running

在后台线程中汇报 running 进度

当工作在一个或多个 worker 线程中执行,但 running 事件希望放到单独的后台汇报线程中 时,使用 Progress::spawn_running_reporter。worker 继续维护自己的业务状态;后台 汇报线程只负责等待超时或 RunningProgressPointHandle::report 信号,然后调用你提供 的快照闭包,把当前业务状态转换为新的 ProgressCounters

这种模式适合并行执行器:正数汇报间隔下,worker 的热路径不需要直接调用 reporter; 当间隔是 Duration::ZERO 时,又可以在每个 worker 进度点之后唤醒后台循环,而且不会 忙等。协调线程持有 RunningProgressGuard guard,worker 只拿 RunningProgressPointHandle,结束时由协调线程调用 stop_and_join

下面的示例用 qubit-atomicAtomicCount 作为跨线程共享的完成计数。只有在你采用这一写法时,才需要在 Cargo.toml 里增加对应依赖:

qubit-atomic = "0.10"
use std::{
    sync::Arc,
    thread,
    time::Duration,
};

use qubit_atomic::AtomicCount;
use qubit_progress::{
    Progress,
    ProgressCounters,
    StdoutProgressReporter,
};

let reporter = StdoutProgressReporter::default();
let completed = Arc::new(AtomicCount::zero());

thread::scope(|scope| {
    let loop_completed = Arc::clone(&completed);
    let progress = Progress::new(&reporter, Duration::ZERO);
    let running_progress =
        progress.spawn_running_reporter(scope, move || {
            ProgressCounters::new(Some(3))
                .with_completed_count(loop_completed.get())
        });
    let progress_point_handle = running_progress.point_handle();

    let mut handles = Vec::new();
    for _ in 0..3 {
        let completed_worker = Arc::clone(&completed);
        let point = progress_point_handle.clone();
        handles.push(scope.spawn(move || {
            completed_worker.inc();
            point.report();
        }));
    }
    for handle in handles {
        handle.join().unwrap();
    }

    running_progress.stop_and_join();
});

如果汇报间隔是正数,RunningProgressPointHandle::report 是 no-op;后台汇报线程会 通过超时等待定时醒来。这样 worker 可以无条件调用它,同时 stop/join 仍由 协调线程持有的 guard 负责。

阶段化进度

ProgressStage 描述操作处于哪个业务阶段。它和生命周期阶段不同:复制文件这个 stage 可以处于 running、finished、failed 或 canceled 等 phase。

use std::time::Duration;

use qubit_progress::{
    ProgressEvent,
    ProgressPhase,
    ProgressStage,
};

let stage = ProgressStage::new("verify", "Verify installation")
    .with_index(2)
    .with_total_stages(5)
    .with_weight(0.2);

let event = ProgressEvent::builder()
    .phase(ProgressPhase::Running)
    .total(10)
    .completed(7)
    .elapsed(Duration::from_secs(12))
    .stage(stage)
    .build();

assert_eq!(event.phase(), ProgressPhase::Running);
assert_eq!(event.counters().completed_count(), 7);

计数语义

ProgressCounters 支持已知总量和未知总量两类进度:

当总量已知且为零时,进度被视为完成:

use qubit_progress::ProgressCounters;

let counters = ProgressCounters::new(Some(0));
assert_eq!(counters.progress_percent(), Some(100.0));

Reporter 行为

Reporter 本质上会产生副作用。它可以写终端、写文件、发日志、更新 UI 桥接层, 也可以在测试中记录事件。如果 reporter panic,调用方自行决定传播还是隔离。

WriterProgressReporter 会写出紧凑的人类可读文本。

StdoutProgressReporterStderrProgressReporter 是基于 WriterProgressReporter 的便捷 reporter,分别输出到标准输出和标准错误。

LoggerProgressReporter 通过 log crate 输出,并支持配置 target 和 level。

公共 API 速查

文档

测试和 CI

在 crate 根目录运行快速本地检查:

cargo test
cargo clippy --all-targets -- -D warnings

要尽量匹配仓库 CI 环境,运行:

./align-ci.sh
./ci-check.sh
./coverage.sh json

./align-ci.sh 会同步本地工具链和 CI 相关配置;./ci-check.sh 运行与流水线一致的 检查。修改需要覆盖率体现的行为时,可以运行 ./coverage.sh

贡献

欢迎 issue 和 pull request。请保持改动聚焦;行为变化时补充或更新测试;公共 API 或 用户可见行为变化时同步更新 README 或 rustdoc。

贡献代码即表示你同意贡献内容使用同一份 Apache License, Version 2.0 许可。

许可和版权

Copyright © 2026 Haixing Hu, Qubit Co. Ltd.

本软件使用 Apache License, Version 2.0 许可。

作者和维护

胡海星 — Qubit Co. Ltd.

源码仓库github.com/qubit-ltd/rs-progress
API 文档docs.rs/qubit-progress
Crate 发布crates.io/crates/qubit-progress