Skip to content

perf(ci): isolate bottlecap build-directory cache per concurrent build - #1369

Open
lucaspimentel wants to merge 1 commit into
mainfrom
lpimentel/isolate-bottlecap-build-cache
Open

lucaspimentel wants to merge 1 commit into
mainfrom
lpimentel/isolate-bottlecap-build-cache

Conversation

@lucaspimentel

@lucaspimentel lucaspimentel commented Sep 10, 2026 •

Copy link
Copy Markdown
Member

Overview

The two bottlecap compile Dockerfiles mount the cargo build directory (target) as a BuildKit cache mount with no sharing mode. All concurrently-scheduled bottlecap (*) compile jobs that use the same Dockerfile therefore share one cache mount, and cargo serializes them on its build-directory lock: while one build holds the lock, the others log Blocking waiting for file lock on build directory and sit idle.

Measured over 240 successful compile attempts across 30 main pipelines (2026-06-25 to 2026-09-09):

  • 14.2% of attempts (34/240) hit the cargo build-directory lock wait
  • Lock wait: p50 4.0m, p90 5.6m, max 13.1m
  • 8.6% of all compile wall time is spent waiting on this lock (2.3h of 26.7h)
  • Jobs that hit the lock take +2.8m longer at the median (8.5m vs 5.7m)
  • Worst flavors: amd64 33%, arm64, fips 23%

This adds sharing=private to the target cache mount only, in both images/Dockerfile.bottlecap.compile and images/Dockerfile.bottlecap.alpine.compile. Each concurrent build now gets its own cache instance instead of blocking.

Why not a per-flavor id=? A per-flavor cache id= (e.g. id=target-${PLATFORM}-fips${FIPS}) would also isolate the caches, but neither Dockerfile has a # syntax= directive and CI builds under docker:20.10, so build-arg expansion inside a mount id= may silently not apply on that frontend. If expansion failed, all flavors would collapse into one literal id, which is worse than the current behavior and hard to notice. sharing=private needs no variable expansion.

Why the registry/git mounts are unchanged: the cargo registry and git cache mounts are read-mostly and shared registry cache is genuinely useful across flavors. The exclusive lock that caused every measured wait is on the build directory, not the registry. Keeping this to one variable also means any change in the lock-wait rate after merge is attributable to this change.

Known risk (worth flagging, not buried): 5 of 10 analyzed compile-job stalls occur at the final LTO link (lto = true, codegen-units = 1, bottlecap/Cargo.toml:129-131). If those stalls are memory pressure from concurrent LTO links on a shared runner, the cargo lock has been accidentally serializing them, and this change lets up to 4 same-Dockerfile flavors hit LTO simultaneously, which could raise the job_execution_timeout rate. This is an unvalidated hypothesis (would need runner co-tenancy data; 9-10 events is likely underpowered), but it means the timeout rate should be watched after merge rather than assumed to improve.

Testing

  • docker build --check (Docker 29.8.0, current buildkit frontend) parses both modified Dockerfiles with no warnings. The Dockerfile parser strips full-line comments inside continued RUN instructions, so the added comment lines are inert.

CI validation on this branch

Validated across all 4 GitLab pipelines for this branch (Sep 10-16, all containing the fix), 34 compile attempts (32 first attempts + 2 retries):

  • All 8 flavors compiled successfully in every pipeline (both failures recovered on retry).
  • Blocking waiting for file lock on build directory appeared in 0 of 34 job traces (baseline on main: 14.2% of attempts).
  • Median successful compile duration: ~5.8m, comparable to the baseline median (5.7m).

The 2 failed attempts, both unrelated to the target-mount lock and both green on retry:

  • bottlecap (arm64, alpine) (Sep 11, 117s, script_failure): transient race in the shared /root/.cargo/git cache mount; cargo crashed removing serverless-components-.../config.lock (No such file or directory, then Directory not empty) while a concurrent build held it. This is the mount family this PR deliberately leaves shared; it is a candidate for the same treatment if it recurs.
  • bottlecap (arm64, fips) (Sep 16, 1207s, job_execution_timeout): trace shows the final LTO link (-C lto -C codegen-units=1) start ~3m into the job, then silence until the 20m runner timeout. One data point consistent with the LTO-stall risk above (~3% of attempts, vs 23% lock-hit rate for this flavor on main); the retry completed in 321s.

One caveat to watch after merge: with sharing=private, all 4 same-Dockerfile flavors run concurrently, so each builds with a cold target-cache instance, reducing cross-run warm-cache reuse. 4 pipelines is too small a sample to detect that effect; watch per-flavor durations alongside the timeout rate.

Copilot AI lite review requested due to automatic review settings September 10, 2026 19:18
@lucaspimentel
lucaspimentel requested a review from a team as a code owner September 10, 2026 19:18
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-16T21:24:26.973829Z ae742dd Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The added full-line comments are embedded inside continued RUN --mount=... instructions and can be interpreted as part of the shell command on some Dockerfile frontends, risking broken builds or dropping the intended mount behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR aims to reduce CI wall time for Bottlecap compile jobs by preventing concurrent builds from contending on Cargo’s shared target build-directory lock when using BuildKit cache mounts.

Changes:

  • Updates the target BuildKit cache mount to use sharing=private in the AL2 compile Dockerfile.
  • Updates the target BuildKit cache mount to use sharing=private in the Alpine compile Dockerfile.
File summaries
File Description
images/Dockerfile.bottlecap.compile Sets the target cache mount to sharing=private to avoid cross-job contention on Cargo’s build-directory lock.
images/Dockerfile.bottlecap.alpine.compile Same sharing=private change for the Alpine-based compile image to isolate concurrent builds.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread images/Dockerfile.bottlecap.alpine.compile
Comment thread images/Dockerfile.bottlecap.compile
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Pipelines

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 76a8c01 | Docs | View more details | Give us feedback!

@lucaspimentel
lucaspimentel force-pushed the lpimentel/isolate-bottlecap-build-cache branch 3 times, most recently from 1253e16 to ae742dd Compare September 16, 2026 17:12
@lucaspimentel
lucaspimentel marked this pull request as ready for review September 16, 2026 21:22
@lucaspimentel

Copy link
Copy Markdown
Member Author

@DataDog review

@datadog-datadog-prod-us1 datadog-datadog-prod-us1 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datadog Autotest: PASS

More details

BuildKit private cache mounts give each concurrent compile job a separate Cargo target directory. The change keeps the build output path and the shared registry and Git caches unchanged.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Datadog Autotest · Commit ae742dd · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

The target cache mount in the compile Dockerfiles had no sharing mode, so
concurrent bottlecap compile jobs using the same Dockerfile shared one cache
and serialized on cargo's build-directory lock. Measured across 240 compile
attempts on 30 main pipelines: 14.2% of attempts waited on the lock (median
wait 4.0m, max 13.1m), adding 2.8m to the median job duration when hit.

sharing=private gives each concurrent build its own target cache instead of
blocking. Cache reuse is barely affected: 89% of attempts are already fully
cold. The cargo registry and git mounts stay shared, since the measured
contention was only on the target directory.
@lucaspimentel
lucaspimentel force-pushed the lpimentel/isolate-bottlecap-build-cache branch from ae742dd to 76a8c01 Compare October 1, 2026 20:38
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.

2 participants