Skip to content

fix(build): speed up build preparation before workers start - #367

Merged
alexey-igrychev merged 6 commits into
mainfrom
fix/build/reduce-preparation-work
Sep 27, 2026
Merged

alexey-igrychev merged 6 commits into
mainfrom
fix/build/reduce-preparation-work

Conversation

@alexey-igrychev

@alexey-igrychev alexey-igrychev commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Most of the time before the first build worker starts went into re-preparing git repositories and re-hashing stapel stage checksums once per image and platform. On a Deckhouse configuration (1122 content-based tag stages, 188 remote repositories) that phase dropped from 26:33 to 4:58 with warm git mirrors; with an empty git cache, remote prefetching takes it from 22:26 to 13:09, and 8:22 together with werf#7930 and werf#7931. Stage digests are unchanged.

What

  • A remote commit without submodules is read from the repository objects instead of a checked-out worktree: on the Deckhouse workload 85 worktree switches (489 s) become 0. A commit with submodules still gets a worktree, and an existing worktree cache is reused, not reset.
  • A shallow local clone is unshallowed once per graph calculation instead of once per image: 551 Fetching origin blocks become 1. gitWorktree.forceShallowClone, gitWorktree.allowUnshallow and the shallow git clone is not allowed error are unchanged.
  • Each stapel shell stage checksum is hashed once per builder; repeated reads across images and platforms return the cached value. The builder snapshots the shell configuration it was constructed with, so a later mutation of that configuration cannot make execution and the cached checksum diverge.
  • With --parallel, remote repositories are prefetched before graph calculation with at most 4 concurrent tasks, bounded further by --parallel-tasks-limit; --parallel=false or a limit of 1 keeps the previous serial order. VERIFIED: git trace2 shows a peak of 4 concurrent fetch/ls-remote processes over 316 started.
  • Remote repositories sharing a mirror, including ones reached through a URL alias, are prefetched once rather than once per configured name.
  • Every ref of a repository is prefetched, not only the first one: refs that share a mirror are prepared in one task in configuration order, and mirrors are handed to workers dynamically, so a worker that finishes early takes the next mirror instead of idling.
  • A slow git ls-remote --tags against one remote no longer blocks tag resolution for other remotes; cache refresh behavior is unchanged.
  • VERIFIED: all 1122 anchor stage digests are identical between the unmodified and the changed binary across the warm, cold and prefetching runs of that configuration.
  • No new flags, no werf.yaml surface, and nothing changes after workers start.

Why

Preparation was scoped per image and per platform instead of per build: every image re-checked and re-unshallowed the same local repository, remote repositories were cloned and fetched serially at the moment their first mapping was generated, and every remote commit read went through a full worktree checkout that only submodule resolution actually needs. On a configuration with over a thousand stages this made preparation longer than the builds it precedes, and the cost grows with the number of images, not with the amount of work to build. A persistent on-disk cache of prepared state was rejected: it would hide the duplicated work behind an invalidation problem instead of removing it.

Digest calculation reads every user stage checksum repeatedly, once per
image and platform, and each read hashed the whole command list again. On
a configuration with hundreds of stapel images this dominated stage
determination. The builder now caches a stage checksum after the first
read and snapshots the shell configuration it was built from, so a later
mutation of the caller's config cannot make execution and the cached
checksum diverge.

Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
Every remote git mapping materialized a worktree to read the commit,
paying a full checkout per commit even though only object reads follow.
A worktree is needed solely to resolve submodules, so a commit without
submodules now gets a handle backed by the repository objects directly;
commits with submodules keep the previous path, including worktree reuse.

Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
The ls-remote tag cache held one global lock for the whole map, so a slow
lookup against one remote blocked tag resolution for every other remote.
The lock is now per cache key: map access stays global and brief, while
the network call holds only the entry it fills, and cache refresh
behavior is unchanged.

Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
Graph calculation prepared git repositories per image and platform: a
shallow local clone was checked and unshallowed again for every image,
and remote repositories were cloned and fetched strictly one after
another. Unshallowing now happens once per graph calculation, and
selected remote repositories are prefetched with at most four concurrent
tasks, bounded further by --parallel-tasks-limit and disabled without
--parallel. Repositories sharing a mirror or reached through a URL alias
are fetched once.

Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
@alexey-igrychev

alexey-igrychev commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator Author

Verification

  • Cold and warm runs of a large Deckhouse configuration up to scheduler start, comparing all 1122 anchor digests between the unmodified and the changed binary; source and binary checksums were pinned so the code could not change mid-measurement. Cold to workers: 22:26 -> 13:09, of which stage determination 17:02 -> 7:24. Warm to workers: 26:33 -> 4:58, worktree switches 85 -> 0, Fetching origin 551 -> 1.
  • Mutation: removing the no-submodules shortcut -> Remote object checksums failed; keeping the per-image unshallow -> Local Git preparation failed; dropping the checksum snapshot -> Shell stage checksums failed; resolving a mirror alias as a distinct repository, bypassing the worker limit, or skipping the cache publication -> Remote Git preparation failed; unwiring the limit from --parallel-tasks-limit -> Remote preparation limit failed; restoring the single global tag-cache lock -> Remote tag concurrency timed out. For the per-ref prefetch: prefetching only the first ref of a mirror, deduplicating refs by repository name instead of cache key, and handing out no tasks at all each failed Remote Git preparation.
  • Race detector run over pkg/build/image.

Review focus

  • initRepoHandleBackedByWorkTree: whether any caller relies on the worktree existing as a side effect for a submodule-free commit.
  • prepareRemoteGitRepos: mirror identity comes from GetClonePath() evaluated before fetching; confirm this covers every aliasing case that matters here, and that grouping refs of one mirror into a single task cannot deadlock against withMirrorKindLock.
  • Prefetch happens before graph calculation, so a repository referenced only by an image excluded from processing is not prefetched — confirm the selection matches the images actually built.

@alexey-igrychev

Copy link
Copy Markdown
Collaborator Author

The same change is open upstream as werf#7928, where all checks are green (unit, lint, e2e_simple/extra/complex, integration_git/main, all build matrix entries). This branch carries two fork-only differences: the shell config snapshot also clones Packages, and the new test fixtures keep build.sbom.enable: false.

Prefetching selected one ref per storage mirror, so a repository used at
several refs had all but the first fetched serially during graph
calculation, after the concurrent window had closed. Refs are now grouped
by mirror and the whole group is prefetched in one task: refs sharing a
mirror stay serialized, as the mirror lock requires anyway, while
different mirrors proceed concurrently. Tasks are handed out dynamically,
so a worker that finishes a small mirror picks up the next one instead of
idling on its static share.

Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
The handle constructor for a commit without submodules took a context it
never used, and the comment on mirror grouping claimed more than
GetClonePath actually collapses.

Signed-off-by: Aleksei Igrychev <aleksei.igrychev@palark.com>
@alexey-igrychev

Copy link
Copy Markdown
Collaborator Author

Upstream twin werf#7928 is green with the same commits plus a cleanup that drops an unused context argument and tightens a comment. Measured together with #368 and #369: cold run to the first build worker 13:09 -> 8:22, all 1122 anchor digests identical to the unmodified binary.

@alexey-igrychev
alexey-igrychev marked this pull request as ready for review September 27, 2026 08:13
@alexey-igrychev
alexey-igrychev merged commit 4e39f88 into main Sep 27, 2026
11 of 14 checks passed
@alexey-igrychev
alexey-igrychev deleted the fix/build/reduce-preparation-work branch September 27, 2026 08:13
alexey-igrychev added a commit that referenced this pull request Sep 28, 2026
🤖 I have created a release *beep* *boop*
---


##
[3.6.1-dk.1](v3.6.0-dk.1...v3.6.1-dk.1)
(2026-09-28)


### Features

* **sbom:** expose the ISPRAS checker options in sbom validate
([#375](#375))
([8fe11f0](8fe11f0))


### Bug Fixes

* **build, git:** reuse one ssh connection for git requests
([#368](#368))
([708a660](708a660))
* **build, git:** reuse one ssh connection for git requests
([werf#7930](https://github.com/deckhouse/delivery-kit/issues/7930))
([6bf8313](6bf8313))
* **build:** resolve content anchors concurrently in parallel builds
([#369](#369))
([26d9ae6](26d9ae6))
* **build:** resolve content anchors concurrently in parallel builds
([werf#7931](https://github.com/deckhouse/delivery-kit/issues/7931))
([39f664f](39f664f))
* **build:** speed up build preparation before workers start
([#367](#367))
([4e39f88](4e39f88))
* **build:** speed up build preparation before workers start
([werf#7928](https://github.com/deckhouse/delivery-kit/issues/7928))
([7a188c2](7a188c2))
* **build:** stop per-digest local image scans before image workers
start ([werf#7929](https://github.com/deckhouse/delivery-kit/issues/7929))
([20d74b6](20d74b6))
* **git:** keep a healthy cached worktree on canceled builds
([#374](#374))
([ed3e5fa](ed3e5fa))
* **git:** keep a healthy cached worktree on canceled builds
([werf#7935](https://github.com/deckhouse/delivery-kit/issues/7935))
([ad187a4](ad187a4))
* **git:** keep local submodule reuse for nested submodule names
([#371](#371))
([0e82710](0e82710))
* **git:** keep local submodule reuse for nested submodule names
([werf#7933](https://github.com/deckhouse/delivery-kit/issues/7933))
([196df14](196df14))
* **git:** rebuild a broken cached worktree instead of failing
([#370](#370))
([967c83d](967c83d))
* **git:** rebuild a broken cached worktree instead of failing
([werf#7932](https://github.com/deckhouse/delivery-kit/issues/7932))
([c89fdb6](c89fdb6))
* **sbom:** keep long sbom validate checker messages on one line
([#376](#376))
([5ec87d4](5ec87d4))
* **storage:** avoid repeated local image scans before builds
([#366](#366))
([88bf86f](88bf86f))
* **storage:** reuse recent tags listings for stage lookups on cache
misses ([#372](#372))
([fad4fea](fad4fea))
* **storage:** reuse recent tags listings for stage lookups on cache
misses ([werf#7934](https://github.com/deckhouse/delivery-kit/issues/7934))
([d3d0c41](d3d0c41))


### Miscellaneous Chores

* force release 3.6.1-dk.1
([30bdf77](30bdf77))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
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.

1 participant