This episode covers .NET performance and diagnostics: profiling with dotnet-trace and dotnet-counters, memory analysis and garbage collection, allocation optimization with Span, Memory, and stackalloc, and JIT optimization and tiered compilation.

Slow-running code isn't always wrong logic — often the problem is wasteful memory allocation, a garbage collector working too hard, or a query loading too much data. But guessing isn't enough: performance must be measured.
The .NET toolchain provides production-grade diagnostic tools that can be installed and run with a single command. Only after measuring correctly is optimization worthwhile.
Episode 15 teaches profiling with dotnet-trace and dotnet-counters, memory diagnostics, optimization with new types like Span, and an understanding of the JIT and tiered compilation.
dotnet-counters monitors runtime metrics in real time — memory allocation, GC, and CPU utilization:
dotnet tool install --global dotnet-counters
dotnet counters monitor -n NamaAplikasi --counters System.RuntimeThe dotnet counters monitor command displays metrics like gen-0-gc-count, cpu-usage, and alloc-rate. If memory allocation stays persistently high, that's your first clue to investigate unnecessary allocations.
To see where time really goes, use dotnet-trace, which produces a trace file containing call stacks and the duration of each method:
dotnet tool install --global dotnet-trace
dotnet trace collect -p <pid> -o trace.nettrace
dotnet trace convert trace.nettrace --format speedscopeThe --format speedscope conversion output can be opened in an editor like VS Code to read a flamegraph — a visualization of each call's duration. From there you know which methods to optimize instead of guessing.
Besides external tools, .NET has APIs to measure allocation from inside the code:
static long HitungAlokasi(Func<int> kerja)
{
var sebelum = GC.GetAllocatedBytesForCurrentThread();
var hasil = kerja();
var sesudah = GC.GetAllocatedBytesForCurrentThread();
return sesudah - sebelum;
}
var alokasi = HitungAlokasi(() => ProsesData());
Console.WriteLine($"Alokasi: {alokasi} byte");GC.GetAllocatedBytesForCurrentThread counts the bytes allocated by the code on that thread. Knowing the allocation figure per operation helps you evaluate whether an optimization really has an impact.
Patterns that produce many short-lived small objects — like composing strings repeatedly — make the GC work hard. Use StringBuilder for string concatenation inside loops, and choose ArrayPool for buffers that are used frequently.
Span<T> lets you access a slice of an array or other memory without allocating a new object:
static int JumlahDigit(Span<char> teks)
{
var total = 0;
foreach (var c in teks)
{
if (char.IsDigit(c)) total++;
}
return total;
}
var data = "Order#12345".AsSpan();
Console.WriteLine($"Digit: {JumlahDigit(data)}");data.AsSpan() creates a lightweight view without copying the string. Parsing operations that use many substrings can now be done without a single allocation.
For small, fixed-size buffers, stackalloc allocates them on the stack — without touching the heap at all:
Span<byte> buffer = stackalloc byte[256];
buffer[0] = 0x01;
Console.WriteLine($"Byte pertama: {buffer[0]}");stackalloc byte[256] is allocated on the stack and automatically discarded when the method finishes. Use it for small buffers on very critical code paths — not for large or variable sizes.
The .NET runtime uses tiered compilation in two tiers: methods are compiled quickly (tier 0) when first called, then recompiled with full optimization (tier 1) if used frequently. This gives fast startup and peak performance for hot code.
For truly critical paths, you can tell the JIT with AggressiveOptimization on the method:
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
static int HitungKritis(int a, int b)
{
return a * b + a / b;
}AggressiveOptimization pushes the JIT to compile the method directly to the more optimal tier. Use it sparingly — only for methods proven to be hotspots from profiling, not for every method.
Key takeaways:
GC.GetAllocatedBytesForCurrentThread measures the impact of optimization.Span<T> and stackalloc eliminate allocations on critical paths.In the next episode 16 we organize large-scale projects: architecture and design patterns — clean architecture and layered architecture, dependency injection, CQRS and the mediator pattern, and DDD fundamentals with modular solution organization.