Skip to content

[Draft] Add perf-build.yml: per-commit ASP.NET Core build → Build Cache Service#2

Closed
LoopedBard3 wants to merge 5 commits into
mainfrom
loopedbard3/aspnetcore-perf-build-pipeline
Closed

[Draft] Add perf-build.yml: per-commit ASP.NET Core build → Build Cache Service#2
LoopedBard3 wants to merge 5 commits into
mainfrom
loopedbard3/aspnetcore-perf-build-pipeline

Conversation

@LoopedBard3

@LoopedBard3 LoopedBard3 commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Draft — fork-staged for review.

What

Adds .azure/pipelines/perf-build.yml — a new Azure DevOps pipeline that builds every commit on main for dotnet/aspnetcore (Windows multi-arch + Linux x64 + Linux arm64) and uploads the per-RID Microsoft.AspNetCore.App.Runtime.{rid} packs to the .NET Performance team's Build Cache Service (BCS) under builds/aspnetcore/buildArtifacts/{sha}/{configKey}/.... dotnet/crank then resolves Microsoft.AspNetCore.App runtime binaries by commit SHA for performance-regression bisection.

Modeled directly on dotnet/runtime's eng/pipelines/performance/perf-build.yml.

Architecture — three-stage shape (mirrors runtime)

RegisterBuild  ─┐  (parallel, writes buildInfo.json per configKey)
build          ─┤  (parallel, produces pipeline artifacts)
                │
UploadArtifacts ┘  (dependsOn: [build, RegisterBuild],
                    uploads archives next to the markers)
  • RegisterBuild — delegates to register-build-jobs.yml@performance (companion Generalize BCS upload + register-build templates with repoName parameter dotnet/performance#5241), which writes builds/aspnetcore/buildArtifacts/{sha}/{configKey}/buildInfo.json for each config in parallel with the actual build. Cheap, but matters: it gives this repo BCS-layout symmetry with runtime, so any future dotnet-performance-infra tooling that reads buildInfo.json works uniformly across both repos.

  • build — three jobs (Windows multi-arch in one job following ci.yml's Windows_build pattern; Linux x64; Linux arm64 cross-built from x64 host). Each invokes the existing default-build.yml@self template with -pack -all --no-build-java -p:OnlyPackPlatformSpecificPackages=true, then a post-build step unzips the per-RID Microsoft.AspNetCore.App.Runtime.{rid}.{ver}.nupkg into a microsoft.aspnetcore.app.runtime.{rid}/Release/runtimes/{rid}/... layout and zips/tars it as a pipeline artifact.

  • UploadArtifacts (internal + IndividualCI/Manual only) — delegates to upload-build-artifacts-jobs.yml@performance (also companion Angular 6 multi project SPA  dotnet/aspnetcore#5241). One template call dispatches into five per-configKey upload jobs:

    buildType:
      - aspnetcore_x64_linux
      - aspnetcore_arm64_linux
      - aspnetcore_x64_windows
      - aspnetcore_arm64_windows
      - aspnetcore_x86_windows
    repoName: aspnetcore
    

    The shared template handles the az storage blob upload against pvscmdupload/$web/builds/aspnetcore/buildArtifacts/{sha}/{configKey}/{file}.

Not extending 1ES.Official

Unlike ci.yml (which ships signed bits) or ci-unofficial.yml (which validates PRs), this pipeline produces intermediate build artifacts that go to a private BCS cache — they aren't signed or redistributed. SDL / PoliCheck / TSA scans add no security value for that workload. Runtime's own perf-build runs as a plain non-1ES pipeline against the same .NET Performance Azure service connection and has done so for years; we follow the same pattern. This drops ~30 LOC of 1ES boilerplate (the 1esPipelines resource, the extends: block, the sdl: config) and lets jobs inherit the pipeline's default pool naturally.

If dnceng/internal AzDO ever rejects a non-1ES pipeline in this project (unverified — we'd be the first non-1ES one in .azure/pipelines/), the fallback is a one-line switch to 1ES.Unofficial.PipelineTemplate.yml (used today by ci-unofficial.yml and identitymodel-helix-matrix.yml).

Atomicity

The MissingBuildsTrigger Azure Function in internal/dotnet-performance-infra indexes only Azure DevOps builds with overall Status == Succeeded. All stages run with the default continueOnError: false, so any RegisterBuild / build / UploadArtifacts failure sinks the overall build to Failed and the Function skips indexing for that SHA. Operator retries the whole build green. The atomic unit is the AzDO build, not the per-config job.

Archive contract (load-bearing)

Per-RID archive root MUST be microsoft.aspnetcore.app.runtime.{rid} (lowercase, matching the canonical name an extracted nupkg lands at). The future aspnetcore-overlay crank PR will hardcode this name in a FindDirectory(extractDir, $"microsoft.aspnetcore.app.runtime.{rid}") call mirroring crank PR 878's runtime overlay. Renaming the archive root breaks every future crank consumer.

Archive contents:

microsoft.aspnetcore.app.runtime.{rid}/
  Release/
    runtimes/{rid}/
      lib/net{X}.0/Microsoft.AspNetCore.*.dll
      native/aspnetcorev2*.dll       (Windows only — ANCM/IIS bits)

configKey set (v1 lock)

aspnetcore_x64_linux, aspnetcore_arm64_linux, aspnetcore_x64_windows, aspnetcore_arm64_windows, aspnetcore_x86_windows. macOS + musl + Linux arm32 deferred to v2 (not current crank perf scenarios). Once uploaded, renaming requires storage-level migration — crank caches latestBuilds.json for 1 hour.

Sequencing / dependencies

  1. Generalize BCS upload + register-build templates with repoName parameter dotnet/performance#5241 must merge first — this pipeline resolves register-build-jobs.yml@performance and upload-build-artifacts-jobs.yml@performance through the internal/dotnet-performance AzDO mirror, which only updates after dotnet/performance/main advances.
  2. dotnet/aspnetcore PR (this one, when retargeted to upstream) merges.
  3. Pipeline definition created in dnceng/internal AzDO; record the pipeline ID.
  4. Manually queue one perf-build run; verify three stages run and archives + buildInfo.json land in pvscmdupload/$web/builds/aspnetcore/buildArtifacts/{sha}/{configKey}/.
  5. (Future, separate PR.) Update internal/dotnet-performance-infra so MissingBuildsTrigger handles aspnetcore configKeys + the aspnetcore pipeline. After deploy, latestBuilds.json at builds/aspnetcore/latest/main/latestBuilds.json lands on the next 8h tick. End-to-end with crank.

Out of scope (tracked, not done here)

  • Crank-side: extend PlatformToBcsConfig with the aspnetcore mapping + add an aspnetcore overlay code path. Separate future crank PR.
  • internal/dotnet-performance-infra indexer/schema changes. Separate future PR — needs (a) extending the dispatcher's known config-key list, (b) registering the new aspnetcore pipeline ID, and (c) resolving the builds/{repoName}/ blob-path mystery on the indexer side.
  • release/X.Y branches. v1 ships main only.

File-level changes

  • Added: .azure/pipelines/perf-build.yml (three-stage, non-1ES)
  • Deleted (in a prior commit on this branch): .azure/pipelines/jobs/perf-build-upload-job.yml (functionality replaced by the dotnet/performance shared template)

LoopedBard3 and others added 2 commits June 2, 2026 15:47
Adds a new internal-only Azure DevOps pipeline that builds every commit
on main and uploads per-RID ASP.NET Core runtime packs to the Build
Cache Service (BCS, `pvscmdupload`) so dotnet/crank can resolve
`Microsoft.AspNetCore.App` runtime binaries by commit SHA for
performance-regression bisection. Modeled on dotnet/runtime's
`eng/pipelines/performance/perf-build.yml` but adapted to aspnetcore's
1ES extension model (`v1/1ES.Official.PipelineTemplate.yml@1esPipelines`
+ `.azure/pipelines/jobs/default-build.yml@self`).

Pipeline structure:

* Single `build` stage with 3 jobs that reuse the existing
  `default-build.yml` template: `Windows_build` (x64+x86+arm64 in
  one job, mirroring `ci.yml`'s `Windows_build` pattern),
  `Linux_x64_build`, `Linux_arm64_build`. Each job runs the normal
  build.cmd/build.sh `--pack --all -p:OnlyPackPlatformSpecificPackages=true`
  invocation, then a pwsh afterBuild step packs the per-RID nupkg into
  a BCS-shaped archive (lowercase
  `microsoft.aspnetcore.app.runtime.{rid}/Release/runtimes/{rid}/...`
  root — the lowercase root is a load-bearing contract for the future
  aspnetcore-overlay crank PR, so the packer asserts the archive root
  after creation).
* Conditional `UploadArtifacts` stage gated to
  `internal + (IndividualCI | Manual)` runs (Manual is included so
  the runbook's first seed-run can produce uploads). Five per-config
  jobs use the new helper template
  `jobs/perf-build-upload-job.yml`, one per configKey:
  `aspnetcore_x64_linux`, `aspnetcore_arm64_linux`,
  `aspnetcore_x64_windows`, `aspnetcore_arm64_windows`,
  `aspnetcore_x86_windows`.

Per-config upload-job atomicity invariant (load-bearing): a single
`AzureCLI@2` task does, in order, (1) `az storage blob upload`
the archive to
`builds/aspnetcore/buildArtifacts/{sha}/{configKey}/{archiveFile}`,
(2) HEAD-verify the static-website URL with a short retry loop
(defaults: 6 attempts × 10 s, both tunable via pipeline parameters
`headRetryCount` / `headRetryDelaySeconds` so post-launch CDN
visibility tuning is a YAML edit), and (3) ONLY IF (2) succeeded,
upload `{"buildId":"..."}` to `buildInfo.json`. Any failure
throws, fails the job (`continueOnError: false`, the default),
which marks the overall AzDO build `PartiallySucceeded` and
causes the MissingBuildsTrigger Azure Function to skip indexing
this SHA entirely (whole-pipeline-indexing contract — operator
retries the whole build green).

Scope: this commit only adds the aspnetcore-side YAML on this
fork/branch. The three companion changes — dotnet-performance
`repoName` leaf-template PR, dotnet-performance-infra schema
PR, and dotnet-performance-infra `MissingBuildsTriggerAspNetCore`
Function App fork PR — are tracked separately and will be drafted
later.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the inline .azure/pipelines/jobs/perf-build-upload-job.yml
(deleted) with a single call to dotnet/performance's generalized
upload-build-artifacts-jobs.yml dispatcher (the same template
runtime's perf-build.yml uses). Companion change in dotnet/performance
parameterizes the dispatcher + leaf templates with repoName and pool*.

* Adds 'performance = internal/dotnet-performance' to resources.repositories.
* Drops the headRetryCount/headRetryDelaySeconds parameters and the
  _aspNetCoreBcs* variables — the shared template handles account
  name, container, base URL, and subscription internally and runtime's
  pipeline has shipped without an explicit HEAD-verify step from the
  start. The MissingBuildsTrigger Function uses the AzDO build's
  overall Status == Succeeded as the sole atomicity gate.
* Replaces the five inline perf-build-upload-job.yml@self template
  calls with a single upload-build-artifacts-jobs.yml@performance
  dispatch passing buildType: [aspnetcore_x64_linux,
  aspnetcore_arm64_linux, aspnetcore_x64_windows,
  aspnetcore_arm64_windows, aspnetcore_x86_windows], repoName:
  aspnetcore, and the dnceng internal 1ES Ubuntu pool (required
  because 1ES.Official mandates every job declare a pool).

Net diff: 142 lines of perf-build.yml edited, perf-build-upload-job.yml
(137 lines) deleted. Build stage unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LoopedBard3 and others added 3 commits June 11, 2026 16:05
Two related changes:

1. Drop 1ES.Official extension. Per-commit perf-build artifacts go to a
   private BCS cache (not signed / redistributed), so SDL / PoliCheck /
   TSA scans add no security value here. Matches runtime's perf-build
   which also runs as a plain (non-1ES) pipeline against the same
   .NET Performance Azure service connection. Removes ~30 LOC of
   1ES boilerplate (resources entry for 1esPipelines, extends block,
   sdl config) and lets jobs inherit the pipeline's default pool
   naturally.

2. Adopt runtime's three-stage shape:

     RegisterBuild  ─┐  parallel (dependsOn: [])
     build          ─┤  parallel
                     │
     UploadArtifacts ┘  dependsOn: [build, RegisterBuild]

   The new RegisterBuild stage delegates to
   register-build-jobs.yml@performance (with repoName: aspnetcore)
   which writes a tiny per-configKey buildInfo.json marker to
   builds/aspnetcore/buildArtifacts/{sha}/{configKey}/buildInfo.json.
   Matches the layout of the runtime perf-build so any future
   dotnet-performance-infra dashboard / cleanup tool that reads
   buildInfo.json works uniformly for both repos.

   Companion change in dotnet/performance parameterizes
   register-build-job.yml with repoName.

UploadArtifacts now depends on both Build and RegisterBuild with
condition: succeeded() — any stage failure sinks the overall AzDO
build to Failed and the MissingBuildsTrigger Function skips indexing
for the SHA (whole-pipeline-indexing contract preserved).

Pool params previously passed to the upload dispatcher are removed
(they only existed to satisfy 1ES.Official's pool requirement; without
1ES, jobs inherit the pipeline default pool).

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

The shared dotnet/performance leaf template upload-build-artifacts-job.yml
hardcodes:
    condition: eq(stageDependencies.Build.<job>.result, 'Succeeded')

In Azure Pipelines expression evaluation, stageDependencies.<stageName>
is case-sensitive (verified via Microsoft Learn / Azure DevOps docs).
The previous lowercase 'stage: build' here would silently resolve to a
null result, every per-artifact upload job's condition would evaluate
false, and NO artifacts would ever land in BCS — but the pipeline would
report as Succeeded since no job actually failed. A true silent failure.

Capitalizing to 'Build' matches dotnet/runtime's perf-build.yml convention
and the leaf template's hardcoded reference. dependsOn list for the
UploadArtifacts stage updated to match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The three build jobs (Windows x64/x86/arm64, Linux x64, Linux arm64) each
carried a near-verbatim copy of the "find the runtime-pack nupkg, extract it
into the lowercase microsoft.aspnetcore.app.runtime.{rid}/Release/ layout, and
zip it" recipe in their afterBuild hook. Replace all three with a single,
parameterized .azure/pipelines/tools/pack-bcs-archives.ps1 (-Rids / -Format)
so there is exactly one copy of the find-and-zip logic to maintain.

Verified the script's nupkg assumptions against real published packages
(Microsoft.AspNetCore.App.Runtime.{linux-x64,win-x64} 8.0.28): the runtime
pack name, the runtimes/{rid}/lib managed-assembly layout, the win native dir,
and the absence of pdb/dbg all match what the recipe expects.

Invoke via pwsh (PowerShell 7), not Windows PowerShell 5.1: 5.1's
Compress-Archive writes backslash path separators into the zip, which a .NET
ZipFile extraction on a Linux crank agent treats as one flat filename, so
crank's FindDirectory("microsoft.aspnetcore.app.runtime.{rid}") would never
match. The prior inline Windows block used powershell: (5.1) and would have
produced corrupt Windows archives. The archive-root assertion now also runs
under pwsh and fails the build loudly if the contract is ever broken.

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

Copy link
Copy Markdown
Owner Author

Superseded: this perf-build pipeline is being moved into dotnet/performance for perf-team merge control. The performance-hosted version rebuilds aspnetcore from source (it cannot reuse aspnetcore's default-build.yml due to @self resolving to the host repo) and triggers per-commit via a repository-resource CI trigger on the dotnet-aspnetcore mirror. Closing in favor of that work. Branch retained for reference.

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