Skip to content

Commit d144970

Browse files
authored
Merge pull request #3460 from JoeStech/main
Migrate a .NET application (nopCommerce) to Azure Cobalt 100
2 parents bd50926 + fb7d781 commit d144970

8 files changed

Lines changed: 516 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
title: Environment setup
3+
weight: 2
4+
5+
### FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
# Environment setup
10+
11+
Set up an Arm Cobalt environment first, then keep your toolchain and runtime configuration stable across all test runs. This creates a reliable Arm baseline for migration and tuning work.
12+
13+
Start with the official [Azure Cobalt setup guide](https://learn.arm.com/learning-paths/servers-and-cloud-computing/cobalt/) and complete VM provisioning there first.
14+
15+
This path was validated with Ubuntu 24.04 LTS on Azure Cobalt in `westus2` (example VM size: `Standard_D2ps_v6`, 2 vCPUs).
16+
17+
## Install tools on Cobalt VM
18+
19+
Install the toolchain on the Cobalt VM before building and testing.
20+
21+
```bash
22+
sudo apt-get update -y
23+
sudo apt-get install -y git curl jq ca-certificates gnupg docker.io
24+
sudo usermod -aG docker "$USER"
25+
26+
sudo add-apt-repository ppa:dotnet/backports -y
27+
sudo apt-get update -y
28+
sudo apt-get install -y dotnet-sdk-9.0
29+
```
30+
31+
Verify:
32+
33+
Confirm architecture and tool versions before proceeding.
34+
35+
```bash
36+
uname -m
37+
dotnet --version
38+
docker --version
39+
```
40+
41+
Expected output: `Arm` (kernel strings may still show architecture-specific values). Ensure the .NET SDK version is 9.0.x.
42+
43+
If you are upgrading from an older .NET version before migrating to Cobalt, you can use [GitHub Copilot modernization for .NET](https://learn.microsoft.com/dotnet/core/porting/github-copilot-app-modernization/overview) to assess the project and guide the upgrade. In Visual Studio Code, open Copilot Chat and use `@modernize-dotnet`; in Visual Studio, use the Modernize action from Solution Explorer.
44+
45+
## PostgreSQL prerequisite for nopCommerce install
46+
47+
nopCommerce defaults to SQL Server, but this learning path uses PostgreSQL for Arm validation. For PostgreSQL installs, `citext` is required before migration/installation. Without it, installer migrations fail with `type "citext" does not exist` (captured in local test artifacts).
48+
49+
Create PostgreSQL and enable `citext` before running the installer:
50+
51+
```bash
52+
# Start PostgreSQL for local validation.
53+
docker run -d --name nop-postgres \
54+
-e POSTGRES_USER=nop \
55+
-e POSTGRES_PASSWORD=<password> \
56+
-e POSTGRES_DB=nopcommerce \
57+
-p 5432:5432 postgres:16
58+
59+
# Enable the extension required by nopCommerce migrations.
60+
docker exec nop-postgres psql -U nop -d nopcommerce \
61+
-c "CREATE EXTENSION IF NOT EXISTS citext;"
62+
```
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
---
2+
title: Build and baseline
3+
weight: 3
4+
5+
### FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
# Build and baseline
10+
11+
Create a reproducible Arm baseline before optimization work. This page pins source, verifies a clean build, and captures a representative endpoint baseline so later changes can be measured against a known control.
12+
13+
## Clone and pin the source
14+
15+
Pinning the exact tag and commit avoids silent drift in dependencies and behavior.
16+
17+
```bash
18+
git clone https://github.com/nopSolutions/nopCommerce.git
19+
cd nopCommerce
20+
git fetch --tags --prune
21+
git checkout release-4.90.3
22+
git rev-parse --short=10 HEAD # expect 9beda11c42
23+
```
24+
25+
## Restore and build on Arm
26+
27+
Run on the Arm VM to establish a native Arm baseline.
28+
29+
```bash
30+
# Restore dependencies first so build failures are easier to triage.
31+
dotnet restore src/Presentation/Nop.Web/Nop.Web.csproj
32+
33+
# Build release binaries without re-restoring packages.
34+
dotnet build src/Presentation/Nop.Web/Nop.Web.csproj -c Release --no-restore
35+
```
36+
37+
## Start and install nopCommerce
38+
39+
Start the app locally and complete installer setup with PostgreSQL.
40+
41+
```bash
42+
cd src/Presentation/Nop.Web
43+
dotnet run -c Release --no-build --urls http://0.0.0.0:5000
44+
```
45+
46+
Complete installation with PostgreSQL (`citext` enabled), then verify:
47+
48+
```bash
49+
# Root should return storefront content.
50+
curl -s -o /dev/null -w 'root=%{http_code}\n' http://127.0.0.1:5000/
51+
52+
# Install route should redirect once installation is complete.
53+
curl -s -o /dev/null -w 'install=%{http_code}\n' http://127.0.0.1:5000/install
54+
```
55+
56+
Expected after successful install: `root=200`, `install=302`.
57+
58+
## Baseline methodology
59+
60+
Do not benchmark `/install`. Baseline real storefront paths:
61+
62+
- `/`
63+
- `/search/`
64+
- `/catalog/searchtermautocomplete`
65+
- `/product/search`
66+
- `/category/products/`
67+
- `/addproducttocart/catalog/...`
68+
- `/addproducttocart/details/...`
69+
- `/shoppingcart/productdetails_attributechange/...`
70+
- `/product/combinations`
71+
- `/cart/estimateshipping`
72+
- `/cart/selectshippingoption`
73+
74+
Use the endpoint tester:
75+
76+
```bash
77+
# Use fixed concurrency and iterations for a repeatable starting point.
78+
python3 test_nopcommerce_endpoints.py \
79+
--base-url http://127.0.0.1:5000 \
80+
--concurrency 8 \
81+
--iterations 20 \
82+
--json-out arm_before.json
83+
```
84+
85+
## Baseline quality rules
86+
87+
Use these rules before you compare any optimization result:
88+
89+
- Run at least 3 baseline trials with identical parameters.
90+
- Keep endpoint order fixed per run (not randomized) when comparing before vs after.
91+
- Keep database state and seeded data identical across runs.
92+
- Capture raw JSON for every run and compare medians, not single outliers.
93+
94+
This baseline process becomes the control for every later tuning or code-change decision.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
title: Audit dependencies
3+
weight: 4
4+
5+
### FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
# Audit dependencies
10+
11+
Run dependency discovery before migration changes so you understand direct references, transitive risk, and hidden native payloads. This avoids late surprises when deploying to Arm.
12+
13+
### 1. Map direct references
14+
15+
Start with project-level references to see what your app explicitly depends on.
16+
17+
```bash
18+
rg -n "<PackageReference|<ProjectReference" src
19+
```
20+
21+
### 2. List transitive dependencies
22+
23+
Enumerate the full dependency graph; transitive packages often carry architecture-sensitive constraints.
24+
25+
```bash
26+
dotnet list src/Presentation/Nop.Web/Nop.Web.csproj package --include-transitive
27+
```
28+
29+
### 3. Generate an SBOM
30+
31+
Generate an SBOM so you can track all components, versions, and exposure surface as a first-class migration artifact. While not strictly necessary for migration purposes, this is a best practice that will save your team time down the road. You can also give this SBOM to an LLM to extract insights about your codebase for you.
32+
33+
```bash
34+
dotnet tool install --global CycloneDX
35+
dotnet CycloneDX src/Presentation/Nop.Web/Nop.Web.csproj -o sbom/
36+
```
37+
38+
If tool installation is blocked, treat `dotnet list --include-transitive` plus `*.deps.json` evidence as a temporary fallback, not the final state.
39+
40+
### 4. Inspect package internals for native payloads
41+
42+
Inspect package contents directly to find architecture-specific native binaries.
43+
44+
```bash
45+
mkdir -p /tmp/nupkg-audit
46+
cp ~/.nuget/packages/<package>/<version>/<package>.<version>.nupkg /tmp/nupkg-audit/
47+
cd /tmp/nupkg-audit
48+
unzip -l <package>.<version>.nupkg | rg "runtimes/|native/"
49+
```
50+
51+
This is how you catch hidden architecture-specific binaries.
52+
53+
## Dependency cascade rule
54+
55+
Treat dependencies as a chain, not isolated items:
56+
57+
- App depends on library A
58+
- Library A depends on library B
59+
- Library B is architecture-sensitive
60+
61+
You must resolve B first, then validate A, then validate the app.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
title: Containerize the application
3+
weight: 5
4+
5+
### FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
# Containerize the application
10+
11+
Containerization should preserve reproducibility across architectures. This page shows practical paths for Dockerfile-based builds and .NET SDK container publish, with guardrails for multi-architecture delivery.
12+
13+
### 1. Dockerfile path
14+
15+
Run a Dockerfile build with buildx:
16+
17+
```bash
18+
# Create or reuse a buildx builder that supports multi-architecture builds.
19+
docker buildx create --use --name nopx
20+
21+
# Build and publish a manifest list that includes multiple architectures.
22+
docker buildx build --platform linux/amd64,linux/arm64 -t [your repo:tag name] --push .
23+
```
24+
25+
### 2. .NET SDK publish path (single architecture)
26+
27+
Use SDK publish when you want tighter integration with .NET build settings and fewer custom Docker steps.
28+
29+
```bash
30+
dotnet publish src/Presentation/Nop.Web/Nop.Web.csproj \
31+
-c Release \
32+
/t:PublishContainer \
33+
-p:ContainerImageTag=publish-test
34+
```
35+
36+
### 3. .NET SDK multi-arch path
37+
38+
Define runtime identifiers and multi-arch container runtime identifiers in the project file, then publish once.
39+
40+
```xml
41+
<PropertyGroup>
42+
<RuntimeIdentifiers>linux-x64;linux-arm64</RuntimeIdentifiers>
43+
<ContainerRuntimeIdentifiers>linux-x64;linux-arm64</ContainerRuntimeIdentifiers>
44+
</PropertyGroup>
45+
```
46+
47+
```bash
48+
# Publish all configured runtime identifiers from one project definition.
49+
dotnet publish src/Presentation/Nop.Web/Nop.Web.csproj -c Release /t:PublishContainer
50+
```
51+
52+
This is the scalable path for one image definition across both architectures.
53+
54+
## SDK version guardrail
55+
56+
Before choosing the SDK publish path for multi-architecture images, check the SDK version used by CI and by developer workstations:
57+
58+
```bash
59+
dotnet --version
60+
```
61+
62+
If you cannot use a .NET SDK version that supports multi-RID container publishing, or if SDK publish does not produce the multi-architecture image index you need, use the `docker buildx build --platform linux/amd64,linux/arm64` workflow instead. Multi-RID container publishing starts with .NET SDK versions 8.0.405, 9.0.102, and 9.0.2xx.
63+
64+
## Recommendation
65+
66+
Use SDK publish as the default migration path. Use a Dockerfile workflow for advanced layer control or custom build logic.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
title: Performance tuning on Cobalt
3+
weight: 6
4+
5+
### FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
# Performance tuning on Cobalt
10+
11+
Treat tuning as an experiment pipeline, not a one-shot tweak. On Arm, apply changes in a controlled sequence, measure with fixed workloads, and keep only optimizations that repeatedly improve your target metrics.
12+
13+
## Tuned run configuration
14+
15+
Start with a conservative runtime profile that is commonly effective for server workloads. Keep these settings outside the application code at first so you can switch profiles without rebuilding.
16+
17+
```bash
18+
export DOTNET_TieredCompilation=1
19+
export DOTNET_TieredPGO=1
20+
export DOTNET_ReadyToRun=1
21+
export DOTNET_gcServer=1
22+
export DOTNET_gcConcurrent=1
23+
export DOTNET_EnableDiagnostics=0
24+
export DOTNET_ThreadPool_ForceMinWorkerThreads=2
25+
```
26+
27+
Increase `DOTNET_ThreadPool_ForceMinWorkerThreads` only when traces or load-test data show thread-pool starvation.
28+
29+
### Optional spin-wait experiment for .NET 8, 9, and 10
30+
31+
If the workload burns CPU while waiting for short-lived thread-pool work, test disabling the thread-pool unfair semaphore spin limit:
32+
33+
```bash
34+
export DOTNET_ThreadPool_UnfairSemaphoreSpinLimit=0
35+
```
36+
37+
Treat this as an experiment, not a default. Turning off spin waiting can reduce wasted CPU on small instances or oversubscribed containers, but it can also increase wake-up latency and reduce peak throughput. Validate it separately from the base tuned profile.
38+
39+
### Tiered PGO and ReadyToRun optimize different phases
40+
41+
`DOTNET_TieredPGO=1` and `DOTNET_ReadyToRun=1` are not the same optimization:
42+
43+
- `ReadyToRun` favors startup and early request latency by using precompiled code when it is available.
44+
- `TieredPGO` favors steady-state throughput by letting the JIT recompile hot methods with runtime profile data.
45+
- Test them together and separately if startup latency and warmed throughput both matter.
46+
47+
Run the same endpoint suite used in the baseline:
48+
49+
```bash
50+
# Keep benchmark parameters identical to baseline for valid comparison.
51+
python3 test_nopcommerce_endpoints.py \
52+
--base-url http://127.0.0.1:5000 \
53+
--concurrency 8 \
54+
--iterations 20 \
55+
--json-out arm_after.json
56+
```
57+
58+
## Validation rules for credible tuning gains
59+
60+
Use these rules before adopting a tuning profile:
61+
62+
- Run at least 5 baseline and 5 tuned trials.
63+
- Keep endpoint sequence fixed across all runs.
64+
- Reset warm-up policy consistently (always warm or always cold).
65+
- Require improvement in both throughput and p95 latency, not just one.
66+
- Set a minimum practical threshold (for example, >=5% median gain) before rollout.
67+
68+
## Instance-size tradeoffs
69+
70+
Do not assume the same profile wins on every Azure Cobalt VM size. Smaller instances often benefit from lower CPU burn and predictable p95 latency. Larger instances often benefit from higher concurrency, but can expose GC, database, and shared-resource contention. Compare throughput, p95/p99 latency, CPU utilization, and error rate for each instance size before keeping a tuning profile.
71+
72+
## Interpretation
73+
74+
- If only one metric improves while p95 or error rate regresses, do not treat the change as a win.
75+
- If run-to-run variation is near the observed delta, treat it as noise.
76+
- Keep architecture-specific profiles separate; do not force one profile onto all architectures.
77+
78+
79+
Use this page as a tuning workflow template, then validate with your production-like traffic profile before rollout.

0 commit comments

Comments
 (0)