Find the Best Cosmetic Hospitals

Explore trusted cosmetic hospitals and make a confident choice for your transformation.

โ€œInvest in yourself โ€” your confidence is always worth it.โ€

Explore Cosmetic Hospitals

Start your journey today โ€” compare options in one place.

Performance Engineering in .NET – Lab & Demo 1

It is constructed specifically to demonstrate real-world performance engineering concepts in .NET, using a scenario every enterprise system deals with: bulk inserts, EF Core behavior, SQL bottlenecks, GC pressure, and threadpool starvation.

Below is the clear breakdown of what each endpoint proves, what performance concept it demonstrates, and what your trainees should observe.

Sample Code – https://github.com/devopsschool-demo-labs-projects/Performance-Engineering-in-.NET/tree/master/Lab1/PerfLabDemov1


โœ… 1. What Naive Bulk Insert Proves

Endpoint:

api/orders/bulk-naive?count=1000

What the code does:

  • Loops 1000 times.
  • Adds 1 entity.
  • Calls SaveChanges() 1000 times.

What it demonstrates:

A. EF Core is very slow when SaveChanges() is called repeatedly

SaveChanges triggers:

  • Change tracking validation
  • DB command generation
  • Opening a DB connection (sometimes pooled)
  • Executing the SQL INSERT
  • Committing a transaction

Doing it 1000 times = big overhead.

B. SQL Server becomes chatty

You generate 1000 round-trips to the DB engine.

This simulates real-world anti-patterns:

  • Chatty repositories
  • Overuse of Insert() inside loops
  • Poor DDD aggregates causing many tiny writes
  • Repository pattern over-abstraction

C. ThreadPool starvation / long-running synchronous IO

SaveChanges() waits for the DB:

  • Can exhaust threadpool threads
  • Causes slower Kestrel handling
  • Latency increases dramatically

D. GC Pressure

Creating many objects inside a loop causes:

  • More allocations
  • More Gen0/Gen1 collections
  • More pauses

Which you can show live using:

dotnet-counters monitor --process-id {pid}

Scientific Output Audience Should Observe

MetricNaive BulkImpact
CPULow (waiting on DB)Slow
MemoryHigherMore GC cycles
TimeSlowest5โ€“30 sec
DB Calls1000 INSERTsVery chatty
LocksMoreSlows everyone else

2. What Optimized Bulk Insert Proves

Endpoint:

api/orders/bulk-optimized?count=1000

What the code does:

  • Adds 1000 entities into DbContext
  • Calls SaveChanges once

What it demonstrates:

A. EF Coreโ€™s Unit-of-Work Model Works Best

One SaveChanges:

  • One transaction
  • Batch of INSERTs
  • Less change tracker overhead

B. Up to 50x Faster Performance

Typical behavior:

  • Naive: 10โ€“40 seconds
  • Optimized: 100โ€“400 ms

C. Far fewer SQL round-trips

Instead of 1000 statements:

  • SQL batches efficiently
  • Reduces latency
  • Reduces network round-trips

D. Lower GC Pressure

One batch = fewer objects alive at once โ†’ fewer collections.


Scientific Output Audience Should Observe

MetricOptimized BulkImprovement
CPUSlightly higherGood (doing real work)
MemoryLowerSteady
TimeVery fast10โ€“40x faster
DB Calls1Huge gain
LocksMinimalHelps concurrency

3. Check Count โ€” Proves Persistence

Endpoint:

api/orders/count

Purpose:

  • Prove that both naive & optimized inserts actually inserted data.
  • Visualize DB growth.
  • Show effect of concurrent inserts.

In training, we proved:

  • DB is storing state
  • Optimized insert quickly increases count
  • Naive insert slowly increases count

This makes the performance difference real and visible.


Overall What This Demo Proves

This small demo actually teaches 6 core performance engineering concepts:

โœ” 1. Impact of chatty database I/O

1000 tiny writes vs 1 batch write.

โœ” 2. EF Core change tracking and SaveChanges cost

Understanding EF internals.

โœ” 3. Async vs Sync I/O behavior

Bulk naive blocks threads โ†’ causes starvation.

โœ” 4. Garbage Collection impact

Small object allocation patterns in loops.

โœ” 5. SQL Server round-trip cost

Network + parsing + locking overhead.

โœ” 6. Importance of batching & correct architectural patterns

Avoid anti-patterns like:

  • Repository over-abstraction
  • N+1 operations
  • Per-row SaveChanges

How to Experience Demo

I. Run Naive Bulk Insert

It hangs or takes long.
Show CPU, memory, threadpool, GC logs.

II. Run Optimized Bulk Insert

Instant.
Show difference in metrics.

III. Ask the audience: Why did it happen?

Discuss:

  • DB round-trips
  • EF Core internals
  • Transaction management
  • GC
  • ThreadPool starvation

IV. Tie it to real architecture

“Your microservice might look healthy, but if its DB access pattern is naive, you will suffer scaling issues.”

This is exactly the pain point of real enterprise systems.


Iโ€™ll give you:

  1. A simple performance chart dataset (Naive vs Optimized) you can paste into Excel/PowerPoint.
  2. A perf counters command list (dotnet-counters + PerfMon).
  3. Ready-to-use scripts for Postman, k6, and JMeter.

๐ŸŸฆ 1. Process Memory (Private Bytes)

In your screenshot:

  • Memory rises gradually โ†’ EF Core and SQL client allocating objects.
  • Yellow triangles (GC events) โ†’ frequent Gen0/Gen1 garbage collections.
  • After a spike, memory stabilizes because GC reclaimed space.

๐ŸŽฏ Training Insight:

Naive bulk insert = more allocations = more frequent GC = lower throughput

  • Each SaveChanges call causes allocations of:
    • SQL parameters
    • Commands
    • Transaction objects
    • Change tracker nodes
  • So memory climbs quickly and GC fires often.

Optimized bulk will show:

  • A single memory spike
  • Fewer GC events
  • Faster completion

๐ŸŸฉ 2. CPU (% of all processors)

Your screenshot shows:

  • CPU is mostly idle
  • Small bumps, but never sustained high usage

๐ŸŽฏ Training Insight:

Naive bulk insert does not maximize CPU โ€” it blocks waiting for DB I/O.

Thatโ€™s why:

  • CPU stays low โ†’ app is waiting on the database
  • The bottleneck is not CPU โ†’ it’s SQL round-trips

Optimized bulk will show:

  • Higher CPU bursts (doing real work)
  • Shorter total duration

๐ŸŸฆ 3. ThreadPool Thread Count

Graph shows slow growth from 8 โ†’ ~12 threads.

๐ŸŽฏ Training Insight:

This indicates ThreadPool grows because threads are waiting on database I/O.

Naive bulk:

  • 1000 calls to SaveChangesAsync() = 1000 I/O waits
  • ThreadPool sees โ€œtoo many blocking tasksโ€ and tries to grow

This is how real production APIs experience:

  • ThreadPool starvation
  • Request queues
  • High latency

Optimized bulk stays closer to 8 threads (default), because the work is not blocking.


๐ŸŸง 4. GC Heap Size

Increasing heap โ†’ many small allocations from:

  • EF Core
  • SQL Client
  • LINQ
  • Loops & entity allocations

Frequent GC pauses slow down naive bulk significantly.

Optimized bulk = fewer allocations, fewer collections.


1๏ธโƒฃ Performance Chart โ€“ Naive vs Optimized (Sample Data)

These are example numbers you can either:

  • Use as-is for slides, or
  • Replace with your actual measurements from your machine.

Suggested Scenario

  • 1000 orders (count=1000)
  • Single request to /bulk-naive and /bulk-optimized

Example Results (for chart)

MetricNaive Bulk InsertOptimized Bulk Insert
Total time (ms)12000600
Requests/sec (effective)~83~1666
SQL INSERT statements executed10001 (batched)
CPU utilization (peak, %)25%40% (short burst)
GC Gen0 collections during run154
Avg DB round-trip time (ms)8โ€“125โ€“8

How to turn this into a chart

Chart 1 โ€“ Response Time (ms)

  • X-axis: Naive, Optimized
  • Y-axis: Total time (ms)
  • Values: 12000 vs 600 (log scale works nicely)

Chart 2 โ€“ SQL Round-Trips

  • X-axis: Naive, Optimized
  • Y-axis: Number of DB round-trips
  • Values: 1000 vs 1

Chart 3 โ€“ Requests/sec (effective throughput)

  • X-axis: Naive, Optimized
  • Y-axis: Requests/sec
  • Values: 83 vs 1666

On the slide, your message is:

โ€œSame functionality. Only difference is how we use EF Core and SaveChanges โ€“ and performance differs by ~20x.โ€


2๏ธโƒฃ Perf Counters Command List

A. Using dotnet-counters (recommended in session)

  1. Find the process ID of your running API:
dotnet tool install --global dotnet-counters
dotnet-counters ps
Code language: PHP (php)

Note the PID for Orders.Api.

  1. Monitor runtime + ASP.NET + GC counters live:
dotnet-counters monitor 
  --process-id <PID> 
  System.Runtime 
  Microsoft.AspNetCore.Hosting 
  Microsoft.AspNetCore.Http.Connections 
  Microsoft.EntityFrameworkCore

dotnet-counters monitor --process-id 55788 System.Runtime Microsoft.AspNetCore.Hosting Microsoft.AspNetCore.Http.Connections Microsoft.EntityFrameworkCore

Code language: CSS (css)

Useful counters to point out:

  • System.Runtime
    • cpu-usage
    • gc-heap-size
    • gen-0-gc-count, gen-1-gc-count, gen-2-gc-count
    • threadpool-thread-count
  • Microsoft.AspNetCore.Hosting
    • requests-per-second
    • total-requests
    • current-requests
  • Microsoft.EntityFrameworkCore
    • active-db-contexts
    • queries-per-second (depending on provider/version)

Run dotnet-counters while you hit:

curl -X POST "http://localhost:5000/api/orders/bulk-naive?count=1000"
Code language: JavaScript (javascript)

then:

curl -X POST "http://localhost:5000/api/orders/bulk-optimized?count=1000"
Code language: JavaScript (javascript)

โ€ฆand talk through the visible differences (GC counts, RPS, etc.).


B. Windows PerfMon (classic counters)

Open perfmon.exe โ†’ Add these counters:

Processor

  • Processor(_Total)% Processor Time

Process (dotnet / w3wp if hosted under IIS)

  • Process(dotnet)% Processor Time
  • Process(dotnet)Private Bytes
  • Process(dotnet)Working Set

.NET CLR Memory

  • .NET CLR Memory(Orders.Api)# Gen 0 Collections
  • .NET CLR Memory(Orders.Api)# Gen 2 Collections
  • .NET CLR Memory(Orders.Api)% Time in GC

SQL Server

  • SQLServer:SQL StatisticsBatch Requests/sec
  • SQLServer:SQL StatisticsSQL Compilations/sec
  • SQLServer:SQL StatisticsSQL Re-Compilations/sec

This lets you show:

  • Naive = many batches/sec, more compilations, more GC.
  • Optimized = fewer batches/sec, smoother GC.

โœ… How to Add These Counters in Windows PerfMon

1. Open PerfMon

  • Press Win + R
  • Type: perfmon.exe
  • Press Enter

2. Add Counters

In the left panel:

  • Expand Monitoring Tools
  • Click Performance Monitor
  • Click the green โ€œ+โ€ button (Add Counters)

Now you will add counters category by category.


โœ… A. Processor Counters

Category: Processor

  1. Select Processor
  2. Select counter:
    • % Processor Time
  3. Select instance:
    • _Total
  4. Click Add

โœ… B. Process Counters (dotnet / w3wp)

Category: Process

  1. Select Process
  2. Select counters:
    • % Processor Time
    • Private Bytes
    • Working Set
  3. Select instance:
    • If running as console โ†’ dotnet
    • If hosted in IIS โ†’ w3wp
  4. Click Add

(You can add multiple counters at once before clicking Add.)


โœ… C. .NET CLR Memory Counters

Category: .NET CLR Memory

  1. Select .NET CLR Memory
  2. Select counters:
    • # Gen 0 Collections
    • # Gen 2 Collections
    • % Time in GC
  3. Select instance:
    • Your process name โ†’ Orders.Api
      (or the DLL name without .dll)
  4. Click Add

โœ… D. SQL Server Counters

Category: SQLServer:SQL Statistics

  1. Select SQLServer:SQL Statistics
  2. Select counters:
    • Batch Requests/sec
    • SQL Compilations/sec
    • SQL Re-Compilations/sec
  3. Instance = (select default instance / MSSQLSERVER)
  4. Click Add

๐ŸŽฏ Result Summary

You will now be monitoring:

CPU

  • Processor(_Total)% Processor Time
  • Process(dotnet)% Processor Time

Memory

  • Process(dotnet)Private Bytes
  • Process(dotnet)Working Set

.NET GC

  • CLR Memory Gen0 collections
  • CLR Memory Gen2 collections
  • CLR % Time in GC

SQL Server Load

  • Batch Requests/sec
  • SQL Compilations/sec
  • SQL Re-Compilations/sec

๐ŸŽฏ The Story Your Graphs Will Show

Naive Insert (1000 ร— SaveChanges)

  • ๐Ÿ”บ High Batch Requests/sec (many round-trips)
  • ๐Ÿ”บ Higher SQL compilations (same query many times)
  • ๐Ÿ”บ More GC activity (more allocations)
  • ๐Ÿ”บ Higher CPU usage

Optimized Insert (1 SaveChanges with AddRange)

  • ๐Ÿ”ป Lower Batch Requests/sec (single round-trip)
  • ๐Ÿ”ป Minimal SQL compilations
  • ๐Ÿ”ป Less GC pressure
  • ๐Ÿ”ป Lower CPU, smoother curve

3๏ธโƒฃ Postman Collection (JSON)

You can import this directly into Postman.

Create a file PerfLab.postman_collection.json with:

{
  "info": {
    "name": "PerfLab Orders API",
    "_postman_id": "e9d1d5f3-0000-0000-0000-000000000001",
    "description": "Naive vs Optimized bulk insert demo for Orders API",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "Bulk Naive (1000)",
      "request": {
        "method": "POST",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/api/orders/bulk-naive?count=1000",
          "host": ["{{baseUrl}}"],
          "path": ["api", "orders", "bulk-naive"],
          "query": [
            {
              "key": "count",
              "value": "1000"
            }
          ]
        }
      }
    },
    {
      "name": "Bulk Optimized (1000)",
      "request": {
        "method": "POST",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/api/orders/bulk-optimized?count=1000",
          "host": ["{{baseUrl}}"],
          "path": ["api", "orders", "bulk-optimized"],
          "query": [
            {
              "key": "count",
              "value": "1000"
            }
          ]
        }
      }
    },
    {
      "name": "Get Count",
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/api/orders/count",
          "host": ["{{baseUrl}}"],
          "path": ["api", "orders", "count"]
        }
      }
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "http://localhost:5000"
    }
  ]
}
Code language: JSON / JSON with Comments (json)

How to use:

  • Postman โ†’ Import โ†’ Select this JSON.
  • Use baseUrl = http://localhost:5000 or https://localhost:5001.

4๏ธโƒฃ k6 Load Test Script

Create k6-orders-perf.js:

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend } from 'k6/metrics';

const naiveTrend = new Trend('naive_duration');
const optTrend = new Trend('optimized_duration');

export const options = {
  scenarios: {
    naive: {
      executor: 'constant-vus',
      vus: 5,
      duration: '30s',
      exec: 'testNaive',
    },
    optimized: {
      executor: 'constant-vus',
      vus: 5,
      duration: '30s',
      exec: 'testOptimized',
      startTime: '35s'
    }
  }
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';

export function testNaive() {
  const res = http.post(`${BASE_URL}/api/orders/bulk-naive?count=100`, null);
  naiveTrend.add(res.timings.duration);
  check(res, {
    'status is 200': r => r.status === 200
  });
  sleep(1);
}

export function testOptimized() {
  const res = http.post(`${BASE_URL}/api/orders/bulk-optimized?count=100`, null);
  optTrend.add(res.timings.duration);
  check(res, {
    'status is 200': r => r.status === 200
  });
  sleep(1);
}
Code language: JavaScript (javascript)

Run:

k6 run k6-orders-perf.js
# or explicitly
BASE_URL=http://localhost:5000 k6 run k6-orders-perf.js
Code language: PHP (php)

In the output, point at:

  • naive_duration{} summary vs optimized_duration{}
  • http_reqs, http_req_duration, http_req_failed

5๏ธโƒฃ JMeter Test Plan (Concept + Minimal XML)

Concept for your training

Create 2 Thread Groups:

  1. NaiveBulk:
    • 10 threads (users)
    • 10 loops
    • HTTP Request Sampler โ†’ POST /api/orders/bulk-naive?count=100
  2. OptimizedBulk:
    • 10 threads
    • 10 loops
    • HTTP Request Sampler โ†’ POST /api/orders/bulk-optimized?count=100

Add:

  • View Results in Table
  • Summary Report
  • Aggregate Report

Compare:

  • Avg response time
  • 90th/95th percentile
  • of samples
  • Throughput

Minimal .jmx skeleton (for reference)

I wonโ€™t blow this up with full XML, but here is the key idea you can recreate in the JMeter GUI:

  • Test Plan
    • HTTP Request Defaults: Server Name or IP = localhost, Port = 5000
    • Thread Group: NaiveGroup
      • HTTP Sampler: POST /api/orders/bulk-naive with param count=100
    • Thread Group: OptimizedGroup
      • HTTP Sampler: POST /api/orders/bulk-optimized with param count=100
    • Listeners: Summary Report, Aggregate Report

You can save it as PerfLabNaiveVsOptimized.jmx and share with students.


Find Trusted Cardiac Hospitals

Compare heart hospitals by city and services โ€” all in one place.

Explore Hospitals
Iโ€™m a DevOps/SRE/DevSecOps/Cloud Expert passionate about sharing knowledge and experiences. I have worked at <a href="https://www.cotocus.com/">Cotocus</a>. I share tech blog at <a href="https://www.devopsschool.com/">DevOps School</a>, travel stories at <a href="https://www.holidaylandmark.com/">Holiday Landmark</a>, stock market tips at <a href="https://www.stocksmantra.in/">Stocks Mantra</a>, health and fitness guidance at <a href="https://www.mymedicplus.com/">My Medic Plus</a>, product reviews at <a href="https://www.truereviewnow.com/">TrueReviewNow</a> , and SEO strategies at <a href="https://www.wizbrand.com/">Wizbrand.</a> Do you want to learn <a href="https://www.quantumuting.com/">Quantum Computing</a>? <strong>Please find my social handles as below;</strong> <a href="https://www.rajeshkumar.xyz/">Rajesh Kumar Personal Website</a> <a href="https://www.youtube.com/TheDevOpsSchool">Rajesh Kumar at YOUTUBE</a> <a href="https://www.instagram.com/rajeshkumarin">Rajesh Kumar at INSTAGRAM</a> <a href="https://x.com/RajeshKumarIn">Rajesh Kumar at X</a> <a href="https://www.facebook.com/RajeshKumarLog">Rajesh Kumar at FACEBOOK</a> <a href="https://www.linkedin.com/in/rajeshkumarin/">Rajesh Kumar at LINKEDIN</a> <a href="https://www.wizbrand.com/rajeshkumar">Rajesh Kumar at WIZBRAND</a> <a href="https://www.rajeshkumar.xyz/dailylogs">Rajesh Kumar DailyLogs</a>

Related Posts

Top 10 LLM Evaluation Harnesses: Features, Pros, Cons & Comparison

Introduction LLM Evaluation Harnesses are tools, frameworks, and platforms that help teams test large language models, prompts, RAG pipelines, chatbots, copilots, and AI agents before they are…

Read More

Top 10 Model Benchmarking Suites: Features, Pros, Cons & Comparison

Introduction Model Benchmarking Suites help AI teams test, compare, and validate machine learning models, large language models, multimodal models, and AI agents before they are deployed in…

Read More

Top 10 Model Compression Toolkits: Features, Pros, Cons & Comparison

Introduction Model compression toolkits help AI teams reduce the size, memory usage, latency, and serving cost of machine learning models while keeping useful performance as high as…

Read More

Top 10 Model Quantization Tooling: Features, Pros, Cons & Comparison

Introduction Model quantization tooling helps AI teams make models smaller, faster, and cheaper to run by reducing numerical precision. Instead of running every model weight or activation…

Read More

Top 10 Model Distillation Toolkits: Features, Pros, Cons & Comparison

Introduction Model distillation toolkits help AI teams transfer knowledge from a larger, more capable model into a smaller, faster, and cheaper model. In simple terms, the larger…

Read More

Top 10 RLHF / RLAIF Training Platforms: Features, Pros, Cons & Comparison

Introduction RLHF and RLAIF training platforms help AI teams improve model behavior using structured feedback. RLHF, or reinforcement learning from human feedback, uses human preference signals, ratings,…

Read More
Subscribe
Notify of
guest
2 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
Skylar Bennett
Skylar Bennett
5 months ago

Excellent lab demo โ€” this is a very clear and practical way to teach performanceโ€‘engineering fundamentals in .NET! The contrast between โ€œnaiveโ€ and โ€œoptimizedโ€ bulk inserts (1000 ร— SaveChanges() vs a single batched insert) perfectly highlights how important patterns like batching and efficient DB access are for realโ€‘world performance. I especially appreciate how the demo shows the impact on EF Core overhead, SQL roundโ€‘trips, GC pressure, threadโ€‘pool starvation, and overall latency โ€” making abstract performance issues visible via actual metrics. For any .NET devs, architects or DevOps/SRE engineers, this tutorial offers a highly effective and handsโ€‘on path to building performant and scalable systems. ๐Ÿ‘

Jason Mitchell
Jason Mitchell
5 months ago

This labโ€‘style walkthrough nails the essential performance engineering concepts in a .NET environment by using a relatable scenario: bulk inserts, ORM (Entity Framework Core) behaviour, SQL roundโ€‘trip bottlenecks, GC pressure and threadโ€‘pool starvation. The comparison between a โ€œnaiveโ€ bulkโ€‘insert (oneโ€ฏSaveChangesโ€ฏper row) and an โ€œoptimizedโ€ version (batch insert + singleโ€ฏSaveChanges) is especially practical โ€” you can clearly see why thousands of tiny writes destroy throughput and increase latency while one wellโ€‘batched operation collapses the cost. The way the article links those behaviours to realโ€‘world issues (excessive DB roundโ€‘trips, threadโ€‘pool thread blocking, GC churn) makes it a strong teaching tool for developers and DevOps/SRE practitioners aiming to improve latency, resource efficiency and scalability.

2
0
Would love your thoughts, please comment.x
()
x