Skip to content

vinca-gha --timings (pack each stage's recipes into jobs by measured build time) and --keep-file - #179

Merged
Tobias-Fischer merged 4 commits into
RoboStack:masterfrom
Tobias-Fischer:feat/timed-batching
Oct 23, 2026
Merged

Tobias-Fischer merged 4 commits into
RoboStack:masterfrom
Tobias-Fischer:feat/timed-batching

Conversation

@Tobias-Fischer

@Tobias-Fischer Tobias-Fischer commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

The staged GitHub Actions workflow cuts each stage into jobs of --batch_size recipes in list order. The heavy recipes of a stage often end up in one job, which then holds up the whole stage.

In RoboStack's jazzy full rebuild, a single job was running alone for 55% of the wall time on Linux. For example, rviz_default_plugins and nav2_behavior_tree shared a 25-minute job, while the other jobs of that stage took about 3 minutes.

vinca-gha --timings <file> [--runners N] (opt-in; without --timings nothing changes):

  • Input: the file holds {"recipes": {name: seconds}, "default": seconds, "job_overhead": seconds}.
  • Packing: each stage is packed longest-first into the number of jobs that gives the shortest stage on N runners. N defaults to GitHub's limit on concurrent jobs with its hosted runners, 20 or 5 on macOS (the Free plan). It uses fewer jobs when that costs at most 1%. No job gets more than --batch_size recipes, and each job keeps the stage's order.
  • Short stages: consecutive stages of at most 2 minutes still share one job, in order.
  • Compatibility packages: with package_name_mode: both, each ros-<distro>-<name> package only depends on ros2-<name> (and the mutex, which that needs too). It is now built right after that package in the same job, and its time counts towards it, instead of a stage later in another job. One whose ros2- package isn't being built stays where it is (batch_stages(..., followers=...)).
  • Stages stay sequential, so the dependency order doesn't change.

vinca-gha --keep-file <path>: every build job (unix, macOS, Windows) ends with an upload-artifact step for that file, named after the job, also when the build failed. RoboStack uses it to keep the build times its jobs record, which become the --timings input.

Checked against the real jazzy rebuild (linux-64, 2405 recipes, per-recipe times from its logs, simulated with 8 runners):

stages jobs wall runner time workflow
by count (today) 48 117 6.96 h (the run took 6.93 h) 12.0 h 292 KiB
--timings --runners 8 52 233 2.68 h 12.2 h 384 KiB
--timings (20 runners) 283 2.47 h (3.00 h if only 8 are free) 442 KiB
  • Correctness: every recipe's ROS dependencies are in an earlier stage or earlier in its job, and every compatibility package comes right after its package.
  • Fewer free runners: if only 5 are free instead of the planned 8, wall time is 3.71 h. Pass --runners when fewer can be counted on, e.g. when several workflows share the account's limit.
  • Tests: 11 new tests for time packing, the batch-size cap, shared short stages, followers, BuildTimings.from_file, compatibility_followers and --keep-file. All 305 pass, and ruff is clean.

RoboStack/robostack#139 records the times in its build jobs, keeps them with --keep-file, and passes them back as --timings, averaged over its distributions.

The measurements and the comparison with #149's scheduler are in the analysis behind this; in short, plain time-balancing within the existing stages captures most of the gain.

🤖 Claude (AI assistant), on behalf of @Tobias-Fischer

🤖 Generated with Claude Code

Tobias-Fischer and others added 3 commits October 23, 2026 14:07
…time

Each stage of the staged build workflow is cut into jobs of --batch_size
recipes in list order, so the heavy recipes of a stage often end up in one
job that holds up the whole stage. In RoboStack's jazzy full rebuild one job
ran alone 55% of the time on Linux (e.g. rviz_default_plugins and
nav2_behavior_tree in a 25-minute job next to 3-minute ones).

With --timings (a JSON file of measured seconds per recipe, a default for
unmeasured ones and the per-job overhead) and --runners, each stage is packed
longest-first into the number of jobs that gives the shortest stage on that
many runners (fewer jobs when that costs at most 1% or one job's overhead),
never more than --batch_size recipes per job, each job keeping the stage's
order. Consecutive short stages (5 minutes) still share a job. Without
--timings nothing changes.

Simulated on the measured jazzy rebuild: 6.9 -> 2.6 h on linux-64, 5.8 -> 2.1 h
on linux-aarch64, 9.0 -> 5.0 h on osx-arm64, for at most 3% more runner time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…thresholds

With package_name_mode: both, each ros-<distro>-<name> compatibility package
only depends on ros2-<name> (and the mutex, which that needs too). With
--timings it's now built right after it in the same job, its time counted
towards it, instead of a stage later in another job; one whose ros2- package
isn't being built stays where it was. batch_stages takes these as `followers`.

Thresholds, from simulating the generated workflows with jazzy's measured
linux-64 times (8 runners): consecutive single-job stages share a job up to
120 s (was 300), and a stage takes fewer jobs only when that costs at most 1%
(no extra one-job-overhead slack): 2.68 h instead of 3.21 h, 233 jobs, 384 KiB.
Today's workflow models at 6.96 h (the run took 6.93 h). Every recipe's ROS
dependencies are in an earlier stage or earlier in its job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rs says otherwise

20 jobs at once with GitHub-hosted runners, 5 on macOS (the Free plan).
Simulated on jazzy linux-64: 283 jobs, 442 KiB, 2.47 h on 20 runners
(3.00 h if only 8 are free).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tobias-Fischer added a commit to RoboStack/robostack that referenced this pull request Oct 23, 2026
…s by time

The build jobs now record their times: `rs <distro> build-ci` times each
recipe's build and `rs <distro> upload` each package's upload, appending to
distros/<distro>/work/build_timings.jsonl, which every build-branch job keeps
as an artifact (build-times-<job>, a step `rs gha` adds after the build).

robostack-bot update-build-timings (weekly, with the dependency reports)
merges the last week's artifacts into shared/build_timings/<platform>.json:
each recipe's newest time per distribution; a distribution uses the average
over all of them, so a recipe new to it still has a time. Compatibility
recipes share one median per distribution.

Once a platform has times and the pinned vinca supports it, `rs <distro> gha`
passes them to vinca-gha --timings (RoboStack/vinca#179), which packs each
stage's recipes into jobs by time and builds compatibility packages right
after their package; until then nothing changes. A workflow over GitHub's
size limit is retried with fewer runners first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each build job (unix, macOS, Windows) then ends with an upload-artifact step
for that file, also when the build failed; RoboStack keeps the build times its
jobs record this way, for --timings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tobias-Fischer added a commit to RoboStack/robostack that referenced this pull request Oct 23, 2026
…s by time

Each build-branch job records how long each recipe took to build (`rs
<distro> build-ci`, in distros/<distro>/work/build_timings.jsonl) and keeps
that file as an artifact (vinca-gha --keep-file). robostack-bot
update-build-timings (weekly, with the dependency reports) merges the last
week's into shared/build_timings/<platform>.json, {recipe: {distro: seconds}}.

With times for the platform, `rs <distro> gha` passes vinca-gha --timings
(each recipe's average over the distributions, so a recipe new to one
distribution still has a time): it packs each stage's recipes into jobs by
time and builds compatibility packages right after their package. Until then
the jobs are cut by count as before. A workflow over GitHub's size limit is
retried with fewer runners first.

The vinca pin moves to RoboStack/vinca#179 (--timings, --keep-file).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Tobias-Fischer Tobias-Fischer changed the title vinca-gha --timings: pack each stage's recipes into jobs by measured build time vinca-gha --timings (pack each stage's recipes into jobs by measured build time) and --keep-file Oct 23, 2026
@Tobias-Fischer

Copy link
Copy Markdown
Contributor Author

@baszalmstra - what do you think of this approach? Meant to replace / complement #149.

@traversaro - do you mind having a look as well?

RoboStack PR to save the timings in the first instance and then use the new functionality: RoboStack/robostack#139

@baszalmstra

Copy link
Copy Markdown
Collaborator

this looks like a much better approach than what I tried!

@traversaro

traversaro commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Great! How do we plan to maintain the timings file? Every full rebuild overwrites the timing file check in in the repo?

@traversaro

Copy link
Copy Markdown
Member

Great! How do we plan to maintain the timings file? Every full rebuild overwrites the timing file check in in the repo?

Never-ending, I now saw the corresponding robostack PR!

@Tobias-Fischer
Tobias-Fischer merged commit c52ce0c into RoboStack:master Oct 23, 2026
3 checks passed
@Tobias-Fischer
Tobias-Fischer deleted the feat/timed-batching branch October 23, 2026 05:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants