Add .NET 10 / ASP.NET Core 10 implementation and perf-lab integration#497
Add .NET 10 / ASP.NET Core 10 implementation and perf-lab integration#497cdhermann wants to merge 4 commits intoquarkusio:mainfrom
Conversation
Adds a dotnet10/ implementation equivalent to the existing Quarkus 3 and Spring Boot implementations, with full perf-lab, CI, and script integration. Application (dotnet10/): - ASP.NET Core 10 Minimal API, EF Core + Npgsql, same domain model and endpoints as the Quarkus implementation - OpenTelemetry traces (10% sampling) + metrics via OTLP, matching Quarkus - System.Text.Json with source-generated context (AOT-safe) - Health check via AddHealthChecks().AddDbContextCheck<T>() - Startup log matching perf-lab logFileStartedRegex pattern - Unit tests (xUnit) and E2E tests (Testcontainers, excluded from CI pending a database service container) perf-lab integration: - main.yml: dotnet10 RUNTIMECMD with updateScript: skip-version-update; runCmd uses env KEY=value prefix so exec $RUN_CMD correctly handles env-var assignments vs. the binary name - run-benchmarks.sh: dotnet10 in ALLOWED_RUNTIMES/DEFAULT_RUNTIMES; xmx_to_dotnet_gc_limit() converts --jvm-memory to hex DOTNET_GCHeapHardLimit so memory constraints are equivalent to the JVM -Xmx flag - Runtime constraints mirror JVM: CPU pinning via taskset (APP_CMD_PREFIX), DOTNET_GCHeapHardLimit from --jvm-memory, DOTNET_ProcessorCount from --cpus-app, DOTNET_gcServer=1 - helpers/dotnet.yml + requirements.yml: ensure-dotnet requirement check - scripts/time-to-1st-request.sh: fixed exec $RUN_CMD word-splitting for env-var-prefixed commands via env binary prefix CI (.github/workflows/main.yml): - New dotnet-build-test job (builds + unit tests; E2E excluded) - Java 21 setup included because Hyperfoil (used by the test harness) requires it Scripts: - stress.sh and 1strequest.sh detect dotnet binaries and set correct runtime env vars; 1strequest.sh gains --no-purge flag for local dev use - scripts/remote-setup.sh: one-command preparation of a bare Linux benchmark host Docs: - DOTNET-PORTING.md: Quarkus->dotnet concept mapping, implementation status, native AOT blocker rationale, checklist for adding future dotnet versions - README.md: dotnet10 entry, remote benchmark workflow, script examples Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Thank you @cdhermann for this! We need to discuss internally how we want to handle this. I'm on my way out on PTO for the next week. I just didn't want to let it hang without any feedback at all. |
Thanks, @edeandrea, for the quick feedback and for taking this into internal discussions. I think it’s great that you're considering it. If it makes it into the test suite, that would be awesome. However, I completely understand if it doesn't—maintaining parity between the .NET and JVM versions as they evolve is a non-negligible amount of work. What I’d be most interested in, though, is a 'head-to-head' comparison in a real lab environment just to see who currently has the edge in terms of throughput and resource consumption. |
Detects gcc and zlib dev headers on the remote host independently. If either is missing, reads /etc/os-release to identify the distro and installs the appropriate batteries-included packages: - Debian/Ubuntu/Mint/Pop: apt-get update + build-essential zlib1g-dev - Fedora: gcc gcc-c++ make zlib-devel - RHEL/CentOS/AlmaLinux/Rocky: dnf groupinstall 'Development Tools' + zlib-devel - openSUSE/SLES: zypper install gcc gcc-c++ make zlib-devel - Arch/Manjaro: pacman -Sy base-devel - Unknown: falls back on ID_LIKE, then warns with manual instructions Prevents silent GraalVM native-image build failures on fresh Ubuntu servers where gcc and zlib dev headers are not pre-installed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…h-log Native Quarkus binaries start in ~80ms, which is fast enough that the async watch-log script can fire LOG_REGEX_REACHED before the main thread has registered its wait-for — causing qDup to hang indefinitely. Adding a 1s sleep between ctrlC and the signal gives the main thread time to register wait-for regardless of how fast the app starts. JVM runtimes (~2.5s startup) were never affected by this race. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The run-load-test phase was starting the app first, then setting up watch-log. By the time tail -f started, the app had already been running for several seconds and had written its "started" message to the log file. tail -f replays existing file content on open, so the regex fired instantly — causing the main thread to skip past wait-for LOG_REGEX_REACHED and attempt to collect load results before wrk had run at all. Fix: move the watch-log invocation to before the app start. The log file is already created empty by `touch`, so tail -f on an empty file follows only new writes. The app's "started" line is always new content from tail's perspective. Also remove the sleep: 1s from the watch-log callback. The sleep was added as a workaround for a different race condition but does not actually block in a watch→then handler (qDup executes watch callbacks asynchronously without honouring sleep semantics there). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Hi @cdhermann ! We wanted to again thank you for all of this great work! To be honest we're really flattered that someone would take what we've done to this next level! That being said, I don't think it is something that we will merge, mostly because we lack the dotnet experience to properly review this and also to maintain it going forward. We'd like to leave this PR open for discoverability in case others are interested in it - it really is good work.
Unfortunately, due to security concerns, we can't run something in our lab environment that we haven't reviewed thoroughly, and as mentioned, we just don't have the dotnet experience to give this a proper review. |
Summary
Adds a .NET 10 implementation (
dotnet10/) equivalent to the existing Quarkus 3 and Spring Bootimplementations, along with full perf-lab and CI integration.
Application (
dotnet10/)Same domain model, endpoints, and behaviour as the Quarkus implementation:
AddHealthChecks().AddDbContextCheck<T>()WithMetrics→ OTLPTraceIdRatioBasedSampler(0.1)System.Text.Json(source-generated, AOT-safe)The app emits a startup log matching the perf-lab
logFileStartedRegexpattern(
.*dotnet10.+started in.*).perf-lab integration
scripts/perf-lab/main.yml—dotnet10entry inRUNTIMESandRUNTIMECMDS;updateScript: skip-version-updateprevents abort when qDup'supdate-runtime-versionruns.runCmdusesenv KEY=valueprefix so thattime-to-1st-request.sh'sexec $RUN_CMDcorrectly word-splits env-var assignments vs. the binary.
scripts/perf-lab/run-benchmarks.sh—dotnet10inALLOWED_RUNTIMES/DEFAULT_RUNTIMES;xmx_to_dotnet_gc_limit()converts--jvm-memory(e.g.-Xmx512m) to a hexDOTNET_GCHeapHardLimitso memory constraints match the JVM exactly.scripts/perf-lab/helpers/dotnet.yml/requirements.yml—ensure-dotnetrequirement check.taskset(APP_CMD_PREFIX)DOTNET_GCHeapHardLimitcomputed from--jvm-memoryDOTNET_ProcessorCountfrom--cpus-appDOTNET_gcServer=1CI (
.github/workflows/main.yml)New
dotnet-build-testjob that builds the app and runs unit tests (E2E tests excludedpending a database service container — see open items).
Scripts
scripts/stress.shandscripts/1strequest.sh— detect dotnet binaries and set the correctruntime env vars.
1strequest.shalso gains a--no-purgeflag to skipsudo purge/drop_cachesfor local dev use without sudo.scripts/remote-setup.sh— one-command preparation of a bare Linux benchmark host(SSH key install, passwordless sudo, environment verification).
Docs
DOTNET-PORTING.md— Quarkus→dotnet concept mapping table, implementation status,native AOT blocker rationale, and checklist for adding future dotnet versions.
README.md— dotnet10 entry, remote benchmark workflow, script examples.Benchmark results while end-to-end testing
on a Hetzner CPX62 VPS, 16-core / 30 GB, 512 MB heap limit
./run-benchmarks.sh \ --runtimes 'dotnet10' \ --repo-url https://github.com/cdhermann/spring-quarkus-perf-comparison.git \ --host quarkus-dotnet-performance-comparison \ --user root \ --cpus-app 0-3 \ --cpus-db 4-6 \ --cpus-otel 7-9 \ --cpus-load-gen 10-12 \ --cpus-monitoring 13 \ --cpus-first-request 10and
./run-benchmarks.sh \ --runtimes 'quarkus3-jvm' \ --repo-url https://github.com/cdhermann/spring-quarkus-perf-comparison.git \ --host quarkus-dotnet-performance-comparison \ --user root \ --quarkus-version 3.34.5 \ --cpus-app 0-3 \ --cpus-db 4-6 \ --cpus-otel 7-9 \ --cpus-load-gen 10-12 \ --cpus-monitoring 13 \ --cpus-first-request 10Open items (not blocking)
with
--filter "FullyQualifiedName!~Dotnet10.Tests.E2e"until either Testcontainers or aGitHub Actions service container is wired up.
.github/dependabot.ymlcurrently only covers Maven.A follow-up can add a
nugetentry pointing atdotnet10/.upstream (dotnet/efcore#29840).
A
dotnet10-nativeruntime will be added once that stabilises.java -versioncalls in perf-lab are misleading for dotnet runtimes — harmless noisesince Java is always present (qDup is a Java tool), but could be made conditional on
${{RUNTIME.type}} != dotnetin a follow-up.