Skip to content

Add ci channel for per-commit Build Cache Service framework resolution - #878

Open
LoopedBard3 wants to merge 14 commits into
dotnet:mainfrom
LoopedBard3:AddBuildCacheSystemSupportForCrank
Open

Add ci channel for per-commit Build Cache Service framework resolution#878
LoopedBard3 wants to merge 14 commits into
dotnet:mainfrom
LoopedBard3:AddBuildCacheSystemSupportForCrank

Conversation

@LoopedBard3

@LoopedBard3 LoopedBard3 commented Apr 14, 2026

Copy link
Copy Markdown
Member

Adds a new ci channel that resolves .NET binaries from the Build Cache Service (BCS) instead of NuGet feeds, giving per-commit granularity for performance-regression bisection instead of feed cadence.

On the ci channel, crank always overlays BOTH shared frameworks in every job — the base runtime (Microsoft.NETCore.App, from dotnet/runtime) and the ASP.NET Core shared framework (Microsoft.AspNetCore.App, from dotnet/aspnetcore). Both default to latest, and each repo can be pinned independently to bisect one while holding the other at latest.

Named ci (not buildcache) per review feedback — "buildcache" is an implementation detail; ci describes where the binaries come from, consistent with the other channel names.

CLI surface

Rather than add new arguments, the ci channel reuses the existing runtimeVersion / aspNetCoreVersion arguments — on this channel they carry a commit SHA instead of a feed version:

  • empty → latest cached build for that repo on main
  • 8–40 hex chars → pin/bisect that repo at that commit
  • a feed version stringrejected with a clear error (version-string pinning is not supported on ci; conversely the other channels accept a version string and reject a SHA)

No new arguments are introduced. Latest always resolves against main — the only branch the BCS pipeline builds — so there's no branch knob to configure. To target a specific point, pin a commit SHA; SHA lookups are branch-independent, so a commit that happens to live on a release branch still resolves. (If the pipeline starts caching release branches later, branch selection can be added back as a purely additive argument.)

Usage:

# latest of both on main
--application.channel ci

# bisect aspnetcore, runtime stays latest
--application.channel ci --application.aspNetCoreVersion <aspnetcore-sha>

# bisect the base runtime, aspnetcore stays latest
--application.channel ci --application.runtimeVersion <runtime-sha>

# pin both
--application.channel ci --application.runtimeVersion <runtime-sha> --application.aspNetCoreVersion <aspnetcore-sha>

Controller (Job.cs) — reuses RuntimeVersion / AspNetCoreVersion (no new properties); on the ci channel they carry a commit SHA.

Agent (Startup.cs / BuildCacheClient.cs) — new CLI:

  • --build-cache-base-url (default: https://pvscmdupload.z22.web.core.windows.net)
  • --build-cache-disabled

How resolution works

The agent still builds the app with real NuGet packages (so restore/publish behave normally), then overlays BCS bits per publish kind:

  • Framework-dependent: a per-job isolated dotnet home is created at <temp>/crank-buildcache/home-…/, mirrored from the global _dotnethome, and overlaid with the BCS runtime and aspnetcore archives. StartProcess resolves from this home; the global _dotnethome is never mutated (concurrent jobs / other channels unaffected).
  • Self-contained: BCS files are overlaid onto the published outputFolder; the SDK-bound apphost is preserved.

The base runtime archive is raw build output, so BCS bits are overlaid onto a feed-installed runtime. The aspnetcore archive is the runtime-pack nupkg stored verbatim (carrying deps.json + runtimeconfig.json), so that framework folder is built entirely from BCS and the job fails loudly if the pack is incomplete. If a requested commit isn't in the cache, crank fails rather than silently falling back.

Version surfaces (bisection distinguishability)

Every ci run stamps the resolved commits so bisection points never collide, and the stamp reaches the perf database:

  • results measurements NetCoreAppVersion / AspNetCoreVersion (what lands in the controller --json and the SQL/ES document as application.netCoreAppVersion / aspNetCoreVersion) → {feedVersion}+ci.{12-char sha} on the ci channel; non-ci runs are unchanged ({feedVersion}+{12-char sha}). The +ci. marker is folded into the existing fields rather than added as new keys, so bisection runs are self-identifying without any downstream schema change.
  • job.RuntimeVersion / job.AspNetCoreVersion (the DTO, visible on the agent job object) → {feedVersion}+ci.{8-char sha}
  • Dependency records carry the full 40-char CommitHash + repo URL for both runtime and aspnetcore

The 12- and 8-char forms and the full Dependency hash all encode the same underlying commit; the numeric version prefix stays feed-coherent (so runtimeconfig/roll-forward stay valid) and the +ci.{sha} suffix is the per-commit distinguisher.

Cache correctness

RuntimeVersion and AspNetCoreVersion are part of BuildKeyData, so --application.options.reuseBuild cache-busts when a pinned SHA changes. The reuse path also records the +ci.{sha} version measurements (it returns before the fresh-build recording, so without this a reused ci run would surface no version).

Backward compatibility

Fully gated on --application.channel ci. DefaultChannel is unchanged (current). No new controller properties are added — the existing runtimeVersion / aspNetCoreVersion arguments only change meaning on the ci channel; every other channel takes the exact same path as before, so old controllers/agents interoperate unchanged. Audited diff-level against main: purely additive, no opt-out-path behavior change.

Verification

  • dotnet build clean; 119/119 unit tests pass (BuildCacheClientTests.cs adds 40 methods covering ParseLatestBuilds, commit-SHA validation incl. path-traversal, the SHA-vs-version channel rules, dual-repo overlay, isolated-home concurrency, SelectHighestManagedDir numeric ordering, cleanup, etc.).
  • Local both-overlay hello-world runs (win-x64) confirmed the three ci paths end-to-end:
    • latest resolution engages the correct latest/main/latestBuilds.json path per repo;
    • pinned aspnetcore + latest runtime overlays both and runs, with results surfacing aspNetCoreVersion = {ver}+ci.8f51f037469f and netCoreAppVersion = {ver}+ci.6550ccf7827c (matching the resolved commits), while the non-ci load job carries no ci. marker (gating intact);
    • a feed version string on ci fails fast with the documented error.

Post-merge dependency: the aspnetcore latestBuilds.json index (published by the BCS indexer / dotnet-performance pipeline) must exist for latest-aspnetcore resolution; until then the agent fails loudly rather than silently falling back.

Copilot AI 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.

Pull request overview

This PR adds a new buildcache channel to Crank’s agent/runtime resolution pipeline so the runtime binaries can be sourced per-commit from the Build Caching Service (BCS), enabling finer-grained performance regression bisection than VMR feed cadence allows.

Changes:

  • Added build-cache-related properties to Job for selecting commit/branch/config.
  • Added BuildCacheClient for resolving latest builds, downloading/extracting artifacts, and overlaying runtime binaries.
  • Updated agent startup/version resolution logic and docs to support the new buildcache channel and new agent CLI options.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/Microsoft.Crank.Models/Job.cs Adds job-level inputs for BCS commit/branch/config selection.
src/Microsoft.Crank.Agent/Startup.cs Adds CLI options and integrates buildcache into runtime version resolution + post-publish overlay.
src/Microsoft.Crank.Agent/BuildCacheClient.cs Implements BCS querying/downloading/extraction and overlay helpers.
docs/dotnet_versions.md Documents buildcache channel usage and configuration knobs.
docs/build_cache_requirements.md Describes required BCS-side blob layout/access for the integration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Microsoft.Crank.Agent/Startup.cs Outdated
Comment thread src/Microsoft.Crank.Agent/Startup.cs Outdated
Comment thread src/Microsoft.Crank.Agent/Startup.cs Outdated
Comment thread src/Microsoft.Crank.Agent/BuildCacheClient.cs Outdated
Comment thread src/Microsoft.Crank.Agent/BuildCacheClient.cs Outdated
Comment thread src/Microsoft.Crank.Agent/BuildCacheClient.cs Outdated
Comment thread src/Microsoft.Crank.Agent/BuildCacheClient.cs Outdated
Comment thread docs/build_cache_requirements.md
Comment thread src/Microsoft.Crank.Agent/BuildCacheClient.cs Outdated
Adds a new 'buildcache' channel that resolves .NET runtime binaries from the
Build Caching Service (BCS) instead of VMR feeds. This provides per-commit
granularity for performance regression bisection.

Key changes:
- New Job.cs properties: BuildCacheCommitSha, BuildCacheBranch, BuildCacheConfig
- New BuildCacheClient.cs: HTTP client for BCS latestBuilds.json and artifact
  download/extraction with post-build overlay into published output
- Startup.cs: 'buildcache' channel in version resolution that builds with real
  NuGet packages then overlays BCS runtime binaries (194 files) after publish
- Agent CLI options: --build-cache-base-url, --build-cache-repo-name,
  --build-cache-disabled
- Documentation in docs/dotnet_versions.md and docs/build_cache_requirements.md

Usage: --application.channel buildcache [--application.buildCacheCommitSha <sha>]

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@LoopedBard3
LoopedBard3 force-pushed the AddBuildCacheSystemSupportForCrank branch from 99d0195 to 41394e4 Compare April 14, 2026 23:39
@LoopedBard3 LoopedBard3 self-assigned this Apr 29, 2026
LoopedBard3 and others added 3 commits May 1, 2026 11:58
Changes:

* BuildCacheClient: rewrite to drop ~250 LOC of dead code from an earlier
  abandoned design (synthesizing a new shared-framework dir with a synthetic
  version). The FDD overlay path is restored as a new public API.

* New OverlayDotnetHome(extractDir, dotnetHome, runtimeVersion): overlays BCS
  bits into dotnetHome/shared/Microsoft.NETCore.App/{runtimeVersion}/ and
  host/fxr/{runtimeVersion}/ + the dotnet host. Wired into Startup.cs after
  publish so framework-dependent jobs actually run against BCS bits instead of
  silently using the feed runtime.

* OverlayPublishedOutput: copy ALL managed/native runtime files
  unconditionally (previously skipped any file not already in destination,
  which would silently drop new DLLs introduced by the BCS commit). Also
  copies hostfxr/hostpolicy/dotnet for self-contained.

* Hardening:
  - URL-encode repoName/commitSha/buildCacheConfig/branch with
    Uri.EscapeDataString.
  - Atomic download (.partial -> rename) and Content-Length validation so
    truncated archives are not reused after a failed run.
  - Retry transient HTTP failures via ProcessUtil.RetryOnExceptionAsync.
  - Replace shelling out to tar with System.Formats.Tar.TarFile.
  - Wrap synchronous archive extraction in Task.Run.
  - Per-(commit,config) SemaphoreSlim + per-call unique extract dir to avoid
    races between concurrent jobs.
  - Drop unused targetFramework parameter from DownloadAndExtractAsync.
  - All commit-SHA Substring uses go through ShortSha (Math.Min length guard).
  - ParseLatestBuilds: case-insensitive branch_name/BranchName handling and
    skip non-object metadata properties safely.

* Startup.cs:
  - Validate user-supplied BuildCacheCommitSha length (>= 8 chars) up front
    instead of crashing later.
  - Stop mutating runtimeVersion before PatchRuntimeConfig - use feed-resolved
    version so runtimeconfig.json points to a really-installed shared
    framework dir. Suffix +buildcache.<sha> is now applied to
    job.RuntimeVersion only, after PatchRuntimeConfig has run.
  - Treat 0-file overlay as fatal (job.Error + return null) so silent failures
    do not produce wrong-runtime benchmarks.
  - Promote overlay-failure log from Info to fatal job error.

* docs/dotnet_versions.md: add trailing newline.

* test/Microsoft.Crank.UnitTests/BuildCacheClientTests.cs: 17 new tests
  covering ParseLatestBuilds (PascalCase / snake_case / mixed / missing
  fields / case-insensitive lookup / non-object values), GetPlatformMoniker,
  ShortSha, GetNativeLibName, OverlayPublishedOutput unconditional copy,
  pdb/dbg skip, OverlayDotnetHome shared-framework precondition, and full
  shared-framework + hostfxr overlay.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…verlay

Live-tested against an agent with --application.channel buildcache. The first
end-to-end run was reporting the FEED commit hash in .NET Runtime Version
because:

1. The .version file in shared/Microsoft.NETCore.App/{ver}/ was never rewritten
   after the BCS overlay, so any consumer of that file (notably the agent's own
   BenchmarksNetCoreAppVersion measurement metadata) reported the feed-installed
   commit.
2. Even with .version rewritten, the metadata-capture block runs immediately
   after the feed runtime install -- before the post-publish overlay -- so the
   feed commit was captured into Job.Measurements before BCS bits were in place.

Fixes:

* OverlayDotnetHome now accepts an optional commitSha parameter and rewrites
  shared/Microsoft.NETCore.App/{ver}/.version with the BCS commit so anything
  reading that file gets the correct hash.

* Startup.cs splits the BCS overlay into two stages:
  - dotnet-home overlay runs RIGHT AFTER feed runtime install (before the
    BenchmarksNetCoreAppVersion metadata is captured), so the metadata picks up
    the BCS commit. Treat 0 files as fatal.
  - published-output overlay runs after publish (only path where outputFolder
    exists). For self-contained, 0 files is fatal; for FDD it's expected.

* Two new tests:
  - OverlayDotnetHome_WithCommitSha_RewritesVersionFile
  - OverlayDotnetHome_WithoutCommitSha_LeavesVersionFileUntouched

Live test result with --application.channel buildcache --application.framework
net11.0 against a Linux agent:
  | .NET Runtime Version | 11.0.0-preview.5.26256.117+603403d9cb49 |
  | Requests/sec         | 7,593,977                               |
The +603403d9cb49 suffix is the actual BCS commit, and 212+212 files were
overlaid (dotnet home + published output).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d apphost, and more

Round-3 fixes informed by a rubber-duck critique + a second live test.

Per-job ISOLATED dotnet home (replaces in-place overlay of shared dotnetHome)

The biggest design change: stop mutating the global dotnetHome. After feed
install, build a per-job dotnet home by copying the relevant subtrees
(shared/Microsoft.NETCore.App/{ver}, shared/Microsoft.AspNetCore.App/{ver},
host/fxr/{ver}, dotnet[.exe]) and overlay BCS bits into the copy. The global
dotnetHome stays clean. This fixes three real bugs at once:

  - cross-job pollution (a later non-buildcache job would have read the BCS
    commit from .version),
  - concurrent buildcache jobs racing on the same shared framework dir,
  - mid-overlay failures leaving a permanently-corrupted shared framework that
    _installedDotnetRuntimes would continue to trust.

The per-job home is exposed via JobContext.BuildCacheDotnetHome and used by
StartProcess so framework-dependent jobs actually load BCS bits at runtime.
Cleanup happens at job-end.

Build cache fields in BuildKeyData

Job.BuildKeyData now carries BuildCacheCommitSha / BuildCacheBranch /
BuildCacheConfig so reuseBuild + NoBuild don't silently reuse a build pinned
to a different BCS commit.

SDK-bound apphost is no longer clobbered

A live test exposed this regression: the BCS archive ships the raw,
unbound apphost. Overlaying it on top of the SDK-bound published binary left
the apphost with the placeholder SHA-256 binding and the app refused to start
with "This executable is not bound to a managed DLL to execute. The binding
value is: 'c3ab8ff1...'". Fixed by NOT overlaying apphost. CoreCLR JIT, GC,
managed BCL, hostfxr, hostpolicy still all come from BCS, so the perf
relevant code is correct. (Doc comment explains how a future enhancement
could rebind a BCS apphost via Microsoft.NET.HostModel.HostWriter.)

BCS-config RID for overlay discovery

Stop using the host's GetPlatformMoniker for overlay discovery. Resolve the
RID from the buildCacheConfig instead so an explicit musl/cross-arch override
finds the right runtime pack inside the archive.

Numeric-aware managed lib dir selection

SelectHighestManagedDir parses net{major}.0 numerically so the BCS archive
could ship multiple TFMs without lexically picking net9.0 over net10.0.

Non-retryable 404

New BuildCacheNotFoundException sentinel + RetryTransientAsync helper so 404
responses fail fast instead of being retried 3 times.

Strict SHA validation

ValidateCommitSha requires 8-40 hex chars; rejects "../../../etc/passwd",
non-hex, and silly inputs early instead of letting them propagate into URLs
and temp paths.

Cleanup

CleanupExtractDir wipes per-call extraction dirs after overlay; the archive
in the parent commit dir is intentionally kept so subsequent jobs for the
same commit can skip the download. Per-job dotnet home is also deleted at
job end.

Executable bits

EnsureExecutable preserves +x on native files and the dotnet host on
Unix-like systems (File.Copy can drop the bit if the destination didn't
have it).

Tests (36 BuildCacheClient tests, all passing)

New coverage for:
  - SelectHighestManagedDir picks net11.0 over net10.0 over net9.0,
  - GetRidForConfig for every supported config,
  - ValidateCommitSha (accepts/rejects cases incl. path-traversal-like
    inputs),
  - CreateBuildCacheDotnetHome: mirrors global, overlays BCS, rewrites
    .version, and asserts the global home is NOT touched,
  - Two concurrent CreateBuildCacheDotnetHome invocations produce isolated
    homes,
  - OverlayPublishedOutput preserves a pre-existing SDK-bound apphost.

Live test result

  --application.framework net11.0 --application.channel buildcache against a
  Linux agent:

    | .NET Runtime Version | 11.0.0-preview.6.26277.104+38d408d22a64 |
    | Requests/sec         | 8,456,046                               |

  Per-job dotnet home created at:
    /tmp/crank-buildcache/home-38d408d2-coreclr_x64_linux-aaa3385bb0ae4bdca01c7c6b4a7ca2bc

  Global dotnetHome .version preserved: b520ee7cc01690f70d1431951f554c4e0666a69a
  (the feed commit, not the BCS commit) — proving the isolation works.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LoopedBard3 and others added 3 commits June 19, 2026 12:25
Extends the buildcache channel (currently runtime/coreclr only) to also
resolve dotnet/aspnetcore builds from the Build Cache Service, selected
per-job via a new buildCacheRepo property (runtime|aspnetcore, default
runtime). The runtime path is unchanged; aspnetcore is purely additive.

- Job.cs: BuildCacheRepo property + build-key plumbing.
- BuildCacheClient.cs: BuildCacheFlavor enum + ParseFlavor/GetConfigMap/
  AllConfigs; flavour-aware ResolveBuildCacheConfig/GetArtifactFile/
  GetRidForConfig; CreateBuildCacheDotnetHome + OverlayPublishedOutput
  overlay Microsoft.AspNetCore.App (managed-only, no host) for aspnetcore.
- Startup.cs: flavour-aware selection, version defaulting/sentinels,
  aspNetCoreVersion re-resolution, Q2 single-component collision handling,
  apply + reporting (+buildcache.{sha} on AspNetCoreVersion).
- Tests: aspnetcore ParseFlavor/GetRidForConfig/overlay/home tests (93 green).
- Docs: buildCacheRepo usage, aspnetcore archive layout, single-component caveat.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch the aspnetcore build-cache flavour from a transformed
microsoft.aspnetcore.app.runtime.{rid}/Release/... archive to the verbatim
runtime-pack nupkg. A nupkg is a zip on every OS, so it extracts with the same
ZipFile path and exposes runtimes/{rid}/... at the archive root (no Release
wrapper). This keeps the full shipped artifact (crank already filters managed/
native at consume time) instead of a lossy projection.

- PlatformToBcsConfigAspNetCore artifact files -> .nupkg
- CreateBuildCacheDotnetHome / OverlayPublishedOutput aspnetcore overlay reads
  runtimes/{rid} from the extract root; runtime flavour unchanged
- ExtractArchiveAsync treats .nupkg as a zip
- Tests + docs updated for the raw-nupkg layout

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Builds on the previous commit (which switched the aspnetcore artifact to the verbatim
runtime-pack nupkg and fixed the .nupkg extension / extractor / bare runtimes/{rid}
layout). The verbatim nupkg carries the host-resolvable Microsoft.AspNetCore.App.deps.json
+ runtimeconfig.json next to the managed assemblies, so the per-job dotnet home can build
the ASP.NET Core shared framework ENTIRELY from BCS instead of overlaying onto a feed copy.

- CreateBuildCacheDotnetHome: for the aspnetcore flavour, place Microsoft.AspNetCore.App
  directly from the pack (all managed incl deps.json + runtimeconfig.json + native,
  synthesized .version); do not clone the feed copy of that framework. Fail loud
  (BuildCacheIncompleteException) if the pack is missing managed assemblies, deps.json,
  or runtimeconfig.json -- for perf runs, erroring beats silently running mixed/feed bits.
  Base runtime + host stay feed-cloned. New PlaceAspNetFrameworkFromPack +
  ResolveAspNetRuntimesDir (bare layout, wrapped fallback).
- OverlayPublishedOutput (SCD): unchanged behaviour (managed *.dll + native only, app's
  own .deps.json governs); now resolves the pack via ResolveAspNetRuntimesDir.
- Runtime flavour: overlay path unchanged.
- Tests: aspnetcore archive helper now includes deps.json + runtimeconfig.json; FDD test
  asserts pristine direct-placement (metadata present, no feed leak); +2 fail-loud
  negatives. Docs updated. 95/95 unit tests green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@LoopedBard3
LoopedBard3 marked this pull request as ready for review July 30, 2026 22:56
@LoopedBard3 LoopedBard3 added the enhancement New feature or request label Jul 30, 2026
@LoopedBard3 LoopedBard3 added the documentation Improvements or additions to documentation label Jul 30, 2026
@LoopedBard3
LoopedBard3 marked this pull request as draft July 30, 2026 23:03
Refactor the buildcache channel from a single-flavour-per-job selector to
always overriding BOTH the base runtime (Microsoft.NETCore.App, dotnet/runtime)
and the ASP.NET Core shared framework (Microsoft.AspNetCore.App, dotnet/aspnetcore)
in every job, latest-by-default, with independent per-repo pinning.

- Job.cs: replace BuildCacheRepo/BuildCacheCommitSha/BuildCacheBranch/BuildCacheConfig
  with per-repo BuildCacheRuntimeCommitSha/BuildCacheAspNetCoreCommitSha and
  BuildCacheRuntimeBranch/BuildCacheAspNetCoreBranch (config auto-derived per repo).
- JobContext.cs: split BuildCacheExtractDir into runtime + aspnetcore extract dirs.
- BuildCacheClient.cs: rework CreateBuildCacheDotnetHome to a both-flavour signature;
  factor the runtime overlay into OverlayRuntimeIntoHome; each side guarded so a
  single framework can still be overlaid in isolation.
- Startup.cs: drop the --build-cache-repo-name option; resolve+download both repos
  independently; overlay published output per flavour; annotate both RuntimeVersion
  and AspNetCoreVersion with +buildcache.{sha}; clean up both extract dirs.
- Tests: migrate call sites to the new signature; add a both-framework overlay test.
- Docs: rewrite dotnet_versions.md and build_cache_requirements.md for the
  two-SHA/two-branch always-both model.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f12e0666-adff-4aaa-b059-821e8384151d
@LoopedBard3
LoopedBard3 marked this pull request as ready for review August 14, 2026 17:59
@LoopedBard3
LoopedBard3 requested a balanced review from Copilot August 17, 2026 17:29

Copilot AI 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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Suppressed comments (6)

src/Microsoft.Crank.Agent/BuildCacheClient.cs:1135

  • This maps every non-ARM64 Linux host to linux-x64, so the linux-musl-x64 configuration declared above is never selected. On a musl agent, buildcache therefore downloads glibc runtime and ASP.NET artifacts instead of failing as the documentation promises for unsupported musl ASP.NET builds, leading to incompatible binaries. Detect musl explicitly (or reject it before resolution).
            return RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "linux-arm64" : "linux-x64";

src/Microsoft.Crank.Agent/Startup.cs:3049

  • This per-job extract is not registered on JobContext until line 3368. If the ASP.NET download, SDK/runtime installation, cancellation check, or home creation fails first, DeleteJobAsync sees null and leaves this potentially multi-GB directory behind. Register the extract immediately so the existing final cleanup owns it on every later return path.
                        runtimeBuildCacheExtractDir = await BuildCacheClient.DownloadAndExtractAsync(
                            _buildCacheBaseUrl, BuildCacheClient.RepoNameRuntime, runtimeBuildCacheCommitSha, runtimeBuildCacheConfigResolved, cancellationToken);

src/Microsoft.Crank.Agent/Startup.cs:3066

  • Like the runtime extract, this directory is only assigned to JobContext after all installation and isolated-home work succeeds. Any intervening failure bypasses the end-of-job cleanup and leaks the extraction. Register it here immediately after creation.
                        aspNetCoreBuildCacheExtractDir = await BuildCacheClient.DownloadAndExtractAsync(
                            _buildCacheBaseUrl, BuildCacheClient.RepoNameAspNetCore, aspNetCoreBuildCacheCommitSha, aspNetCoreBuildCacheConfigResolved, cancellationToken);

src/Microsoft.Crank.Agent/BuildCacheClient.cs:227

  • User SHAs of 8–39 characters are accepted, but ResolveCommitAsync returns them unchanged and this constructs an exact blob path from that value. Static blob storage cannot expand a commit prefix, so an advertised prefix requests a prefix-named directory and returns 404 instead of resolving the full commit. Either require the exact SHA length used by BCS or resolve prefixes through an index/API before downloading.
            var artifactUrl =
                $"{normalizedBaseUrl}/builds/{Uri.EscapeDataString(repoName)}/buildArtifacts/" +
                $"{Uri.EscapeDataString(commitSha)}/{Uri.EscapeDataString(buildCacheConfig)}/{Uri.EscapeDataString(artifactFile)}";

src/Microsoft.Crank.Models/Job.cs:83

  • The PR description still advertises the controller properties BuildCacheCommitSha, BuildCacheBranch, and BuildCacheConfig, plus a --build-cache-repo-name agent option, but this API instead exposes four repo-specific properties and no config override or repo-name option. Update the PR wire-up and usage sections to match the implemented dual-repository contract so consumers do not use nonexistent options.
        public string BuildCacheRuntimeCommitSha { get; set; } = "";
        public string BuildCacheAspNetCoreCommitSha { get; set; } = "";
        public string BuildCacheRuntimeBranch { get; set; } = "";
        public string BuildCacheAspNetCoreBranch { get; set; } = "";

src/Microsoft.Crank.Agent/BuildCacheClient.cs:275

  • Downloaded archives are intentionally retained with no size, age, or count bound. Per-commit bisection—the primary use case—adds runtime and ASP.NET archives for every tested SHA, so a long-lived agent's temp volume grows indefinitely and can eventually exhaust disk space. Add a bounded cache/eviction policy while preserving active extracts.
        /// <summary>
        /// Deletes a previously-extracted directory. Safe to call multiple times. Archives in the
        /// parent commit dir are intentionally NOT deleted so subsequent jobs for the same commit
        /// can reuse the download.

Comment thread src/Microsoft.Crank.Agent/Startup.cs
Comment thread src/Microsoft.Crank.Agent/BuildCacheClient.cs Outdated
Comment thread src/Microsoft.Crank.Agent/BuildCacheClient.cs
…, RID unify

Fixes three Copilot review comments on the buildcache channel:

- #1 (build reuse): a framework-dependent reused build previously returned
  early before any BCS resolve, so it ran the feed runtime instead of the
  resolved BCS bits. Introduce SHA-keyed persistent dotnet homes under a
  shared LRU root and RefreshBuildCacheForReuseAsync on the reuse path:
  re-resolve current shas (latest advances, pinned is idempotent), attach
  the persistent home on hit or re-materialize on drift/eviction (FDD),
  re-overlay only drifted side(s) for self-contained, re-stamp the two
  +buildcache.{sha} version surfaces, and fail loud on refresh failure.

- #2 (overlay validation): OverlayRuntimeIntoHome now reports per-category
  counts (managed assemblies, native libraries, host binaries) and throws
  unless both managed and native files were copied.

- #3 (RID unification): GetPlatformMoniker collapses Windows X86 to win-x64.

Adds unit tests for the persistent-home key/cache-hit/drift behavior.
Full solution builds clean; unit suite 99/99.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f12e0666-adff-4aaa-b059-821e8384151d
Comment thread docs/dotnet_versions.md Outdated

The difference between `latest` and `edge` is that `latest` will pick runtimes and SDKs that are deemed compatible together. For instance a very recent .NET core runtime might be compatible with a less recent ASP.NET runtime. The `edge` is used to pick the absolute latest build for the select TFM.

The `buildcache` channel uses the Build Cache Service (BCS) from `dotnet-performance-infra` to resolve framework versions by individual commit SHA rather than from VMR feeds. This provides much finer-grained control — every cached commit is available, whereas VMR feeds may have multi-day gaps between ingested commits. On this channel crank overrides **both** the base .NET runtime (`Microsoft.NETCore.App`, from dotnet/runtime) **and** the ASP.NET Core shared framework (`Microsoft.AspNetCore.App`, from dotnet/aspnetcore); each defaults to the latest cached build and can be pinned independently. SDK and desktop versions are resolved from `latest`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would prefer to name it ci, because buildcache sounds too much like an implementation detail.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That makes sense, especially with the other channel names. Went ahead and updated this everywhere user-facing, but kept some of the internals as they truly refer to the buildCache.

Comment thread docs/dotnet_versions.md Outdated
### Bisecting ASP.NET Core (pin aspnetcore, runtime stays latest)

```
> crank --config benchmarks.yml --scenario json --profile aspnet-perf-lin --application.channel buildcache --application.buildCacheAspNetCoreCommitSha a1b2c3d4e5f6...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since we use a custom channel, which makes sense (what feed is the runtime coming from), would it be possible to reuse the exist "version" arguments instead of adding new ones? They will be shas and not actual versions.

And not setting a version would pick the latest one from the ci channel (buildcache)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I could see a world where we could set either a version or a sha, since I assume all builds actually have a version.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Moved over to this approach. The ci channel only supports shas because the Cache is keyed by commit and currently doesn't have any mapping or relation to the longer version strings. We would have to decide how we want to map versions to sha's as the VMR does not have a version for every ASP.NET or Runtime commit, and I am not aware of any other place that would have a version for every SHA (though maybe the mapping doesn't have to cover every SHA for general use).

Comment thread docs/dotnet_versions.md Outdated
### Different branch (per repo)

```
> crank --config benchmarks.yml --scenario json --profile aspnet-perf-lin --application.channel buildcache \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does it mean we can target a release branch and it will pick up the latest available build for this branch?

I was assuming it would only be for the main branches,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't see why this would be two different variables, no way we would use different branches for aspnet/dotnet.

Another option would be to reuse the existing version arguments which already have the convention with patterns. Creating a custom format for wildcard versions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yea, this was an idea of where this could be down the line with the idea that custom branches would be selectable from the cache for testing. Instead, I have removed ciBranch and this flow as a whole as we are currently only building for main so the functionality would not actually work for anything. If we find we need this in the future we can add it then, otherwise specific commits can be used from wherever they are built.

Comment thread docs/dotnet_versions.md
| `buildCacheRuntimeBranch` | `main` | Branch to query for the latest runtime build. |
| `buildCacheAspNetCoreBranch` | `main` | Branch to query for the latest aspnetcore build. |

The BCS configuration key (e.g., `coreclr_x64_linux` for runtime, `aspnetcore_x64_linux` for aspnetcore) is auto-detected per repo from the agent platform. Platforms with no aspnetcore config (there is no macOS/musl/arm32 in v1) fail loud rather than silently skipping.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is the kind of idea I am having when suggesting to use existing arguments, and base the logic on the channel.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Landed on this IIUC.

LoopedBard3 and others added 3 commits August 18, 2026 10:15
On the buildcache channel the base shared-framework folder is always resolved
to the latest feed version and the BCS build is overlaid/placed on top, so a
user-supplied runtimeVersion/aspNetCoreVersion was silently ignored. Add a
fail-loud guard that rejects (job.Error) any non-empty, non-"latest" value for
those fields on this channel, steering users to buildCacheRuntimeCommitSha /
buildCacheAspNetCoreCommitSha for pinning/bisection. Document the behavior in
docs/dotnet_versions.md and the controller README.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f12e0666-adff-4aaa-b059-821e8384151d
Rename the user-facing channel 'buildcache' to 'ci'. Reuse runtimeVersion/
aspNetCoreVersion to carry a commit SHA on the ci channel (empty = latest;
a version string is rejected with a clear, arg-named error). Collapse the
four per-repo BuildCache* commit/branch Job fields into a single ciBranch
(default main), used only for latest-lookup resolution. Change the reported
version suffix from +buildcache.{sha} to +ci.{sha}.

Runtime path behavior is unchanged; aspnetcore support remains additive.
Updates docs (dotnet_versions.md, build_cache_requirements.md), Controller
README, and adds unit coverage for IsCommitSha / TryResolveCiVersionPin.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f12e0666-adff-4aaa-b059-821e8384151d
On the ci channel, fold the "+ci.{12-char sha}" build-cache stamp into the
existing AspNetCoreVersion / NetCoreAppVersion result measurements instead of
adding separate keys. These measurements are what land in the controller
--json results and in the SQL/ES document as
application.aspNetCoreVersion / netCoreAppVersion, so BCS bisection runs are
now self-identifying in the perf database (e.g. 8.0.30+ci.8f51f037469f).

- Fresh-build path: the existing .version-file-derived measurement values gain
  a "ci." marker only when useBuildCache (non-ci runs stay {ver}+{sha}).
- Reuse path: records the same AspNetCoreVersion / NetCoreAppVersion keys with
  the {feedVer}+ci.{sha} value (the reuse path returns before the fresh-build
  recording, so without this a reused ci run would surface no version).
- Dropped the additive AspNetCoreCiVersion / NetCoreAppCiVersion keys and the
  Measurements constants; the value now lives in the existing fields.
- Internal DTO job.RuntimeVersion / job.AspNetCoreVersion keep their existing
  +ci.{8-char} stamp (unchanged); surfaced measurements use 12-char to match
  the sibling .version measurements.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f12e0666-adff-4aaa-b059-821e8384151d
@LoopedBard3 LoopedBard3 changed the title Add buildcache channel for Build Caching Service runtime resolution Add ci channel for per-commit Build Cache Service framework resolution Aug 19, 2026
The BCS pipeline only builds main, so no other branch's latest index is ever
published -- ciBranch was a non-functional knob. Remove it entirely; the
'latest' lookup (empty runtimeVersion/aspNetCoreVersion) now always targets
main. A commit SHA still pins one repo while the other stays latest, so
independent per-repo bisection is preserved.

- Job.cs: remove the CiBranch property, its GetBuildKeyData assignment, and the
  BuildKeyData.CiBranch field (dropped from the build key entirely).
- Startup.cs: both resolution sites pass the literal "main" into
  ResolveCommitAsync instead of a job.CiBranch-derived local. BuildCacheClient's
  branch parameter is unchanged (still builds the latest/{branch}/... URL).
- docs/dotnet_versions.md + Controller README: drop the ciBranch example, table
  row, CLI entry, and branch-parity note; reword "latest on ciBranch" to main.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f12e0666-adff-4aaa-b059-821e8384151d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants