Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: Environment setup
weight: 2

### FIXED, DO NOT MODIFY
layout: learningpathall
---

# Environment setup

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.

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.

This path was validated with Ubuntu 24.04 LTS on Azure Cobalt in `westus2` (example VM size: `Standard_D2ps_v6`, 2 vCPUs).

## Install tools on Cobalt VM

Install the toolchain on the Cobalt VM before building and testing.

```bash
sudo apt-get update -y
sudo apt-get install -y git curl jq ca-certificates gnupg docker.io
sudo usermod -aG docker "$USER"

sudo add-apt-repository ppa:dotnet/backports -y
sudo apt-get update -y
sudo apt-get install -y dotnet-sdk-9.0
```

Verify:

Confirm architecture and tool versions before proceeding.

```bash
uname -m
dotnet --version
docker --version
```

Expected output: `Arm` (kernel strings may still show architecture-specific values). Ensure the .NET SDK version is 9.0.x.

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.

## PostgreSQL prerequisite for nopCommerce install

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).

Create PostgreSQL and enable `citext` before running the installer:

```bash
# Start PostgreSQL for local validation.
docker run -d --name nop-postgres \
-e POSTGRES_USER=nop \
-e POSTGRES_PASSWORD=<password> \
-e POSTGRES_DB=nopcommerce \
-p 5432:5432 postgres:16

# Enable the extension required by nopCommerce migrations.
docker exec nop-postgres psql -U nop -d nopcommerce \
-c "CREATE EXTENSION IF NOT EXISTS citext;"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: Build and baseline
weight: 3

### FIXED, DO NOT MODIFY
layout: learningpathall
---

# Build and baseline

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.

## Clone and pin the source

Pinning the exact tag and commit avoids silent drift in dependencies and behavior.

```bash
git clone https://github.com/nopSolutions/nopCommerce.git
cd nopCommerce
git fetch --tags --prune
git checkout release-4.90.3
git rev-parse --short=10 HEAD # expect 9beda11c42
```

## Restore and build on Arm

Run on the Arm VM to establish a native Arm baseline.

```bash
# Restore dependencies first so build failures are easier to triage.
dotnet restore src/Presentation/Nop.Web/Nop.Web.csproj

# Build release binaries without re-restoring packages.
dotnet build src/Presentation/Nop.Web/Nop.Web.csproj -c Release --no-restore
```

## Start and install nopCommerce

Start the app locally and complete installer setup with PostgreSQL.

```bash
cd src/Presentation/Nop.Web
dotnet run -c Release --no-build --urls http://0.0.0.0:5000
```

Complete installation with PostgreSQL (`citext` enabled), then verify:

```bash
# Root should return storefront content.
curl -s -o /dev/null -w 'root=%{http_code}\n' http://127.0.0.1:5000/

# Install route should redirect once installation is complete.
curl -s -o /dev/null -w 'install=%{http_code}\n' http://127.0.0.1:5000/install
```

Expected after successful install: `root=200`, `install=302`.

## Baseline methodology

Do not benchmark `/install`. Baseline real storefront paths:

- `/`
- `/search/`
- `/catalog/searchtermautocomplete`
- `/product/search`
- `/category/products/`
- `/addproducttocart/catalog/...`
- `/addproducttocart/details/...`
- `/shoppingcart/productdetails_attributechange/...`
- `/product/combinations`
- `/cart/estimateshipping`
- `/cart/selectshippingoption`

Use the endpoint tester:

```bash
# Use fixed concurrency and iterations for a repeatable starting point.
python3 test_nopcommerce_endpoints.py \
--base-url http://127.0.0.1:5000 \
--concurrency 8 \
--iterations 20 \
--json-out arm_before.json
```

## Baseline quality rules

Use these rules before you compare any optimization result:

- Run at least 3 baseline trials with identical parameters.
- Keep endpoint order fixed per run (not randomized) when comparing before vs after.
- Keep database state and seeded data identical across runs.
- Capture raw JSON for every run and compare medians, not single outliers.

This baseline process becomes the control for every later tuning or code-change decision.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
title: Audit dependencies
weight: 4

### FIXED, DO NOT MODIFY
layout: learningpathall
---

# Audit dependencies

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.

### 1. Map direct references

Start with project-level references to see what your app explicitly depends on.

```bash
rg -n "<PackageReference|<ProjectReference" src
```

### 2. List transitive dependencies

Enumerate the full dependency graph; transitive packages often carry architecture-sensitive constraints.

```bash
dotnet list src/Presentation/Nop.Web/Nop.Web.csproj package --include-transitive
```

### 3. Generate an SBOM

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.

```bash
dotnet tool install --global CycloneDX
dotnet CycloneDX src/Presentation/Nop.Web/Nop.Web.csproj -o sbom/
```

If tool installation is blocked, treat `dotnet list --include-transitive` plus `*.deps.json` evidence as a temporary fallback, not the final state.

### 4. Inspect package internals for native payloads

Inspect package contents directly to find architecture-specific native binaries.

```bash
mkdir -p /tmp/nupkg-audit
cp ~/.nuget/packages/<package>/<version>/<package>.<version>.nupkg /tmp/nupkg-audit/
cd /tmp/nupkg-audit
unzip -l <package>.<version>.nupkg | rg "runtimes/|native/"
```

This is how you catch hidden architecture-specific binaries.

## Dependency cascade rule

Treat dependencies as a chain, not isolated items:

- App depends on library A
- Library A depends on library B
- Library B is architecture-sensitive

You must resolve B first, then validate A, then validate the app.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Containerize the application
weight: 5

### FIXED, DO NOT MODIFY
layout: learningpathall
---

# Containerize the application

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.

### 1. Dockerfile path

Run a Dockerfile build with buildx:

```bash
# Create or reuse a buildx builder that supports multi-architecture builds.
docker buildx create --use --name nopx

# Build and publish a manifest list that includes multiple architectures.
docker buildx build --platform linux/amd64,linux/arm64 -t [your repo:tag name] --push .
```

### 2. .NET SDK publish path (single architecture)

Use SDK publish when you want tighter integration with .NET build settings and fewer custom Docker steps.

```bash
dotnet publish src/Presentation/Nop.Web/Nop.Web.csproj \
-c Release \
/t:PublishContainer \
-p:ContainerImageTag=publish-test
```

### 3. .NET SDK multi-arch path

Define runtime identifiers and multi-arch container runtime identifiers in the project file, then publish once.

```xml
<PropertyGroup>
<RuntimeIdentifiers>linux-x64;linux-arm64</RuntimeIdentifiers>
<ContainerRuntimeIdentifiers>linux-x64;linux-arm64</ContainerRuntimeIdentifiers>
</PropertyGroup>
```

```bash
# Publish all configured runtime identifiers from one project definition.
dotnet publish src/Presentation/Nop.Web/Nop.Web.csproj -c Release /t:PublishContainer
```

This is the scalable path for one image definition across both architectures.

## SDK version guardrail

Before choosing the SDK publish path for multi-architecture images, check the SDK version used by CI and by developer workstations:

```bash
dotnet --version
```

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.

## Recommendation

Use SDK publish as the default migration path. Use a Dockerfile workflow for advanced layer control or custom build logic.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
title: Performance tuning on Cobalt
weight: 6

### FIXED, DO NOT MODIFY
layout: learningpathall
---

# Performance tuning on Cobalt

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.

## Tuned run configuration

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.

```bash
export DOTNET_TieredCompilation=1
export DOTNET_TieredPGO=1
export DOTNET_ReadyToRun=1
export DOTNET_gcServer=1
export DOTNET_gcConcurrent=1
export DOTNET_EnableDiagnostics=0
export DOTNET_ThreadPool_ForceMinWorkerThreads=2
```

Increase `DOTNET_ThreadPool_ForceMinWorkerThreads` only when traces or load-test data show thread-pool starvation.

### Optional spin-wait experiment for .NET 8, 9, and 10

If the workload burns CPU while waiting for short-lived thread-pool work, test disabling the thread-pool unfair semaphore spin limit:

```bash
export DOTNET_ThreadPool_UnfairSemaphoreSpinLimit=0
```

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.

### Tiered PGO and ReadyToRun optimize different phases

`DOTNET_TieredPGO=1` and `DOTNET_ReadyToRun=1` are not the same optimization:

- `ReadyToRun` favors startup and early request latency by using precompiled code when it is available.
- `TieredPGO` favors steady-state throughput by letting the JIT recompile hot methods with runtime profile data.
- Test them together and separately if startup latency and warmed throughput both matter.

Run the same endpoint suite used in the baseline:

```bash
# Keep benchmark parameters identical to baseline for valid comparison.
python3 test_nopcommerce_endpoints.py \
--base-url http://127.0.0.1:5000 \
--concurrency 8 \
--iterations 20 \
--json-out arm_after.json
```

## Validation rules for credible tuning gains

Use these rules before adopting a tuning profile:

- Run at least 5 baseline and 5 tuned trials.
- Keep endpoint sequence fixed across all runs.
- Reset warm-up policy consistently (always warm or always cold).
- Require improvement in both throughput and p95 latency, not just one.
- Set a minimum practical threshold (for example, >=5% median gain) before rollout.

## Instance-size tradeoffs

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.

## Interpretation

- If only one metric improves while p95 or error rate regresses, do not treat the change as a win.
- If run-to-run variation is near the observed delta, treat it as noise.
- Keep architecture-specific profiles separate; do not force one profile onto all architectures.


Use this page as a tuning workflow template, then validate with your production-like traffic profile before rollout.
Loading
Loading