Repository navigation
Reduce StaticHost publishing time and add full-site CI diagnostics - #1879
David Pine (IEvangelist) wants to merge 6 commits into
Conversation
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
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
Frontend HTML artifact readyThe latest frontend build uploaded the This comment updates automatically when a new frontend build artifact is uploaded. |
There was a problem hiding this comment.
None of these changes to this file are needed/useful to anyone actually contributing to this project. Please undo these changes.
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
There was a problem hiding this comment.
🟡 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
Optimalwhile 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.
| 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') }} |
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
Controlled compression resultsCI run 38449500455 passed on commit The frontend was rendered once. Each variant then used a clean compression/publish directory and
Directory totals, exact task timings, CPU/RSS measurements, and raw data are in Representative document bytes and decoding
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 Browser measurement correctionI 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
Validation
|
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
Corrected browser/cache resultsThe browser-only job in CI run 38598948534 passed on commit 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 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 visitsPage transfer includes the main document and same-origin resources, not just HTML.
These descriptive medians show 42-62 ms more document download time and 40-56 ms higher FCP with 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 visitsIn 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
Validation
|
Final validation statusCI run 38598948534 completed successfully on 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. |

Summary
Default StaticHost Brotli publishing to
Optimalinstead of the SDK'sSmallestSize, 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.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.actionlint -shellcheck= -pyflakes= .github\workflows\apphost-build.yml .github\workflows\ci.yml— passed; ShellCheck/pyflakes disabled.git diff --check— passed.