From 6448f0926519fa599351f94949596e664dc92bf7 Mon Sep 17 00:00:00 2001 From: David Leong <116610336+leongdl@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:36:16 -0700 Subject: [PATCH] fix(cli): Defer zero-duration chunk adjustment Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com> --- crates/openjd-cli/src/run/execution.rs | 55 +++++++++++++++++++++----- specs/cli/run.md | 22 ++++++----- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/crates/openjd-cli/src/run/execution.rs b/crates/openjd-cli/src/run/execution.rs index fbbadcb5..889a1f19 100644 --- a/crates/openjd-cli/src/run/execution.rs +++ b/crates/openjd-cli/src/run/execution.rs @@ -537,6 +537,28 @@ fn adaptive_target_runtime(step: &Step, is_adaptive: bool) -> f64 { .unwrap_or(0.0) } +fn calculate_adaptive_chunk_size( + current_chunk_size: usize, + completed_task_count: usize, + completed_task_duration: f64, + target_runtime_seconds: f64, +) -> Option { + if completed_task_count == 0 + || completed_task_duration <= 0.0 + || !completed_task_duration.is_finite() + { + return None; + } + + let duration_per_task = completed_task_duration / completed_task_count as f64; + let mut adaptive_chunk_size = target_runtime_seconds / duration_per_task; + if completed_task_count < 10 && adaptive_chunk_size > current_chunk_size as f64 { + adaptive_chunk_size = 0.75 * current_chunk_size as f64 + 0.25 * adaptive_chunk_size; + } + + Some((adaptive_chunk_size as usize).max(1)) +} + fn adjust_adaptive_chunk_size( ctx: &RunContext, iter: &mut openjd_model::StepParameterSpaceIterator, @@ -544,16 +566,16 @@ fn adjust_adaptive_chunk_size( completed_task_duration: f64, target_runtime_seconds: f64, ) { - let duration_per_task = completed_task_duration / completed_task_count as f64; - let mut adaptive_chunk_size = target_runtime_seconds / duration_per_task; - if completed_task_count < 10 { - let current = iter.chunks_default_task_count().unwrap_or(1) as f64; - if adaptive_chunk_size > current { - adaptive_chunk_size = 0.75 * current + 0.25 * adaptive_chunk_size; - } - } + let current_chunk_size = iter.chunks_default_task_count().unwrap_or(1); + let Some(adaptive_chunk_size) = calculate_adaptive_chunk_size( + current_chunk_size, + completed_task_count, + completed_task_duration, + target_runtime_seconds, + ) else { + return; + }; - let adaptive_chunk_size = (adaptive_chunk_size as usize).max(1); if Some(adaptive_chunk_size) != iter.chunks_default_task_count() { println!( "{}\tAdjusting chunk size to {adaptive_chunk_size}", @@ -603,3 +625,18 @@ fn report_result(ctx: &mut RunContext, args: &RunArgs, job: &Job, working_dir: & }; crate::common::print_cli_result(&result, &args.output); } + +#[cfg(test)] +mod tests { + use super::calculate_adaptive_chunk_size; + + #[test] + fn zero_duration_defers_adaptive_chunk_adjustment() { + assert_eq!(calculate_adaptive_chunk_size(1, 1, 0.0, 60.0), None); + } + + #[test] + fn measurable_duration_preserves_early_sample_blending() { + assert_eq!(calculate_adaptive_chunk_size(1, 1, 1.0, 9.0), Some(3)); + } +} diff --git a/specs/cli/run.md b/specs/cli/run.md index 0ed6a2c8..4c91e473 100644 --- a/specs/cli/run.md +++ b/specs/cli/run.md @@ -199,17 +199,21 @@ dynamically adjust chunk sizes toward a target runtime: ``` For each completed task: - 1. Count items in the chunk (from the RangeExpr value) - 2. Accumulate total items and total duration - 3. Compute duration_per_task = total_duration / total_items - 4. Compute ideal_chunk_size = target_runtime_seconds / duration_per_task - 5. For the first 10 tasks, blend: 75% current + 25% ideal (conservative ramp) - 6. Clamp to minimum of 1 - 7. Update iterator's default task count if changed +1. Count items in the chunk (from the RangeExpr value) +2. Accumulate total items and total duration +3. If the cumulative duration is zero or non-finite, keep the current chunk size and wait + for a measurable sample +4. Compute duration_per_task = total_duration / total_items +5. Compute ideal_chunk_size = target_runtime_seconds / duration_per_task +6. For the first 10 tasks, blend: 75% current + 25% ideal (conservative ramp) +7. Clamp to minimum of 1 +8. Update iterator's default task count if changed ``` -The blending in step 5 prevents wild oscillation when early tasks have atypical durations. -After 10 tasks, the estimate stabilizes and the ideal size is used directly. +Deferring adjustment when elapsed time is not measurable prevents a zero duration from +producing an infinite ideal size that would saturate to `usize::MAX`. The blending in step 6 +prevents wild oscillation when early tasks have atypical durations. After 10 tasks, the +estimate stabilizes and the ideal size is used directly. `target_runtime_seconds` comes from the step's CHUNK[INT] parameter definition. If not specified or zero, the model layer's `StepParameterSpaceIterator` sets `chunks_adaptive()`