diff --git a/core/src/task/download_task.cpp b/core/src/task/download_task.cpp index e471943..4fd4731 100644 --- a/core/src/task/download_task.cpp +++ b/core/src/task/download_task.cpp @@ -1217,6 +1217,25 @@ EngineState DownloadTaskState::snapshot_state() { Progress DownloadTaskState::snapshot_progress() { Progress p; std::lock_guard lk(mu); + // Per-segment instantaneous speed lives on the live SegWorker (seg_data's 0.5s-sampled + // EMA, see the `speed_bps` update below) -- a segment with no live worker (idle, + // paused, complete, failed) has no speed to report and stays at SegmentProgress's + // default 0. Read every live worker's speed up front so the seg->snapshot() loop below + // (which covers *every* segment, not just live ones -- unlike emit_progress_if_due's + // push-callback version, which only ever reports the segments it currently has + // workers for) can look each one up by index. + double agg_speed = 0; + std::unordered_map worker_speed; + { + std::shared_lock wl(workers_mu); + worker_speed.reserve(workers.size()); + for (auto &[idx, w] : workers) { + worker_speed.emplace(idx, w->speed_bps); + agg_speed += w->speed_bps; + } + p.effective_segments = static_cast(workers.size()); + } + p.speed_bps = static_cast(agg_speed); if (seg) { p.downloaded = seg->downloaded(); for (auto &v : seg->snapshot()) { @@ -1226,15 +1245,15 @@ Progress DownloadTaskState::snapshot_progress() { sp.end = v.end; sp.completed = v.completed; sp.state = v.state; + if (auto it = worker_speed.find(v.index); it != worker_speed.end()) + sp.speed_bps = static_cast(it->second); p.segments.push_back(sp); } } p.total = total_size; - { - std::shared_lock wl(workers_mu); - p.effective_segments = static_cast(workers.size()); - } p.effective_buffer_bytes = effective_buffer; + if (p.speed_bps > 0 && total_size && *total_size > p.downloaded) + p.eta_seconds = static_cast((*total_size - p.downloaded) / p.speed_bps); return p; } diff --git a/core/tests/task/engine_test.cpp b/core/tests/task/engine_test.cpp index df687a8..1dadb29 100644 --- a/core/tests/task/engine_test.cpp +++ b/core/tests/task/engine_test.cpp @@ -457,3 +457,33 @@ VT_TEST(engine_content_length_mismatch_fails_honestly) { VT_CHECK(rec.saw(EngineState::failed)); VT_CHECK_EQ(::access(td.file("clm.bin").c_str(), F_OK), -1); // never renamed into place } + +// --- DAEMON-reported bug: Progress.speed_bps reads 0 for the whole life of a live +// download while downloaded bytes visibly advance. DAEMON reads progress by polling +// DownloadHandle::progress() (engine_port_core.hpp), not the on_progress push callback -- +// this exercises exactly that path. --- + +VT_TEST(engine_polled_progress_reports_nonzero_speed) { + TestServer srv; + VT_REQUIRE(srv.available()); + TmpDir td; + Recorder rec; + Engine eng; + auto h = eng.start(spec_for(srv, "/throttled/file/4M", td.file("sp.bin")), rec.cbs()); + + // Give it real, sustained progress: the speed estimate only updates on a >=0.5s + // sample window (seg_data), so a snapshot taken too early would legitimately read 0 + // even with the bug fixed. Poll until downloaded has clearly advanced twice over. + std::uint64_t speed = 0; + for (int i = 0; i < 400 && speed == 0; ++i) { + std::this_thread::sleep_for(20ms); + auto p = h.progress(); + if (p.downloaded >= 256u * 1024) + speed = p.speed_bps; + } + VT_CHECK(speed > 0); + + h.cancel(/*discard_partial=*/true); + auto r = rec.wait(); + VT_REQUIRE(!r.has_value()); +}