EngineeringSenior DeveloperTechnical Sessions

Session 05: Performance, Memory & Diagnostics

Mục tiêu: Viết code hiệu năng cao, hiểu async internals, và sử dụng diagnostic tools chuyên nghiệp.


1. Benchmarking với BenchmarkDotNet

csharp
// Cài: dotnet add package BenchmarkDotNet
[MemoryDiagnoser]            // Đo memory allocation
[RankColumn]                 // Xếp hạng
[SimpleJob(RuntimeMoniker.Net90)]
public class StringConcatBenchmarks
{
    private readonly string[] _items = Enumerable.Range(0, 1000)
        .Select(i => $"item-{i}")
        .ToArray();

    [Benchmark(Baseline = true)]
    public string StringConcat()
    {
        var result = "";
        foreach (var item in _items)
            result += item + ",";
        return result;
    }

    [Benchmark]
    public string StringBuilder_()
    {
        var sb = new StringBuilder();
        foreach (var item in _items)
            sb.Append(item).Append(',');
        return sb.ToString();
    }

    [Benchmark]
    public string StringJoin() => string.Join(",", _items);

    [Benchmark]
    public string ZeroAllocSpan()
    {
        // Dùng ArrayPool + Span cho zero-alloc
        var buffer = ArrayPool<char>.Shared.Rent(16384);
        try
        {
            var span = buffer.AsSpan();
            int pos = 0;
            foreach (var item in _items)
            {
                item.AsSpan().CopyTo(span[pos..]);
                pos += item.Length;
                span[pos++] = ',';
            }
            return new string(span[..pos]);
        }
        finally
        {
            ArrayPool<char>.Shared.Return(buffer);
        }
    }
}

// Chạy: dotnet run -c Release
// Kết quả mẫu:
// |         Method |        Mean | Allocated |
// |--------------- |------------ |---------- |
// |   StringConcat | 1,234.56 μs |  2.1 MB   |  ← Tệ nhất
// | StringBuilder_ |    45.23 μs |   16 KB   |
// |     StringJoin |    38.12 μs |   12 KB   |
// |  ZeroAllocSpan |    32.45 μs |    8 KB   |  ← Tốt nhất

2. Object Pooling

ObjectPool<T>

csharp
// Microsoft.Extensions.ObjectPool (built-in ASP.NET Core)
// Dùng cho objects tốn kém khi create/destroy

public class PdfGenerator
{
    private static readonly ObjectPool<StringBuilder> _sbPool =
        new DefaultObjectPoolProvider()
            .CreateStringBuilderPool(initialCapacity: 1024, maximumRetainedCapacity: 16384);

    public string GenerateReport(IEnumerable<ReportRow> rows)
    {
        var sb = _sbPool.Get();
        try
        {
            sb.AppendLine("<html><body><table>");
            foreach (var row in rows)
            {
                sb.Append("<tr><td>").Append(row.Name).Append("</td>");
                sb.Append("<td>").Append(row.Value).Append("</td></tr>");
            }
            sb.AppendLine("</table></body></html>");
            return sb.ToString();
        }
        finally
        {
            _sbPool.Return(sb); // Reset & return to pool
        }
    }
}

// Custom object pool
public class ExpensiveResourcePolicy : IPooledObjectPolicy<ExpensiveResource>
{
    public ExpensiveResource Create() => new ExpensiveResource();

    public bool Return(ExpensiveResource obj)
    {
        if (obj.IsCorrupted) return false; // Don't return broken objects
        obj.Reset();
        return true;
    }
}

builder.Services.AddSingleton<ObjectPool<ExpensiveResource>>(sp =>
{
    var provider = sp.GetRequiredService<ObjectPoolProvider>();
    return provider.Create(new ExpensiveResourcePolicy());
});

ArrayPool<T>

csharp
// Tránh allocate large arrays lặp lại
public class ImageProcessor
{
    public async Task<byte[]> ResizeAsync(Stream imageStream, int targetSize)
    {
        // Rent buffer - có thể lớn hơn requested size!
        var buffer = ArrayPool<byte>.Shared.Rent(targetSize);
        try
        {
            var bytesRead = await imageStream.ReadAsync(buffer.AsMemory(0, targetSize));
            // Process image...
            return buffer[..bytesRead].ToArray(); // Copy result
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer, clearArray: true); // Clear sensitive data
        }
    }
}

// ⚠️ Lưu ý ArrayPool:
// 1. Kích thước trả về có thể LỚN HƠN kích thước yêu cầu
// 2. PHẢI return sau khi dùng xong (memory leak nếu không)
// 3. clearArray: true nếu buffer chứa sensitive data

3. Async Deep Dive

ValueTask vs Task

csharp
// Task: luôn allocate object trên heap
// ValueTask: stack-allocated nếu result đã sẵn sàng

// ✅ Dùng ValueTask khi hot path có thể return ngay (cached result)
public ValueTask<Product?> GetProductAsync(int id)
{
    // Cache hit → return ngay, zero allocation
    if (_cache.TryGetValue(id, out var product))
        return ValueTask.FromResult<Product?>(product);

    // Cache miss → async operation
    return LoadFromDatabaseAsync(id);
}

// ⚠️ ValueTask restrictions:
// 1. KHÔNG await 2 lần
// 2. KHÔNG dùng Task.WhenAll(valueTask1, valueTask2)
// 3. KHÔNG lưu vào biến rồi await sau

// ❌ Dangerous
var vt = GetProductAsync(1);
var result1 = await vt;
var result2 = await vt; // 💥 Undefined behavior!

// ✅ Safe: convert to Task nếu cần
var vt = GetProductAsync(1);
var task = vt.AsTask(); // Convert 1 lần
await task; // OK

ConfigureAwait Deep Dive

csharp
// ConfigureAwait(false): KHÔNG capture SynchronizationContext
// → Không switch về UI thread / ASP.NET request thread sau await

// ASP.NET Core: KHÔNG CÓ SynchronizationContext → không cần ConfigureAwait(false)
// Nhưng: Library code NÊN luôn dùng ConfigureAwait(false)
// Vì library có thể được gọi từ WPF/WinForms/ASP.NET Framework

// Library code pattern:
public class MyLibrary
{
    public async Task<T> ProcessAsync<T>(Func<Stream, Task<T>> processor)
    {
        using var stream = await OpenStreamAsync().ConfigureAwait(false);
        var result = await processor(stream).ConfigureAwait(false);
        await LogResultAsync(result).ConfigureAwait(false);
        return result;
    }
}

// Application code (ASP.NET Core): KHÔNG CẦN ConfigureAwait
public class OrderService
{
    public async Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
    {
        var order = Order.Create(cmd);
        await _repository.AddAsync(order);  // No ConfigureAwait needed
        await _eventBus.PublishAsync(new OrderCreated(order.Id));
        return order;
    }
}

SynchronizationContext

csharp
// SynchronizationContext quyết định thread nào chạy continuation sau await

// WPF/WinForms: DispatcherSynchronizationContext → UI thread
// ASP.NET Framework: AspNetSynchronizationContext → request thread
// ASP.NET Core: NULL → ThreadPool thread (fast!)
// Console apps: NULL → ThreadPool thread

// ASP.NET Core KHÔNG CÓ SyncContext → await resume trên bất kỳ ThreadPool thread
// → Đây là lý do ASP.NET Core nhanh hơn ASP.NET Framework

Channel<T> - Producer/Consumer Pattern

csharp
// Channel<T>: high-performance async queue
// Thay thế BlockingCollection cho async scenarios

public class BackgroundOrderProcessor : BackgroundService
{
    private readonly Channel<OrderEvent> _channel;

    public BackgroundOrderProcessor()
    {
        // Bounded channel: backpressure khi đầy
        _channel = Channel.CreateBounded<OrderEvent>(new BoundedChannelOptions(1000)
        {
            FullMode = BoundedChannelFullMode.Wait,  // Wait khi full
            SingleReader = true,                      // 1 consumer → optimize
            SingleWriter = false                      // nhiều producers
        });
    }

    // Producer: non-blocking enqueue
    public async ValueTask EnqueueAsync(OrderEvent evt, CancellationToken ct = default)
    {
        await _channel.Writer.WriteAsync(evt, ct);
    }

    // Consumer: background processing
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var evt in _channel.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                await ProcessEventAsync(evt);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to process order event {OrderId}", evt.OrderId);
                // Dead letter, retry, etc.
            }
        }
    }
}

// Registration
builder.Services.AddSingleton<BackgroundOrderProcessor>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<BackgroundOrderProcessor>());

Parallel.ForEachAsync (.NET 6+)

csharp
// Process items in parallel với concurrency control
public async Task SyncProductsAsync(IReadOnlyList<Product> products)
{
    await Parallel.ForEachAsync(
        products,
        new ParallelOptions
        {
            MaxDegreeOfParallelism = 10,  // Max 10 concurrent
            CancellationToken = _ct
        },
        async (product, ct) =>
        {
            await _externalApi.SyncProductAsync(product, ct);
            _logger.LogInformation("Synced product {ProductId}", product.Id);
        });
}

// So sánh approaches:
// Task.WhenAll → tất cả chạy đồng thời, KHÔNG control concurrency
// Parallel.ForEachAsync → control concurrency, backpressure
// Channel<T> → producer/consumer, decouple

4. Memory Profiling & GC Tuning

Detecting Memory Issues

csharp
// 1. dotnet-counters: live monitoring
// dotnet tool install -g dotnet-counters
// dotnet-counters monitor --process-id <PID> --counters System.Runtime

// Key metrics to watch:
// - gc-heap-size: tổng heap size
// - gen-X-gc-count: số lần GC mỗi generation
// - alloc-rate: allocation rate (bytes/sec)
// - exception-count: exception frequency
// - threadpool-queue-length: bao nhiêu work items đang chờ

// 2. dotnet-trace: capture trace for profiling
// dotnet tool install -g dotnet-trace
// dotnet-trace collect --process-id <PID> --duration 00:00:30
// → Mở file .nettrace trong Visual Studio / PerfView

// 3. dotnet-dump: analyze memory dump
// dotnet-dump collect --process-id <PID>
// dotnet-dump analyze <dump-file>
// > dumpheap -stat          # Top objects by count/size
// > dumpheap -type Order    # Tìm tất cả Order objects
// > gcroot <address>        # Tại sao object chưa bị GC?

GC Configuration cho Production

xml
<!-- runtimeconfig.json hoặc .csproj -->
<PropertyGroup>
    <ServerGarbageCollection>true</ServerGarbageCollection>
    <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>

<!-- GC Heap Limit: giới hạn memory usage -->
<!-- Phù hợp container environments -->
{
  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true,
      "System.GC.Concurrent": true,
      "System.GC.HeapHardLimit": 536870912,  // 512MB hard limit
      "System.GC.HeapHardLimitPercent": 75    // hoặc 75% container memory
    }
  }
}

Allocation-Free Coding Patterns

csharp
// 1. stackalloc cho small buffers
public static bool IsValidHex(ReadOnlySpan<char> input)
{
    Span<byte> bytes = stackalloc byte[input.Length / 2];
    return Convert.TryFromHexString(input, bytes, out _);
}

// 2. String.Create thay StringBuilder cho simple cases
public static string FormatOrderId(int year, int sequence)
{
    return string.Create(12, (year, sequence), (span, state) =>
    {
        "ORD-".AsSpan().CopyTo(span);
        state.year.TryFormat(span[4..], out _);
        span[8] = '-';
        state.sequence.TryFormat(span[9..], out _, "D3");
    });
    // → "ORD-2024-001", zero intermediate allocations
}

// 3. SearchValues<T> (.NET 8+)
private static readonly SearchValues<char> _vowels =
    SearchValues.Create("aeiouAEIOU");

public static int CountVowels(ReadOnlySpan<char> text)
{
    int count = 0;
    int index;
    while ((index = text.IndexOfAny(_vowels)) >= 0)
    {
        count++;
        text = text[(index + 1)..];
    }
    return count;
}
// → SIMD-accelerated search, cực nhanh

// 4. CompositeFormat (.NET 8+)
private static readonly CompositeFormat _logFormat =
    CompositeFormat.Parse("Order {0} processed in {1}ms with status {2}");

public void LogOrderProcessed(Guid orderId, long elapsedMs, string status)
{
    _logger.LogInformation(
        string.Format(null, _logFormat, orderId, elapsedMs, status));
}

5. Diagnostic Scenarios Thực Tế

Scenario 1: Memory Leak Detection

csharp
// Symptom: Gen 2 GC tăng liên tục, heap size không giảm
// Nguyên nhân phổ biến:

// 1. Event handler không unsubscribe
public class OrderObserver
{
    public OrderObserver(IOrderService service)
    {
        service.OrderCreated += OnOrderCreated; // ❌ Never unsubscribed!
    }
}

// Fix: implement IDisposable
public class OrderObserver : IDisposable
{
    private readonly IOrderService _service;
    
    public OrderObserver(IOrderService service)
    {
        _service = service;
        _service.OrderCreated += OnOrderCreated;
    }
    
    public void Dispose() => _service.OrderCreated -= OnOrderCreated;
}

// 2. Static collections growing unbounded
private static readonly Dictionary<string, CacheEntry> _cache = new(); // ❌ Never cleaned!

// Fix: dùng MemoryCache với expiration
private readonly IMemoryCache _cache;
_cache.Set(key, value, TimeSpan.FromMinutes(30));

// 3. HttpClient mới mỗi request
// ❌ using var client = new HttpClient(); // Socket exhaustion!
// ✅ IHttpClientFactory (pooled connections)

Scenario 2: ThreadPool Starvation

csharp
// Symptom: threadpool-queue-length tăng cao, response time tăng
// Nguyên nhân: blocking calls trên ThreadPool threads

// ❌ Sync over async
public ActionResult GetProducts()
{
    var products = _db.Products.ToListAsync().Result; // 💥 Deadlock risk!
    return Ok(products);
}

// ❌ Thread.Sleep trong async context
public async Task ProcessAsync()
{
    await Task.Delay(100); // ✅ OK
    Thread.Sleep(100);     // ❌ Blocks ThreadPool thread
}

// ✅ Fix: properly async all the way
public async Task<ActionResult> GetProducts()
{
    var products = await _db.Products.ToListAsync();
    return Ok(products);
}

Tổng Kết Session 05

ConceptKey Takeaway
BenchmarkDotNetLuôn benchmark trước optimize, đo cả memory allocation
ObjectPoolDùng cho objects tốn kém create/destroy
ArrayPoolTránh GC pressure từ large array allocations
ValueTaskDùng khi hot path có thể return synchronously
Channel<T>Producer/consumer pattern cho background processing
Parallel.ForEachAsyncParallel I/O-bound tasks với concurrency control
stackalloc + SpanZero-allocation cho small buffer operations

Bài tập: Profile 1 API endpoint trong project bằng dotnet-counters. Xác định allocation hotspot, áp dụng ArrayPool hoặc Span<T>, sau đó benchmark trước/sau bằng BenchmarkDotNet.