Skip to content

Reduce StaticHost publishing time and add full-site CI diagnostics - #1879

Draft
David Pine (IEvangelist) wants to merge 6 commits into
mainfrom
ievangelist-build-timing-analysis
Draft

David Pine (IEvangelist) wants to merge 6 commits into
mainfrom
ievangelist-build-timing-analysis

Conversation

@IEvangelist

@IEvangelist David Pine (IEvangelist) commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Summary

Default StaticHost Brotli publishing to Optimal instead of the SDK's SmallestSize, preserving overrides, original content, HTTP negotiation, and immutable caching. Add phase diagnostics, integrity/HTTP regression coverage, and reusable opt-in profiling. Correct the explicitly approved pre-existing dashboard test's two stale persistence-path expectations without changing documentation/runtime behavior.

Measured tradeoff, identical corpus and runner

Full comparison 38449500455 passed on commit 31861a38bf6b3084d081c0a7cf8fcb00e2c703ff. Frontend content was rendered once and SHA-256-verified; both clean prebuilt publishes used unchanged binaries/configuration and matching OCI base layers on the same Ubuntu x64, four-CPU runner with SDK 10.0.401/runtime 10.0.12.

Metric SmallestSize Optimal Tradeoff
Brotli task 4,172.315 s 38.766 s 107.6x faster
Prebuilt publish 74m 58s 6m 19.61s No second frontend render
Publish aggregate CPU 16,946.82 s 467.43 s 97.24% lower
Brotli bytes, 57,966 files 1,305,077,366 1,559,240,776 +19.475%
Published wwwroot bytes 9,997,705,146 10,251,868,556 +2.542%
Actual OCI archive bytes 3,466,521,600 3,720,699,904 +7.332%
File copying 51.873 s 52.580 s Essentially unchanged
Container creation 199.199 s 207.586 s About 8.4 s longer

Raw task/CPU/RSS/directory/size results are in statichost-publish-performance/comparison/comparison.json. Detailed results comment contains test commands and measurements. The first 90-minute attempt was incomplete; the maintainer approved the successful three-hour rerun. No incomplete baseline totals were extrapolated.

Corrected browser/cache follow-up

I found that Playwright routing disabled HTTP caching in the first browser profile. Those interceptor-based browser results are superseded and not used for conclusions. Native Chromium URL blocking now preserves caching, and warm samples must show actual cache hits.

Browser-only follow-up 38598948534 passed complete CI on current commit 2935fc9ad7a3b1e1dc70e3953d9e9ded9333f22d. It reused the previous run's generated frontend and required exactly matching representative-page hashes and Brotli bytes, without recompressing the full API catalog. All 96 browser observations passed; warm navigations reused at least 37 resources.

For cold mobile-like visits, Optimal added 25-106 KB of total page transfer across four samples, about 11.5-16.3%. Median HTML download increased 42-62 ms and FCP increased 40-56 ms under the 5 Mbps/40 ms/4x-CPU lab profile. LCP medians were mixed, with overlapping ranges in most cases; three pairs do not establish significance. Warm mobile-like visits transferred 600 B for both settings on all pages. Native decoding from memory was 15-31% faster with Optimal, under 0.7 ms for both settings.

Corrected browser results comment has per-page transfer/paint tables. Raw observations are in statichost-browser-performance/browser-comparison.json.

Profiling feature and remaining bottlenecks

  • build-profile: frontend CPU/phase/output capture and full StaticHost publish diagnostics.
  • compression-profile: controlled complete-corpus publishing, archive, CPU/RSS, and browser comparison, with the approved three-hour limit. Publish-only diagnostics remain capped at 45 minutes.
  • browser-profile: lighter cache/browser-only repetition from the latest successful paired corpus on the same PR branch.

Measured next targets include approximately 22m of frontend rendering in the profiled CI run, up to 15m in a browser-test shard, 3-4m artifact downloads, about 3.5m container creation, and roughly 2 GB RSS for the complete SDK route table in isolated Kestrel. Large API landing pages and output volume also warrant targeted profiling.

These are CI/lab measurements. The browser-only fixture has fewer routes, so its startup/RSS is not the full-site origin footprint. Production Front Door/TLS/App Service scaling, real-user/physical-device behavior, and worst-case browser pages remain explicit coverage gaps. This PR does not claim that all production bottlenecks are understood.

The PR remains draft for evaluation of the measured cold-visit/size tradeoff. Nothing was merged, deployed, or changed in Azure pipeline configuration.

Third-party links and affiliations

None.

Validation

  • dotnet test tests\StaticHost.Tests\StaticHost.Tests.csproj --configuration Release --filter "Category!=RedisIntegration" --verbosity minimal — passed, 324 tests.
  • dotnet build tests\performance\CompressionProfile\CompressionProfile.csproj --configuration Release --verbosity minimal — passed without warnings/errors.
  • pnpm --dir src\frontend exec vitest run --config vitest.config.ts tests/unit/compression-profile.vitest.test.ts tests/unit/build-performance.vitest.test.ts — passed, 10 tests after adding the cache guard.
  • pnpm --dir src\frontend exec eslint scripts/profile-static-host-compression.mjs tests/unit/compression-profile.vitest.test.ts --max-warnings 0 — passed.
  • Bash syntax checks for both profiling drivers — passed.
  • actionlint -shellcheck= -pyflakes= .github\workflows\apphost-build.yml .github\workflows\ci.yml — passed; ShellCheck/pyflakes disabled.
  • git diff --check — passed.
  • Tiny-fixture checks — passed: clean prebuilt republishing, OCI inspection, exact binary-log timing extraction, representative subset publishing, and genuine warm-cache observations.
  • Full identical-corpus publish/size comparison — passed.
  • Corrected 96-observation browser/cache follow-up and full current CI gate — passed, including all six desktop/tablet/mobile shards, frontend build/lint/unit validation, AppHost/StaticHost/Redis coverage, CodeQL, tools, and release checks.
  • No local Astro/production frontend build was run. No merge or deployment was performed.

Preserve Brotli negotiation and immutable asset caching, cover isolated publishes, and add opt-in full-site CI diagnostics.

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

Copilot-Session: 3761b826-bc44-40e0-99dd-a3162446f1a6
@IEvangelist David Pine (IEvangelist) added the build-profile Opt in to frontend CPU profiling on the next PR CI run label Oct 13, 2026
Exercise the full-site verifier against incorrect compressed content without mutating shared publish fixtures.

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

Copilot-Session: 3761b826-bc44-40e0-99dd-a3162446f1a6
@aspire-repo-bot

Copy link
Copy Markdown
Contributor

Frontend HTML artifact ready

The latest frontend build uploaded the frontend-dist artifact for PR #1879. Use the VS Code button below to open this PR with GitHub Artifacts Explorer and browse the built HTML locally.

VS Code: Open PR #1879 artifacts

This comment updates automatically when a new frontend build artifact is uploaded.

Comment thread CONTRIBUTING.md

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.

None of these changes to this file are needed/useful to anyone actually contributing to this project. Please undo these changes.

@IEvangelist David Pine (IEvangelist) removed the build-profile Opt in to frontend CPU profiling on the next PR CI run label Oct 13, 2026
Update the stale mount and data-directory expectations to match the corrected non-root dashboard example in both shell variants.

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

Copilot-Session: 3761b826-bc44-40e0-99dd-a3162446f1a6
@IEvangelist
David Pine (IEvangelist) marked this pull request as ready for review October 13, 2026 15:22
Copilot AI balanced review requested due to automatic review settings October 13, 2026 15:22

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 profiling job remains unreachable for frontend-only pull requests despite the documented label opt-in.

1 open finding
What changed in this PR

This PR reduces StaticHost publish time by changing the default Brotli compression level and adds publishing diagnostics and regression coverage.

Changes:

  • Defaults Brotli publishing to Optimal while preserving overrides.
  • Adds publish, HTTP negotiation, integrity, and cache-header tests.
  • Adds opt-in full-site CI profiling and contributor guidance.
File Description
tests/​StaticHost.Tests/​StaticHost.Tests.csproj Copies publishing fixtures into test output.
tests/​StaticHost.Tests/​PublishingTargetsTests.cs Tests compression, delivery, and diagnostics.
tests/​scripts/​Measure-StaticHostPublish.ps1 Verifies output and records metrics.
src/​statichost/​StaticHost/​StaticHost.csproj Imports publishing targets.
src/​statichost/​StaticHost/​Publishing.targets Configures Brotli and timing logs.
src/​frontend/​tests/​unit/​dashboard-image-references.vitest.test.ts Corrects persistence expectations.
CONTRIBUTING.md Documents publishing behavior and profiling.
.github/​workflows/​ci.yml Passes the profiling opt-in.
.github/​workflows/​apphost-build.yml Adds the diagnostic publish job.

🧠 Review effort: Balanced


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

Comment thread .github/workflows/ci.yml
if: ${{ needs.changes.outputs.apphost == 'true' }}
uses: ./.github/workflows/apphost-build.yml
with:
profile_publish: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'build-profile') }}
@IEvangelist David Pine (IEvangelist) added the compression-profile Controlled Brotli size, publishing, and browser performance comparison label Oct 13, 2026
@IEvangelist
David Pine (IEvangelist) marked this pull request as draft October 13, 2026 17:05
Add opt-in controlled SmallestSize versus Optimal publishing, image, CPU, memory, native decode, and cold/warm Chromium measurements without rendering the frontend twice.

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

Copilot-Session: 3761b826-bc44-40e0-99dd-a3162446f1a6
Keep ordinary publish diagnostics capped at 45 minutes; use the maintainer-approved three-hour limit only for the full compression comparison.

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

Copilot-Session: 3761b826-bc44-40e0-99dd-a3162446f1a6
@IEvangelist

Copy link
Copy Markdown
Member Author

Controlled compression results

CI run 38449500455 passed on commit 31861a38bf6b3084d081c0a7cf8fcb00e2c703ff. The paired profile used the same generated frontend, SHA-256-verified content, unchanged StaticHost binaries/configuration, and matching OCI base layers on one Ubuntu 24.04 x64 runner with four CPUs, SDK 10.0.401, and runtime 10.0.12.

The frontend was rendered once. Each variant then used a clean compression/publish directory and dotnet publish --no-build; the figures below exclude a second frontend render.

Measurement SmallestSize Optimal Tradeoff
MSBuild BrotliCompress 4,172.315 s, 69m 32s 38.766 s 107.6x faster, 99.07% less time
Complete prebuilt publish 74m 58s 6m 19.61s Includes metadata, copying, verification, and image creation
Publish CPU, user + system 16,946.82 s 467.43 s 97.24% less aggregate CPU
Publish peak RSS 6,137,976 KiB 5,749,352 KiB About 6.3% lower
Brotli payload, 57,966 files 1,305,077,366 B 1,559,240,776 B +19.475%, +254,163,410 B
Published wwwroot 9,997,705,146 B 10,251,868,556 B +2.542%
Actual OCI archive 3,466,521,600 B 3,720,699,904 B +7.332%, +254,178,304 B
File copying 51.873 s 52.580 s Essentially unchanged
Container creation 199.199 s 207.586 s About 8.4 s longer

Directory totals, exact task timings, CPU/RSS measurements, and raw data are in statichost-publish-performance, particularly comparison/comparison.json.

Representative document bytes and decoding

Page Original bytes SmallestSize Brotli Optimal Brotli Additional body bytes
Home 284,026 42,419 50,985 8,566
First-app guide 349,864 47,046 57,240 10,194
Median-sized C# API page 291,958 40,359 48,052 7,693
Median-sized TypeScript API page 189,528 36,986 43,815 6,829

Those HTML-body deltas alone model approximately 11-16 ms of additional transfer at 5 Mbps, or 55-82 ms at 1 Mbps. They exclude CSS/JS/resources, RTT, protocol behavior, and CDN caching; they are not observed page-load penalties.

Isolated native .NET BrotliStream decoding from memory was 15-31% faster with Optimal on these documents, with medians below 0.7 ms for both variants. This is not Chromium network-process CPU.

Browser measurement correction

I found a validation flaw in the first browser profile: Playwright request routing disables HTTP caching. Consequently, its samples labelled "warm" did not exercise a warm HTTP cache, and the interceptor also makes the throttled-network observations unsuitable for conclusions. I am not using those browser samples to recommend a setting.

The profiler now uses Chromium-native URL blocking without a request interceptor and explicitly requires warm navigations to show actual cache hits. All 48 tiny-fixture observations passed that corrected check. A browser-only follow-up using the same generated pages/assets is being prepared so the expensive full-corpus baseline need not be repeated.

Other measured bottlenecks and coverage

  • Frontend build in this CI run: 22m 22s.
  • Slowest browser-test shard: 14m 55s. Its longest individual navigation/hover contract was 116.53s and intentionally checks every token across repeated navigations.
  • Browser-shard artifact downloads: 2m 49s-4m 23s; frontend artifact upload: 2m 57s.
  • Earlier Azure pipeline timelines separately show setup/scheduling, SBOM, governance, and upload costs. Parallel stages are not summed into elapsed time.
  • Production Front Door/App Service behavior, origin scaling/cold starts, field RUM, and worst-case browser pages remain explicit coverage gaps. CI diagnostics are not proof that every production bottleneck is understood.

Validation

  • dotnet test tests/StaticHost.Tests/StaticHost.Tests.csproj --configuration Release --filter "Category!=RedisIntegration" --verbosity minimal: passed, 324 tests.
  • Focused compression-profile/build-performance Vitest tests: passed, 9 tests.
  • Profiler build, focused ESLint, Bash syntax, actionlint expression validation, and git diff --check: passed.
  • Complete CI on the measured commit, including all six desktop/tablet/mobile browser shards: passed.
  • No merge, deployment, or production configuration change was performed.

@IEvangelist David Pine (IEvangelist) added browser-profile Browser/cache-only profiling from the latest complete paired corpus and removed compression-profile Controlled Brotli size, publishing, and browser performance comparison labels Oct 14, 2026
Replace cache-disabling request interception with Chromium URL blocking. Reuse a completed paired corpus for SHA-verified representative browser-only profiling, requiring identical Brotli bytes and genuine cache hits.

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

Copilot-Session: 3761b826-bc44-40e0-99dd-a3162446f1a6
@IEvangelist

Copy link
Copy Markdown
Member Author

Corrected browser/cache results

The browser-only job in CI run 38598948534 passed on commit 2935fc9ad7a3b1e1dc70e3953d9e9ded9333f22d.

This follow-up reused the completed full-corpus run's generated frontend. Each representative page's SHA-256 and Brotli byte count had to match the original SmallestSize/Optimal measurements exactly. The full API catalog was not recompressed.

These results supersede the earlier interceptor-based browser samples. Chromium-native URL blocking keeps HTTP caching enabled, and warm navigations must produce real cached-resource hits. All 96 observations, 48 per setting, passed the content and cache checks. Warm navigations reused at least 37 cached resources.

Environment: Chromium 149.0.7827.55, Node 24.21.0, Ubuntu x64. Each setting used three cold/warm pairs per page/scenario. Mobile-like means a 390x844 viewport, 5 Mbps downstream, 40 ms simulated latency, and 4x CPU throttling.

Cold mobile-like visits

Page transfer includes the main document and same-origin resources, not just HTML.

Page SmallestSize transfer Optimal transfer Additional bytes Transfer increase
Home 304,396 B 340,001 B 35,605 B 11.70%
Guide 654,467 B 760,864 B 106,397 B 16.26%
C# API sample 223,862 B 250,049 B 26,187 B 11.70%
TypeScript API sample 218,138 B 243,149 B 25,011 B 11.47%
Page HTML download, SmallestSize HTML download, Optimal FCP, SmallestSize FCP, Optimal
Home 153.4 ms 204.0 ms 368 ms 420 ms
Guide 180.1 ms 241.9 ms 352 ms 404 ms
C# API sample 147.1 ms 189.3 ms 364 ms 420 ms
TypeScript API sample 116.8 ms 164.8 ms 372 ms 412 ms

These descriptive medians show 42-62 ms more document download time and 40-56 ms higher FCP with Optimal in this throttled lab profile.

LCP medians were mixed, not a consistent regression: home 648→596 ms, guide 756→672 ms, C# 632→616 ms, and TypeScript 600→628 ms. Ranges overlap for most comparisons. Three pairs are insufficient to establish statistical significance or a field-performance guarantee.

Warm visits

In the mobile-like warm-cache profile, transfer was 600 B for both settings on all four pages, with 37-64 cached resources. Document response/download spans were approximately 44-46 ms for both variants. The larger compressed payload therefore did not add body-transfer bytes in these cached revisits.

Desktop cold LCP medians differed by -40 to +12 ms across these pages. Warm desktop transfer was 600-900 B for both settings. The dominant measurable penalty is the cold, bandwidth-constrained transfer, not server TTFB or native decompression.

Scope and interpretation

  • The full-site build/size result remains valid: compression 107.6x faster, Brotli payload +19.475%, published wwwroot +2.542%, and actual OCI archive +7.332%.
  • This browser fixture has fewer SDK routes than the full site. Its startup/RSS is not a measurement of the complete 184k-endpoint origin footprint.
  • The earlier full-route Kestrel measurements showed approximately 2 GB RSS and 3.4-3.7 s initialization in both variants. That footprint is a separate investigation target.
  • Front Door, TLS, deployed App Service scaling, physical devices, field RUM, and worst-case API landing pages were not measured.
  • Raw cache/paint/transfer observations and provenance are in statichost-browser-performance/browser-comparison.json.

Validation

  • Browser-only profile and its exact-content/Brotli/cache-hit checks: passed.
  • Focused profiling/build-performance Vitest contracts: passed, 10 tests.
  • StaticHost unit/HTTP regressions: passed, 324 tests.
  • Profiler build, ESLint, Bash syntax, actionlint, and git diff --check: passed.
  • The remainder of the final CI run is still being verified. No merge or deployment was performed.

@IEvangelist

Copy link
Copy Markdown
Member Author

Final validation status

CI run 38598948534 completed successfully on 2935fc9ad7a3b1e1dc70e3953d9e9ded9333f22d: complete CI gate, all six desktop/tablet/mobile browser shards, frontend production build/lint/unit checks, AppHost/StaticHost/Redis tests, corrected browser-cache profile, CodeQL, tools-generator checks, and release checks all passed.

The full-corpus build/size figures and corrected browser-cache figures are preserved in the two detailed comments above and in the PR body. The expensive full compression comparison was not repeated for the cache correction.

The PR remains draft for the cold-visit/size tradeoff decision. No merge or deployment was performed.

@IEvangelist David Pine (IEvangelist) removed the browser-profile Browser/cache-only profiling from the latest complete paired corpus label Oct 14, 2026
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