From cf96ffae8876eede3dade2c19d4bfd24b8d370cb Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 14:52:00 +0800 Subject: [PATCH 01/32] Add PostgreSQL connection pooling comparison scripts and functionality --- .../AzureDbSample/AzureDbBehavior.cs | 36 +- .../AzureDbSample/ConnectionPool.cs | 74 ++++ .../AzureDbSample/PostgresQuery.cs | 49 +++ .../AzureDbSample/pool-size-comparison.ps1 | 256 ++++++++++++ .../AzureDbSample/pool-size-comparison.sh | 285 ++++++++++++++ .../AzureDbSample/pooling-comparison.ps1 | 370 ++++++++++++++++++ .../AzureDbSample/pooling-comparison.sh | 317 +++++++++++++++ SimNextgenApp.Demo/Program.cs | 118 ++++++ .../Scenarios/AzurePgsqlPoolingScenario.cs | 275 +++++++++++++ 9 files changed, 1778 insertions(+), 2 deletions(-) create mode 100644 SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs create mode 100644 SimNextgenApp.Demo/AzureDbSample/PostgresQuery.cs create mode 100755 SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 create mode 100755 SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh create mode 100644 SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 create mode 100755 SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh create mode 100644 SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs diff --git a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs index 6a45a17..a4176fa 100644 --- a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs +++ b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs @@ -45,6 +45,10 @@ private double Credits private double BurnRatePerSec => spec.VCores / 60.0; private bool IsBurstable => _burstableSpec != null; + // PostgreSQL connection overhead constants (for PgBouncer-style pooling) + private const double ConnectionOverheadSecs = 0.050; // 50ms for new connection setup + private const double TransactionResetOverheadSecs = 0.008; // 8ms for DISCARD ALL (state reset) + public void SetContext(IRunContext context) { _engineContext = context; @@ -106,8 +110,36 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) double estimatedBurstCost = spec.FastSecs * BurnRatePerSec; bool isThrottled = IsBurstable && Credits < estimatedBurstCost; - // 3. Determine Service Time - double baseTime = isThrottled ? spec.SlowSecs : spec.FastSecs; + // 3. Determine Service Time (with PostgreSQL connection overhead if applicable) + double baseTime; + if (load is PostgresQuery query) + { + // Get base execution time (burst or throttled) + double executionTime = isThrottled ? spec.SlowSecs : spec.FastSecs; + + if (query.IsNewConnection) + { + // Full connection setup overhead (direct or pool miss) + baseTime = executionTime + ConnectionOverheadSecs; + } + else if (query.PoolMode == PoolingMode.TransactionPooling) + { + // Transaction pooling: connection state reset overhead + // (DISCARD ALL, temp table cleanup, session var reset) + baseTime = executionTime + TransactionResetOverheadSecs; + } + else + { + // Session pooling: no overhead (connection reused as-is) + baseTime = executionTime; + } + } + else + { + // Fallback for non-PostgresQuery loads + baseTime = isThrottled ? spec.SlowSecs : spec.FastSecs; + } + double actualDuration = -baseTime * Math.Log(1.0 - rnd.NextDouble()); // 4. Pay the Bill diff --git a/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs b/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs new file mode 100644 index 0000000..1580e26 --- /dev/null +++ b/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs @@ -0,0 +1,74 @@ +namespace SimNextgenApp.Demo.AzureDbSample; + +/// +/// Simulates a PgBouncer-style connection pool for PostgreSQL. +/// Manages connection lifecycle based on pooling mode. +/// +internal class ConnectionPool +{ + private readonly int _poolSize; + private readonly PoolingMode _mode; + private readonly HashSet _availableConnections; + private readonly Dictionary _assignedConnections; // Query → Connection + + public ConnectionPool(int poolSize, PoolingMode mode) + { + _poolSize = poolSize; + _mode = mode; + _availableConnections = new HashSet(); + _assignedConnections = new Dictionary(); + + // Initialize pool with connection IDs + for (int i = 0; i < poolSize; i++) + { + _availableConnections.Add($"conn_{i}"); + } + } + + /// + /// Attempts to acquire a connection from the pool. + /// Returns connection ID if successful, null if pool is exhausted. + /// + public string? AcquireConnection(string queryId) + { + if (_availableConnections.Count == 0) + { + // Pool exhausted - query must wait + return null; + } + + // Get first available connection + string connectionId = _availableConnections.First(); + _availableConnections.Remove(connectionId); + _assignedConnections[queryId] = connectionId; + + return connectionId; + } + + /// + /// Releases a connection back to the pool after query completion. + /// + public void ReleaseConnection(string queryId) + { + if (_assignedConnections.TryGetValue(queryId, out string? connectionId)) + { + _assignedConnections.Remove(queryId); + _availableConnections.Add(connectionId); + } + } + + /// + /// Number of available connections in the pool. + /// + public int AvailableCount => _availableConnections.Count; + + /// + /// Number of connections currently in use. + /// + public int InUseCount => _assignedConnections.Count; + + /// + /// Indicates whether the pool is fully exhausted (no available connections). + /// + public bool IsExhausted => _availableConnections.Count == 0; +} diff --git a/SimNextgenApp.Demo/AzureDbSample/PostgresQuery.cs b/SimNextgenApp.Demo/AzureDbSample/PostgresQuery.cs new file mode 100644 index 0000000..48158ed --- /dev/null +++ b/SimNextgenApp.Demo/AzureDbSample/PostgresQuery.cs @@ -0,0 +1,49 @@ +using SimNextgenApp.Demo.CustomModels; + +namespace SimNextgenApp.Demo.AzureDbSample; + +/// +/// Represents a PostgreSQL query with connection pooling metadata. +/// Used to simulate PgBouncer-style connection pooling overhead. +/// +internal class PostgresQuery : MyLoad +{ + /// + /// Indicates whether this query requires a new connection to be established. + /// True = new connection (50ms overhead), False = reused connection + /// + public bool IsNewConnection { get; set; } + + /// + /// The connection ID assigned to this query (for tracking pool usage). + /// + public string ConnectionId { get; set; } = string.Empty; + + /// + /// The pooling mode used for this query. + /// + public PoolingMode PoolMode { get; set; } +} + +/// +/// PostgreSQL connection pooling modes (PgBouncer-style). +/// +internal enum PoolingMode +{ + /// + /// Direct mode: New connection per query (50ms overhead every query). + /// + Direct, + + /// + /// Session pooling: Connection held for session, reused with no overhead. + /// Best for most workloads. + /// + SessionPooling, + + /// + /// Transaction pooling: Connection released after transaction with state reset. + /// Adds 8ms DISCARD ALL overhead per query. + /// + TransactionPooling +} diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 new file mode 100755 index 0000000..6ed0c51 --- /dev/null +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 @@ -0,0 +1,256 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Compare PostgreSQL connection pool performance across different pool sizes. + +.DESCRIPTION + This script runs the azure-pgsql-pooling simulation in Session pooling mode + with varying pool sizes to determine optimal configuration. + +.PARAMETER Duration + Duration in seconds for each pool size test (default: 180) + +.PARAMETER Series + Azure instance series (default: "B") + +.PARAMETER Size + Azure instance size (default: "2ms") + +.PARAMETER InitialCredits + Initial CPU credits (default: 60) + +.PARAMETER PoolSizes + Array of pool sizes to test (default: 5,10,20,50,100) + +.EXAMPLE + ./pool-size-comparison.ps1 + Run comparison with default settings + +.EXAMPLE + ./pool-size-comparison.ps1 -Duration 300 -PoolSizes 10,25,50 + Run with custom duration and pool sizes +#> + +param( + [int]$Duration = 180, + [string]$Series = "B", + [string]$Size = "2ms", + [int]$InitialCredits = 60, + [int[]]$PoolSizes = @(5, 10, 20, 50, 100) +) + +# Color output functions +function Write-ColorOutput { + param([string]$Message, [string]$Color = "White") + Write-Host $Message -ForegroundColor $Color +} + +Write-ColorOutput "╔════════════════════════════════════════════════════════════════╗" "Cyan" +Write-ColorOutput "║ PostgreSQL Pool Size Optimization Analysis ║" "Cyan" +Write-ColorOutput "╚════════════════════════════════════════════════════════════════╝" "Cyan" +Write-Host "" +Write-ColorOutput "Configuration:" "Blue" +Write-Host " • Instance: Azure $Series.$Size" +Write-Host " • Duration per size: $Duration seconds" +Write-Host " • Pool sizes: $($PoolSizes -join ', ')" +Write-Host " • Initial Credits: $InitialCredits" +Write-Host " • Pooling Mode: Session (optimal for comparison)" +Write-Host "" + +$OverallStart = Get-Date + +# Create output directory +$OutputDir = "./output/pool_size_comparison" +New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null + +# Function to run a single pool size +function Run-PoolSize { + param([int]$PoolSize) + + Write-ColorOutput "═══════════════════════════════════════════════════════════════" "Cyan" + Write-ColorOutput "Testing Pool Size: $PoolSize" "Cyan" + Write-ColorOutput "═══════════════════════════════════════════════════════════════" "Cyan" + + $PoolStart = Get-Date + + # Run simulation with session pooling + dotnet run --project ../SimNextgenApp.Demo.csproj -- demo azure-pgsql-pooling ` + --mode session ` + --pool-size $PoolSize ` + --series $Series ` + --size $Size ` + --duration $Duration ` + --initial-credits $InitialCredits + + if ($LASTEXITCODE -ne 0) { + Write-ColorOutput "Simulation failed for pool size $PoolSize" "Red" + return $false + } + + # Backup results + $PoolDir = "$OutputDir/pool_$PoolSize" + New-Item -ItemType Directory -Force -Path $PoolDir | Out-Null + + if (Test-Path "./output/simulation_latency.csv") { + Copy-Item "./output/simulation_latency.csv" "$PoolDir/simulation_latency.csv" + Write-ColorOutput "✓ Backed up latency data" "Green" + } + if (Test-Path "./output/simulation_credits.csv") { + Copy-Item "./output/simulation_credits.csv" "$PoolDir/simulation_credits.csv" + Write-ColorOutput "✓ Backed up credits data" "Green" + } + + $PoolElapsed = (Get-Date) - $PoolStart + Write-ColorOutput "✓ Pool size $PoolSize completed ($([int]$PoolElapsed.TotalSeconds)s)" "Green" + Write-Host "" + return $true +} + +# Run all pool sizes +foreach ($PoolSize in $PoolSizes) { + Run-PoolSize -PoolSize $PoolSize +} + +# Generate summary statistics +Write-ColorOutput "═══════════════════════════════════════════════════════════════" "Cyan" +Write-ColorOutput "Generating Summary Statistics" "Cyan" +Write-ColorOutput "═══════════════════════════════════════════════════════════════" "Cyan" + +# Create summary data structures +$SummaryData = @() + +foreach ($PoolSize in $PoolSizes) { + $CsvFile = "$OutputDir/pool_$PoolSize/simulation_latency.csv" + if (Test-Path $CsvFile) { + $Data = Import-Csv $CsvFile + $Latencies = $Data | ForEach-Object { [double]$_.'Latency (ms)' } + + if ($Latencies.Count -gt 0) { + $SortedLatencies = $Latencies | Sort-Object + $Count = $SortedLatencies.Count + + $Stats = [PSCustomObject]@{ + PoolSize = $PoolSize + AvgLatency = [Math]::Round(($Latencies | Measure-Object -Average).Average, 2) + MinLatency = [Math]::Round(($Latencies | Measure-Object -Minimum).Minimum, 2) + MaxLatency = [Math]::Round(($Latencies | Measure-Object -Maximum).Maximum, 2) + } + + $SummaryData += $Stats + } + } +} + +# Export detailed summary +$SummaryData | Export-Csv "$OutputDir/latency_vs_pool_size.csv" -NoTypeInformation +Write-ColorOutput "✓ Created latency_vs_pool_size.csv" "Green" + +# Export simplified summary for charts +$SummaryData | Select-Object @{Name='Pool Size';Expression={$_.PoolSize}}, @{Name='Average Latency (ms)';Expression={$_.AvgLatency}} | + Export-Csv "$OutputDir/latency_summary.csv" -NoTypeInformation +Write-ColorOutput "✓ Created latency_summary.csv" "Green" + +# Check if graph-cli is available +$GraphAvailable = $null -ne (Get-Command graph -ErrorAction SilentlyContinue) + +if (-not $GraphAvailable) { + Write-ColorOutput "graph-cli not found. Install with: pip install graph-cli" "Yellow" + Write-ColorOutput "Skipping graph generation" "Yellow" + Write-ColorOutput "(CSV files are still available for manual plotting)" "Yellow" +} else { + Write-ColorOutput "Generating comparison charts..." "Blue" + + # Generate line chart: Average Latency vs Pool Size + graph "$OutputDir/latency_summary.csv" ` + --title "Average Latency vs Pool Size (Session Pooling)" ` + --xlabel "Pool Size" ` + --ylabel "Latency (ms)" ` + -o "$OutputDir/latency_vs_pool_size.png" + Write-ColorOutput "✓ Generated latency_vs_pool_size.png (line chart)" "Green" + + # Generate bar chart for easier reading (full scale from 0) + graph "$OutputDir/latency_summary.csv" ` + --title "Average Latency vs Pool Size (Session Pooling)" ` + --xlabel "Pool Size" ` + --ylabel "Latency (ms)" ` + --bar ` + --bar-label ` + -o "$OutputDir/latency_bar_chart.png" + Write-ColorOutput "✓ Generated latency_bar_chart.png (full scale)" "Green" + + # Generate zoomed bar chart (emphasizes differences) + $MinLatency = ($SummaryData | Measure-Object -Property AvgLatency -Minimum).Minimum + $MaxLatency = ($SummaryData | Measure-Object -Property AvgLatency -Maximum).Maximum + $RangeMin = $MinLatency - 5 + $RangeMax = $MaxLatency + 5 + + graph "$OutputDir/latency_summary.csv" ` + --bar ` + --bar-label ` + --title "Average Latency vs Pool Size (Zoomed)" ` + --xlabel "Pool Size" ` + --ylabel "Latency (ms)" ` + --yrange "$RangeMin`:$RangeMax" ` + -o "$OutputDir/latency_bar_chart_zoomed.png" + Write-ColorOutput "✓ Generated latency_bar_chart_zoomed.png (emphasizes differences)" "Green" + + # Generate individual time series for each pool size + foreach ($PoolSize in $PoolSizes) { + $CsvFile = "$OutputDir/pool_$PoolSize/simulation_latency.csv" + if (Test-Path $CsvFile) { + $Data = Import-Csv $CsvFile + $MaxLatency = ($Data | ForEach-Object { [double]$_.'Latency (ms)' } | Measure-Object -Maximum).Maximum + + graph $CsvFile ` + --title "Latency Time Series - Pool Size $PoolSize" ` + --color "blue" ` + --yrange "0:$MaxLatency" ` + -o "$OutputDir/pool_$PoolSize/latency_timeseries.png" + } + } + Write-ColorOutput "✓ Generated individual time series charts" "Green" +} + +# Display summary table +Write-Host "" +Write-ColorOutput "═══════════════════════════════════════════════════════════════" "Cyan" +Write-ColorOutput "Summary: Latency vs Pool Size" "Cyan" +Write-ColorOutput "═══════════════════════════════════════════════════════════════" "Cyan" + +Write-Host $("{0,-12} {1,-15} {2,-15} {3,-15}" -f "Pool Size", "Avg Latency", "Min Latency", "Max Latency") +Write-Host "────────────────────────────────────────────────────────────────────" + +foreach ($Stats in $SummaryData) { + Write-Host $("{0,-12} {1,10:F2} ms {2,10:F2} ms {3,10:F2} ms" -f ` + $Stats.PoolSize, $Stats.AvgLatency, $Stats.MinLatency, $Stats.MaxLatency) +} + +Write-Host "" +Write-ColorOutput "Recommendations:" "Blue" + +# Find optimal pool size (lowest average latency) +$OptimalStats = $SummaryData | Sort-Object AvgLatency | Select-Object -First 1 +Write-ColorOutput " • Optimal pool size: $($OptimalStats.PoolSize) (avg latency: $($OptimalStats.AvgLatency)ms)" "Green" + +# Check for diminishing returns (when improvement < 5%) +for ($i = 1; $i -lt $SummaryData.Count; $i++) { + $PrevAvg = $SummaryData[$i-1].AvgLatency + $CurrentAvg = $SummaryData[$i].AvgLatency + $Improvement = ($PrevAvg - $CurrentAvg) / $PrevAvg * 100 + + if ($Improvement -lt 0) { + Write-ColorOutput " • Pool size $($SummaryData[$i].PoolSize): Performance degraded ($([Math]::Round($Improvement, 2))% worse)" "Yellow" + } elseif ($Improvement -lt 5) { + Write-ColorOutput " • Pool size $($SummaryData[$i].PoolSize): Diminishing returns (<5% improvement)" "Yellow" + } +} + +$OverallElapsed = (Get-Date) - $OverallStart +Write-Host "" +Write-ColorOutput "╔════════════════════════════════════════════════════════════════╗" "Green" +Write-ColorOutput "║ Pool Size Comparison Complete! (Total: $([int]$OverallElapsed.TotalSeconds)s) ║" "Green" +Write-ColorOutput "╚════════════════════════════════════════════════════════════════╝" "Green" +Write-Host "" +Write-Host "Results saved in: " -NoNewline +Write-ColorOutput "$OutputDir" "Blue" diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh new file mode 100755 index 0000000..d5679fb --- /dev/null +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh @@ -0,0 +1,285 @@ +#!/bin/bash + +# SYNOPSIS +# Compare PostgreSQL connection pool performance across different pool sizes. +# DESCRIPTION +# This script runs the azure-pgsql-pooling simulation in Session pooling mode +# with varying pool sizes to determine optimal configuration. +# EXAMPLES +# # Run comparison with default settings (180 seconds per size) +# ./pool-size-comparison.sh +# +# # Run with custom duration +# ./pool-size-comparison.sh --duration 300 + +# ANSI color codes +CYAN='\033[0;36m' +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Default configuration +DURATION=180 +SERIES="B" +SIZE="2ms" +INITIAL_CREDITS=60 +POOL_SIZES=(5 10 20 50 100) + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --duration) + DURATION="$2" + shift 2 + ;; + --series) + SERIES="$2" + shift 2 + ;; + --size) + SIZE="$2" + shift 2 + ;; + --initial-credits) + INITIAL_CREDITS="$2" + shift 2 + ;; + --pool-sizes) + IFS=',' read -ra POOL_SIZES <<< "$2" + shift 2 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + exit 1 + ;; + esac +done + +echo -e "${CYAN}╔════════════════════════════════════════════════════════════════╗${NC}" +echo -e "${CYAN}║ PostgreSQL Pool Size Optimization Analysis ║${NC}" +echo -e "${CYAN}╚════════════════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e "${BLUE}Configuration:${NC}" +echo -e " • Instance: Azure ${SERIES}.${SIZE}" +echo -e " • Duration per size: ${DURATION} seconds" +echo -e " • Pool sizes: ${POOL_SIZES[*]}" +echo -e " • Initial Credits: ${INITIAL_CREDITS}" +echo -e " • Pooling Mode: Session (optimal for comparison)" +echo "" + +OVERALL_START=$SECONDS + +# Create output directory +mkdir -p ./output/pool_size_comparison + +# Function to run a single pool size +run_pool_size() { + local POOL_SIZE=$1 + + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${CYAN}Testing Pool Size: ${POOL_SIZE}${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + + POOL_START=$SECONDS + + # Run simulation with session pooling + dotnet run --project ../SimNextgenApp.Demo.csproj -- demo azure-pgsql-pooling \ + --mode session \ + --pool-size "$POOL_SIZE" \ + --series "$SERIES" \ + --size "$SIZE" \ + --duration "$DURATION" \ + --initial-credits "$INITIAL_CREDITS" + + if [ $? -ne 0 ]; then + echo -e "${RED}Simulation failed for pool size ${POOL_SIZE}${NC}" + return 1 + fi + + # Backup results + mkdir -p "./output/pool_size_comparison/pool_${POOL_SIZE}" + if [ -f ./output/simulation_latency.csv ]; then + cp ./output/simulation_latency.csv "./output/pool_size_comparison/pool_${POOL_SIZE}/simulation_latency.csv" + echo -e "${GREEN}✓ Backed up latency data${NC}" + fi + if [ -f ./output/simulation_credits.csv ]; then + cp ./output/simulation_credits.csv "./output/pool_size_comparison/pool_${POOL_SIZE}/simulation_credits.csv" + echo -e "${GREEN}✓ Backed up credits data${NC}" + fi + + POOL_ELAPSED=$(($SECONDS - $POOL_START)) + echo -e "${GREEN}✓ Pool size ${POOL_SIZE} completed (${POOL_ELAPSED}s)${NC}" + echo "" +} + +# Run all pool sizes +for POOL_SIZE in "${POOL_SIZES[@]}"; do + run_pool_size "$POOL_SIZE" +done + +# Generate summary statistics +echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" +echo -e "${CYAN}Generating Summary Statistics${NC}" +echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + +# Create summary CSV for latency vs pool size +cat > "./output/pool_size_comparison/latency_vs_pool_size.csv" << 'EOF' +Pool Size,Average Latency (ms),Min Latency (ms),Max Latency (ms) +EOF + +for POOL_SIZE in "${POOL_SIZES[@]}"; do + CSV_FILE="./output/pool_size_comparison/pool_${POOL_SIZE}/simulation_latency.csv" + if [ -f "$CSV_FILE" ]; then + # Calculate statistics using awk + STATS=$(awk -F, 'NR>1 { + sum+=$2; + count++; + if(NR==2 || $2max) max=$2; + } + END { + printf "%.2f,%.2f,%.2f", sum/count, min, max; + }' "$CSV_FILE") + + echo "${POOL_SIZE},${STATS}" >> "./output/pool_size_comparison/latency_vs_pool_size.csv" + fi +done + +echo -e "${GREEN}✓ Created latency_vs_pool_size.csv${NC}" + +# Create simplified summary for bar charts +cat > "./output/pool_size_comparison/latency_summary.csv" << 'EOF' +Pool Size,Average Latency (ms) +EOF + +for POOL_SIZE in "${POOL_SIZES[@]}"; do + CSV_FILE="./output/pool_size_comparison/pool_${POOL_SIZE}/simulation_latency.csv" + if [ -f "$CSV_FILE" ]; then + AVG=$(awk -F, 'NR>1 {sum+=$2; count++} END {printf "%.2f", sum/count}' "$CSV_FILE") + echo "${POOL_SIZE},${AVG}" >> "./output/pool_size_comparison/latency_summary.csv" + fi +done + +echo -e "${GREEN}✓ Created latency_summary.csv${NC}" + +# Generate graphs if graph-cli is available +if ! command -v graph &> /dev/null; then + echo -e "${YELLOW}graph-cli not found. Install with: pip install graph-cli${NC}" + echo -e "${YELLOW}Skipping graph generation${NC}" + echo -e "${YELLOW}(CSV files are still available for manual plotting)${NC}" +else + echo -e "${BLUE}Generating comparison charts...${NC}" + + # Generate line chart: Average Latency vs Pool Size + graph "./output/pool_size_comparison/latency_summary.csv" \ + --title "Average Latency vs Pool Size (Session Pooling)" \ + --xlabel "Pool Size" \ + --ylabel "Latency (ms)" \ + -o "./output/pool_size_comparison/latency_vs_pool_size.png" + echo -e "${GREEN}✓ Generated latency_vs_pool_size.png (line chart)${NC}" + + # Generate bar chart for easier reading (full scale from 0) + graph "./output/pool_size_comparison/latency_summary.csv" \ + --title "Average Latency vs Pool Size (Session Pooling)" \ + --xlabel "Pool Size" \ + --ylabel "Latency (ms)" \ + --bar \ + --bar-label \ + -o "./output/pool_size_comparison/latency_bar_chart.png" + echo -e "${GREEN}✓ Generated latency_bar_chart.png (full scale)${NC}" + + # Generate zoomed bar chart (emphasizes differences) + MIN_LATENCY=$(awk -F, 'NR>1 {if(NR==2 || $21 {if(NR==2 || $2>max) max=$2} END {printf "%.0f", max}' "./output/pool_size_comparison/latency_summary.csv") + RANGE_MIN=$(echo "$MIN_LATENCY - 5" | bc) + RANGE_MAX=$(echo "$MAX_LATENCY + 5" | bc) + + graph "./output/pool_size_comparison/latency_summary.csv" \ + --bar \ + --bar-label \ + --title "Average Latency vs Pool Size (Zoomed)" \ + --xlabel "Pool Size" \ + --ylabel "Latency (ms)" \ + --yrange=$RANGE_MIN:$RANGE_MAX \ + -o "./output/pool_size_comparison/latency_bar_chart_zoomed.png" + echo -e "${GREEN}✓ Generated latency_bar_chart_zoomed.png (emphasizes differences)${NC}" + + # Generate individual time series for each pool size + for POOL_SIZE in "${POOL_SIZES[@]}"; do + CSV_FILE="./output/pool_size_comparison/pool_${POOL_SIZE}/simulation_latency.csv" + if [ -f "$CSV_FILE" ]; then + LATENCY_MAX=$(awk -F, 'BEGIN {max=0} NR>1 {if($2>max) max=$2} END {print (max==0?1:max)}' "$CSV_FILE") + graph "$CSV_FILE" \ + --title "Latency Time Series - Pool Size ${POOL_SIZE}" \ + --color "blue" \ + --yrange=0:$LATENCY_MAX \ + -o "./output/pool_size_comparison/pool_${POOL_SIZE}/latency_timeseries.png" + fi + done + echo -e "${GREEN}✓ Generated individual time series charts${NC}" +fi + +# Display summary table +echo "" +echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" +echo -e "${CYAN}Summary: Latency vs Pool Size${NC}" +echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + +printf "%-12s %-15s %-15s %-15s\n" "Pool Size" "Avg Latency" "Min Latency" "Max Latency" +echo "────────────────────────────────────────────────────────────────────" + +for POOL_SIZE in "${POOL_SIZES[@]}"; do + CSV_FILE="./output/pool_size_comparison/pool_${POOL_SIZE}/simulation_latency.csv" + if [ -f "$CSV_FILE" ]; then + STATS=$(awk -F, 'NR>1 { + sum+=$2; + count++; + if(NR==2 || $2max) max=$2 + } + END { + printf "%.2f %.2f %.2f", sum/count, min, max + }' "$CSV_FILE") + + read AVG MIN MAX <<< "$STATS" + printf "%-12s %10.2f ms %10.2f ms %10.2f ms\n" "$POOL_SIZE" "$AVG" "$MIN" "$MAX" + fi +done + +echo "" +echo -e "${BLUE}Recommendations:${NC}" + +# Find optimal pool size (lowest average latency) +OPTIMAL_SIZE=$(awk -F, 'NR>1 {if(NR==2 || $21 {if(NR==2 || $21 && $1==size {print $2}' "./output/pool_size_comparison/latency_summary.csv") + if [ -n "$LAST_AVG" ]; then + IMPROVEMENT=$(echo "scale=2; ($LAST_AVG - $CURRENT_AVG) / $LAST_AVG * 100" | bc) + IS_NEGATIVE=$(echo "$IMPROVEMENT < 0" | bc) + if [ "$IS_NEGATIVE" -eq 1 ]; then + echo -e " • ${YELLOW}Pool size ${POOL_SIZE}: Performance degraded (${IMPROVEMENT}% worse)${NC}" + else + IS_SMALL=$(echo "$IMPROVEMENT < 5" | bc) + if [ "$IS_SMALL" -eq 1 ]; then + echo -e " • ${YELLOW}Pool size ${POOL_SIZE}: Diminishing returns (<5% improvement)${NC}" + fi + fi + fi + LAST_AVG=$CURRENT_AVG +done + +OVERALL_ELAPSED=$(($SECONDS - $OVERALL_START)) +echo "" +echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Pool Size Comparison Complete! (Total: ${OVERALL_ELAPSED}s) ║${NC}" +echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e "Results saved in: ${BLUE}./output/pool_size_comparison/${NC}" diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 new file mode 100644 index 0000000..719de02 --- /dev/null +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 @@ -0,0 +1,370 @@ +<# +.SYNOPSIS + Run PostgreSQL connection pooling comparison across all three modes. +.DESCRIPTION + This script runs the azure-pgsql-pooling simulation for Direct, Session, and Transaction modes, + backs up the CSV results, and generates comparison graphs using graph-cli. +.EXAMPLE + # Run comparison with default settings (120 seconds, pool size 20) + ./pooling-comparison.ps1 +.EXAMPLE + # Run with custom duration and pool size + ./pooling-comparison.ps1 -Duration 300 -PoolSize 30 +.PARAMETER Duration + Simulation duration in seconds (default: 120) +.PARAMETER PoolSize + Connection pool size (default: 20) +.PARAMETER Series + Azure instance series (default: B) +.PARAMETER Size + Azure instance size (default: 2ms) +.PARAMETER InitialCredits + Initial CPU credits (default: 60) +.LINK + https://github.com/gcl-team/SNAS +#> + +param( + [int]$Duration = 120, + [int]$PoolSize = 20, + [string]$Series = "B", + [string]$Size = "2ms", + [int]$InitialCredits = 60 +) + +Write-Host "╔════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan +Write-Host "║ PostgreSQL Connection Pooling Comparison ║" -ForegroundColor Cyan +Write-Host "╚════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan +Write-Host "" +Write-Host "Configuration:" -ForegroundColor Blue +Write-Host " • Instance: Azure $Series.$Size" +Write-Host " • Duration: $Duration seconds" +Write-Host " • Pool Size: $PoolSize" +Write-Host " • Initial Credits: $InitialCredits" +Write-Host "" + +$modes = @( + @{ Name = "direct"; Label = "Direct Connections (50ms overhead)" }, + @{ Name = "session"; Label = "Session Pooling (no overhead)" }, + @{ Name = "transaction"; Label = "Transaction Pooling (8ms overhead)" } +) + +$overallStart = Get-Date + +# Create backup directory +New-Item -ItemType Directory -Force -Path "./output/pooling_comparison" | Out-Null + +# Function to run a single mode +function Run-Mode { + param( + [string]$Mode, + [string]$ModeLabel + ) + + Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host "Running: $ModeLabel" -ForegroundColor Cyan + Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan + + $modeStart = Get-Date + + # Run simulation + dotnet run --project ../SimNextgenApp.Demo.csproj -- demo azure-pgsql-pooling ` + --mode $Mode ` + --pool-size $PoolSize ` + --series $Series ` + --size $Size ` + --duration $Duration ` + --initial-credits $InitialCredits + + if ($LASTEXITCODE -ne 0) { + Write-Host "Simulation failed for $Mode" -ForegroundColor Red + return $false + } + + # Backup results + $modeDir = "./output/pooling_comparison/$Mode" + New-Item -ItemType Directory -Force -Path $modeDir | Out-Null + + if (Test-Path ./output/simulation_latency.csv) { + Copy-Item ./output/simulation_latency.csv "$modeDir/simulation_latency.csv" + Write-Host "✓ Backed up latency data" -ForegroundColor Green + } + if (Test-Path ./output/simulation_credits.csv) { + Copy-Item ./output/simulation_credits.csv "$modeDir/simulation_credits.csv" + Write-Host "✓ Backed up credits data" -ForegroundColor Green + } + + $modeElapsed = ((Get-Date) - $modeStart).TotalSeconds + Write-Host "✓ $ModeLabel completed ($([int]$modeElapsed)s)" -ForegroundColor Green + Write-Host "" + + return $true +} + +# Run all three modes +foreach ($modeInfo in $modes) { + $result = Run-Mode -Mode $modeInfo.Name -ModeLabel $modeInfo.Label + if (-not $result) { + Write-Host "Exiting due to error" -ForegroundColor Red + exit 1 + } +} + +# Merge CSVs and generate comparison graphs +Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan +Write-Host "Merging Results & Generating Comparison Graphs" -ForegroundColor Cyan +Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan + +# Merge latency CSVs +$latencyDirect = "./output/pooling_comparison/direct/simulation_latency.csv" +$latencySession = "./output/pooling_comparison/session/simulation_latency.csv" +$latencyTransaction = "./output/pooling_comparison/transaction/simulation_latency.csv" + +if ((Test-Path $latencyDirect) -and (Test-Path $latencySession) -and (Test-Path $latencyTransaction)) { + Write-Host "Merging latency data..." -ForegroundColor Blue + + # Load all three CSV files + $directData = Import-Csv $latencyDirect + $sessionData = Import-Csv $latencySession + $transactionData = Import-Csv $latencyTransaction + + # Create hashtables indexed by rounded time + $directHash = @{} + $sessionHash = @{} + $transactionHash = @{} + + foreach ($row in $directData) { + $time = [math]::Round([double]$row."Simulation Time (s)", 2) + $directHash[$time] = $row."Latency (ms)" + } + + foreach ($row in $sessionData) { + $time = [math]::Round([double]$row."Simulation Time (s)", 2) + $sessionHash[$time] = $row."Latency (ms)" + } + + foreach ($row in $transactionData) { + $time = [math]::Round([double]$row."Simulation Time (s)", 2) + $transactionHash[$time] = $row."Latency (ms)" + } + + # Merge data + $mergedLatency = @() + $mergedLatency += "Simulation Time (s),Direct (ms),Session (ms),Transaction (ms)" + + $allTimes = $directHash.Keys | Sort-Object + foreach ($time in $allTimes) { + if ($sessionHash.ContainsKey($time) -and $transactionHash.ContainsKey($time)) { + $mergedLatency += "$time,$($directHash[$time]),$($sessionHash[$time]),$($transactionHash[$time])" + } + } + + $mergedLatency | Out-File "./output/pooling_comparison/latency_combined.csv" -Encoding UTF8 + Write-Host "✓ Created latency_combined.csv" -ForegroundColor Green +} + +# Merge credits CSVs +$creditsDirect = "./output/pooling_comparison/direct/simulation_credits.csv" +$creditsSession = "./output/pooling_comparison/session/simulation_credits.csv" +$creditsTransaction = "./output/pooling_comparison/transaction/simulation_credits.csv" + +if ((Test-Path $creditsDirect) -and (Test-Path $creditsSession) -and (Test-Path $creditsTransaction)) { + Write-Host "Merging credits data..." -ForegroundColor Blue + + # Load all three CSV files + $directData = Import-Csv $creditsDirect + $sessionData = Import-Csv $creditsSession + $transactionData = Import-Csv $creditsTransaction + + # Create hashtables indexed by rounded time + $directHash = @{} + $sessionHash = @{} + $transactionHash = @{} + + foreach ($row in $directData) { + $time = [math]::Round([double]$row."Simulation Time (s)", 2) + $directHash[$time] = $row."Credits" + } + + foreach ($row in $sessionData) { + $time = [math]::Round([double]$row."Simulation Time (s)", 2) + $sessionHash[$time] = $row."Credits" + } + + foreach ($row in $transactionData) { + $time = [math]::Round([double]$row."Simulation Time (s)", 2) + $transactionHash[$time] = $row."Credits" + } + + # Merge data + $mergedCredits = @() + $mergedCredits += "Simulation Time (s),Direct,Session,Transaction" + + $allTimes = $directHash.Keys | Sort-Object + foreach ($time in $allTimes) { + if ($sessionHash.ContainsKey($time) -and $transactionHash.ContainsKey($time)) { + $mergedCredits += "$time,$($directHash[$time]),$($sessionHash[$time]),$($transactionHash[$time])" + } + } + + $mergedCredits | Out-File "./output/pooling_comparison/credits_combined.csv" -Encoding UTF8 + Write-Host "✓ Created credits_combined.csv" -ForegroundColor Green +} + +# Generate individual graphs for PowerPoint overlay (with distinct colors!) +if (-not (Get-Command graph -ErrorAction SilentlyContinue)) { + Write-Host "graph-cli not found. Install with: pip install graph-cli" -ForegroundColor Yellow + Write-Host "Skipping graph generation" -ForegroundColor Yellow + Write-Host "(Merged CSV files are still available for manual plotting)" -ForegroundColor Yellow +} else { + Write-Host "Generating individual charts (with distinct colors for overlay)..." -ForegroundColor Blue + + # Generate individual latency and credits graphs with distinct colors + foreach ($modeInfo in $modes) { + $mode = $modeInfo.Name + $modeDir = "./output/pooling_comparison/$mode" + + # Set color based on mode + $color = switch ($mode) { + "direct" { "red" } # Red = worst (highest overhead) + "session" { "green" } # Green = best (no overhead) + "transaction" { "orange" } # Orange = middle (8ms overhead) + } + + # Generate latency graph + $latencyFile = "$modeDir/simulation_latency.csv" + if (Test-Path $latencyFile) { + $latency = Import-Csv $latencyFile + $latencyMax = ($latency | ForEach-Object { $_."Latency (ms)" } | Measure-Object -Maximum).Maximum + if ($null -eq $latencyMax) { $latencyMax = 1 } + + graph $latencyFile --title "Latency - $mode" --color $color --yrange="0:$latencyMax" -o "$modeDir/latency.png" + Write-Host "✓ Generated $mode latency graph ($color)" -ForegroundColor Green + } + + # Generate credits graph + $creditsFile = "$modeDir/simulation_credits.csv" + if (Test-Path $creditsFile) { + $credits = Import-Csv $creditsFile + $creditMax = ($credits | Measure-Object -Property "Credits" -Maximum).Maximum + if ($null -eq $creditMax) { $creditMax = 1 } + + graph $creditsFile --title "Credits - $mode" --color $color --yrange="0:$creditMax" -o "$modeDir/credits.png" + Write-Host "✓ Generated $mode credits graph ($color)" -ForegroundColor Green + } + } + + # Also generate combined overlay graph (all colors in one) + $latencyCombined = "./output/pooling_comparison/latency_combined.csv" + if (Test-Path $latencyCombined) { + $combinedData = Import-Csv $latencyCombined + $latencyMax = ($combinedData | ForEach-Object { + [math]::Max([math]::Max([double]$_."Direct (ms)", [double]$_."Session (ms)"), [double]$_."Transaction (ms)") + } | Measure-Object -Maximum).Maximum + + if ($null -eq $latencyMax) { $latencyMax = 1 } + + graph $latencyCombined --title "Connection Pooling Latency Comparison" --yrange="0:$latencyMax" -o "./output/pooling_comparison/latency_comparison.png" + Write-Host "✓ Generated latency_comparison.png (all modes)" -ForegroundColor Green + } + + $creditsCombined = "./output/pooling_comparison/credits_combined.csv" + if (Test-Path $creditsCombined) { + $combinedData = Import-Csv $creditsCombined + $creditMax = ($combinedData | ForEach-Object { + [math]::Max([math]::Max([double]$_.Direct, [double]$_.Session), [double]$_.Transaction) + } | Measure-Object -Maximum).Maximum + + if ($null -eq $creditMax) { $creditMax = 1 } + + graph $creditsCombined --title "Connection Pooling CPU Credits Comparison" --yrange="0:$creditMax" -o "./output/pooling_comparison/credits_comparison.png" + Write-Host "✓ Generated credits_comparison.png (all modes)" -ForegroundColor Green + } + + # Generate summary bar charts + Write-Host "Generating summary bar charts..." -ForegroundColor Blue + + # Calculate average latencies + $directData = Import-Csv "./output/pooling_comparison/direct/simulation_latency.csv" + $sessionData = Import-Csv "./output/pooling_comparison/session/simulation_latency.csv" + $transactionData = Import-Csv "./output/pooling_comparison/transaction/simulation_latency.csv" + + $directAvg = ($directData | ForEach-Object { [double]$_."Latency (ms)" } | Measure-Object -Average).Average + $sessionAvg = ($sessionData | ForEach-Object { [double]$_."Latency (ms)" } | Measure-Object -Average).Average + $transactionAvg = ($transactionData | ForEach-Object { [double]$_."Latency (ms)" } | Measure-Object -Average).Average + + # Create summary CSV for bar chart + $summaryCsv = @" +Mode,Average Latency (ms) +Direct,$([math]::Round($directAvg, 2)) +Session,$([math]::Round($sessionAvg, 2)) +Transaction,$([math]::Round($transactionAvg, 2)) +"@ + $summaryCsv | Out-File "./output/pooling_comparison/latency_summary.csv" -Encoding UTF8 + + # Generate bar chart (full scale from 0) + graph "./output/pooling_comparison/latency_summary.csv" --bar --bar-label --title "Average Latency Comparison" --ylabel "Latency (ms)" -o "./output/pooling_comparison/latency_bar_chart.png" + Write-Host "✓ Generated latency_bar_chart.png (full scale)" -ForegroundColor Green + + # Generate zoomed bar chart (emphasizes differences) + $summaryData = Import-Csv "./output/pooling_comparison/latency_summary.csv" + $latencies = $summaryData | ForEach-Object { [double]$_."Average Latency (ms)" } + $minLatency = ($latencies | Measure-Object -Minimum).Minimum + $maxLatency = ($latencies | Measure-Object -Maximum).Maximum + $rangeMin = [math]::Floor($minLatency - 0.1) + $rangeMax = [math]::Ceiling($maxLatency + 0.1) + + graph "./output/pooling_comparison/latency_summary.csv" --bar --bar-label --title "Average Latency Comparison (Zoomed)" --ylabel "Latency (ms)" --yrange="$rangeMin`:$rangeMax" -o "./output/pooling_comparison/latency_bar_chart_zoomed.png" + Write-Host "✓ Generated latency_bar_chart_zoomed.png (emphasizes differences)" -ForegroundColor Green +} + +# Calculate and display summary +Write-Host "" +Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan +Write-Host "Summary" -ForegroundColor Cyan +Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan + +$summaryFormat = "{0,-30} {1,15} {2,15} {3,15}" +Write-Host ($summaryFormat -f "Mode", "Avg Latency", "Min Latency", "Max Latency") +Write-Host "───────────────────────────────────────────────────────────────────────────" + +foreach ($modeInfo in $modes) { + $mode = $modeInfo.Name + $csvFile = "./output/pooling_comparison/$mode/simulation_latency.csv" + + if (Test-Path $csvFile) { + $data = Import-Csv $csvFile + $latencies = $data | ForEach-Object { [double]$_."Latency (ms)" } + + $avg = ($latencies | Measure-Object -Average).Average + $min = ($latencies | Measure-Object -Minimum).Minimum + $max = ($latencies | Measure-Object -Maximum).Maximum + + $modeLabel = switch ($mode) { + "direct" { "Direct (50ms overhead)" } + "session" { "Session Pooling (no overhead)" } + "transaction" { "Transaction (8ms overhead)" } + } + + Write-Host ($summaryFormat -f $modeLabel, "$([math]::Round($avg, 2)) ms", "$([math]::Round($min, 2)) ms", "$([math]::Round($max, 2)) ms") + } +} + +Write-Host "═══════════════════════════════════════════════════════════════════════════" +Write-Host "" +Write-Host "🏆 RECOMMENDATIONS:" -ForegroundColor Green +Write-Host " • Session Pooling: " -NoNewline +Write-Host "✅ Best for most OLTP workloads (lowest latency)" -ForegroundColor Green +Write-Host " • Transaction Pooling: " -NoNewline +Write-Host "⚠️ Use only for serverless/multi-tenant scenarios" -ForegroundColor Yellow +Write-Host " • Direct Connections: " -NoNewline +Write-Host "❌ Avoid (highest overhead)" -ForegroundColor Red +Write-Host "" + +$overallElapsed = ((Get-Date) - $overallStart).TotalSeconds +Write-Host "╔════════════════════════════════════════════════════════════════╗" -ForegroundColor Green +Write-Host "║ Comparison Complete! (Total Duration: $([int]$overallElapsed)s) ║" -ForegroundColor Green +Write-Host "╚════════════════════════════════════════════════════════════════╝" -ForegroundColor Green +Write-Host "" +Write-Host "Results saved in: " -NoNewline +Write-Host "./output/pooling_comparison/" -ForegroundColor Blue diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh new file mode 100755 index 0000000..5d713f3 --- /dev/null +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh @@ -0,0 +1,317 @@ +#!/bin/bash + +# SYNOPSIS +# Run PostgreSQL connection pooling comparison across all three modes. +# DESCRIPTION +# This script runs the azure-pgsql-pooling simulation for Direct, Session, and Transaction modes, +# backs up the CSV results, and generates comparison graphs using graph-cli. +# EXAMPLES +# # Run comparison with default settings (120 seconds, pool size 20) +# ./pooling-comparison.sh +# +# # Run with custom duration and pool size +# ./pooling-comparison.sh --duration 300 --pool-size 30 + +# ANSI color codes +CYAN='\033[0;36m' +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Default configuration +DURATION=120 +POOL_SIZE=20 +SERIES="B" +SIZE="2ms" +INITIAL_CREDITS=60 + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --duration) + DURATION="$2" + shift 2 + ;; + --pool-size) + POOL_SIZE="$2" + shift 2 + ;; + --series) + SERIES="$2" + shift 2 + ;; + --size) + SIZE="$2" + shift 2 + ;; + --initial-credits) + INITIAL_CREDITS="$2" + shift 2 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + exit 1 + ;; + esac +done + +echo -e "${CYAN}╔════════════════════════════════════════════════════════════════╗${NC}" +echo -e "${CYAN}║ PostgreSQL Connection Pooling Comparison ║${NC}" +echo -e "${CYAN}╚════════════════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e "${BLUE}Configuration:${NC}" +echo -e " • Instance: Azure ${SERIES}.${SIZE}" +echo -e " • Duration: ${DURATION} seconds" +echo -e " • Pool Size: ${POOL_SIZE}" +echo -e " • Initial Credits: ${INITIAL_CREDITS}" +echo "" + +MODES=("direct" "session" "transaction") +OVERALL_START=$SECONDS + +# Create backup directory +mkdir -p ./output/pooling_comparison + +# Function to run a single mode +run_mode() { + local MODE=$1 + local MODE_LABEL=$2 + + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${CYAN}Running: ${MODE_LABEL}${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + + MODE_START=$SECONDS + + # Run simulation + dotnet run --project ../SimNextgenApp.Demo.csproj -- demo azure-pgsql-pooling \ + --mode "$MODE" \ + --pool-size "$POOL_SIZE" \ + --series "$SERIES" \ + --size "$SIZE" \ + --duration "$DURATION" \ + --initial-credits "$INITIAL_CREDITS" + + if [ $? -ne 0 ]; then + echo -e "${RED}Simulation failed for ${MODE}${NC}" + return 1 + fi + + # Backup results + mkdir -p "./output/pooling_comparison/${MODE}" + if [ -f ./output/simulation_latency.csv ]; then + cp ./output/simulation_latency.csv "./output/pooling_comparison/${MODE}/simulation_latency.csv" + echo -e "${GREEN}✓ Backed up latency data${NC}" + fi + if [ -f ./output/simulation_credits.csv ]; then + cp ./output/simulation_credits.csv "./output/pooling_comparison/${MODE}/simulation_credits.csv" + echo -e "${GREEN}✓ Backed up credits data${NC}" + fi + + MODE_ELAPSED=$(($SECONDS - $MODE_START)) + echo -e "${GREEN}✓ ${MODE_LABEL} completed (${MODE_ELAPSED}s)${NC}" + echo "" +} + +# Run all three modes +run_mode "direct" "Direct Connections (50ms overhead)" +run_mode "session" "Session Pooling (no overhead)" +run_mode "transaction" "Transaction Pooling (8ms overhead)" + +# Merge CSVs and generate comparison graphs +echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" +echo -e "${CYAN}Merging Results & Generating Comparison Graphs${NC}" +echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + +# Merge latency CSVs +LATENCY_DIRECT="./output/pooling_comparison/direct/simulation_latency.csv" +LATENCY_SESSION="./output/pooling_comparison/session/simulation_latency.csv" +LATENCY_TRANSACTION="./output/pooling_comparison/transaction/simulation_latency.csv" + +if [ -f "$LATENCY_DIRECT" ] && [ -f "$LATENCY_SESSION" ] && [ -f "$LATENCY_TRANSACTION" ]; then + echo -e "${BLUE}Merging latency data...${NC}" + + # Create merged latency CSV using a three-pass approach + { + echo "Simulation Time (s),Direct (ms),Session (ms),Transaction (ms)" + + # Build associative arrays using join + join -t, -j1 -o 1.1,1.2,2.2 \ + <(tail -n +2 "$LATENCY_DIRECT" | awk -F, '{printf "%.2f,%s\n", $1, $2}' | sort -t, -k1 -n) \ + <(tail -n +2 "$LATENCY_SESSION" | awk -F, '{printf "%.2f,%s\n", $1, $2}' | sort -t, -k1 -n) \ + | join -t, -j1 -o 1.1,1.2,1.3,2.2 - \ + <(tail -n +2 "$LATENCY_TRANSACTION" | awk -F, '{printf "%.2f,%s\n", $1, $2}' | sort -t, -k1 -n) + } > "./output/pooling_comparison/latency_combined.csv" + + echo -e "${GREEN}✓ Created latency_combined.csv${NC}" +fi + +# Merge credits CSVs +CREDITS_DIRECT="./output/pooling_comparison/direct/simulation_credits.csv" +CREDITS_SESSION="./output/pooling_comparison/session/simulation_credits.csv" +CREDITS_TRANSACTION="./output/pooling_comparison/transaction/simulation_credits.csv" + +if [ -f "$CREDITS_DIRECT" ] && [ -f "$CREDITS_SESSION" ] && [ -f "$CREDITS_TRANSACTION" ]; then + echo -e "${BLUE}Merging credits data...${NC}" + + # Create merged credits CSV using a three-pass approach + { + echo "Simulation Time (s),Direct,Session,Transaction" + + # Build associative arrays using join + join -t, -j1 -o 1.1,1.2,2.2 \ + <(tail -n +2 "$CREDITS_DIRECT" | awk -F, '{printf "%.2f,%s\n", $1, $2}' | sort -t, -k1 -n) \ + <(tail -n +2 "$CREDITS_SESSION" | awk -F, '{printf "%.2f,%s\n", $1, $2}' | sort -t, -k1 -n) \ + | join -t, -j1 -o 1.1,1.2,1.3,2.2 - \ + <(tail -n +2 "$CREDITS_TRANSACTION" | awk -F, '{printf "%.2f,%s\n", $1, $2}' | sort -t, -k1 -n) + } > "./output/pooling_comparison/credits_combined.csv" + + echo -e "${GREEN}✓ Created credits_combined.csv${NC}" +fi + +# Generate individual graphs for PowerPoint overlay (with distinct colors!) +if ! command -v graph &> /dev/null; then + echo -e "${YELLOW}graph-cli not found. Install with: pip install graph-cli${NC}" + echo -e "${YELLOW}Skipping graph generation${NC}" + echo -e "${YELLOW}(Merged CSV files are still available for manual plotting)${NC}" +else + echo -e "${BLUE}Generating individual charts (with distinct colors for overlay)...${NC}" + + # Generate individual latency graphs with distinct colors + for MODE in "${MODES[@]}"; do + MODE_DIR="./output/pooling_comparison/${MODE}" + + # Set color based on mode + case $MODE in + direct) COLOR="red" ;; # Red = worst (highest overhead) + session) COLOR="green" ;; # Green = best (no overhead) + transaction) COLOR="orange" ;; # Orange = middle (8ms overhead) + esac + + if [ -f "${MODE_DIR}/simulation_latency.csv" ]; then + LATENCY_MAX=$(awk -F, 'BEGIN {max=0} NR>1 {if($2>max) max=$2} END {print (max==0?1:max)}' "${MODE_DIR}/simulation_latency.csv") + graph "${MODE_DIR}/simulation_latency.csv" \ + --title "Latency - ${MODE}" \ + --color "$COLOR" \ + --yrange=0:$LATENCY_MAX \ + -o "${MODE_DIR}/latency.png" + echo -e "${GREEN}✓ Generated ${MODE} latency graph (${COLOR})${NC}" + fi + + if [ -f "${MODE_DIR}/simulation_credits.csv" ]; then + CREDIT_MAX=$(awk -F, 'BEGIN {max=0} NR>1 {if($2>max) max=$2} END {print (max==0?1:max)}' "${MODE_DIR}/simulation_credits.csv") + graph "${MODE_DIR}/simulation_credits.csv" \ + --title "Credits - ${MODE}" \ + --color "$COLOR" \ + --yrange=0:$CREDIT_MAX \ + -o "${MODE_DIR}/credits.png" + echo -e "${GREEN}✓ Generated ${MODE} credits graph (${COLOR})${NC}" + fi + done + + # Also generate combined overlay graph (all colors in one) + if [ -f "./output/pooling_comparison/latency_combined.csv" ]; then + LATENCY_MAX=$(awk -F, 'BEGIN {max=0} NR>1 {for(i=2;i<=NF;i++){if($i>max)max=$i}} END {print (max==0?1:max)}' "./output/pooling_comparison/latency_combined.csv") + graph "./output/pooling_comparison/latency_combined.csv" \ + --title "Connection Pooling Latency Comparison" \ + --yrange=0:$LATENCY_MAX \ + -o "./output/pooling_comparison/latency_comparison.png" + echo -e "${GREEN}✓ Generated latency_comparison.png (all modes)${NC}" + fi + + # Generate summary bar charts + echo -e "${BLUE}Generating summary bar charts...${NC}" + + # Calculate average latencies + DIRECT_AVG=$(awk -F, 'NR>1 {sum+=$2; count++} END {printf "%.2f", sum/count}' "./output/pooling_comparison/direct/simulation_latency.csv") + SESSION_AVG=$(awk -F, 'NR>1 {sum+=$2; count++} END {printf "%.2f", sum/count}' "./output/pooling_comparison/session/simulation_latency.csv") + TRANSACTION_AVG=$(awk -F, 'NR>1 {sum+=$2; count++} END {printf "%.2f", sum/count}' "./output/pooling_comparison/transaction/simulation_latency.csv") + + # Create summary CSV for bar chart + cat > "./output/pooling_comparison/latency_summary.csv" << EOF +Mode,Average Latency (ms) +Direct,$DIRECT_AVG +Session,$SESSION_AVG +Transaction,$TRANSACTION_AVG +EOF + + # Generate bar chart (full scale from 0) + graph "./output/pooling_comparison/latency_summary.csv" \ + --bar \ + --bar-label \ + --title "Average Latency Comparison" \ + --ylabel "Latency (ms)" \ + -o "./output/pooling_comparison/latency_bar_chart.png" + echo -e "${GREEN}✓ Generated latency_bar_chart.png (full scale)${NC}" + + # Generate zoomed bar chart (emphasizes differences) + MIN_LATENCY=$(awk -F, 'NR>1 {if(NR==2 || $21 {if(NR==2 || $2>max) max=$2} END {printf "%.0f", max}' "./output/pooling_comparison/latency_summary.csv") + RANGE_MIN=$(echo "$MIN_LATENCY - 0.1" | bc) + RANGE_MAX=$(echo "$MAX_LATENCY + 0.1" | bc) + + graph "./output/pooling_comparison/latency_summary.csv" \ + --bar \ + --bar-label \ + --title "Average Latency Comparison (Zoomed)" \ + --ylabel "Latency (ms)" \ + --yrange=$RANGE_MIN:$RANGE_MAX \ + -o "./output/pooling_comparison/latency_bar_chart_zoomed.png" + echo -e "${GREEN}✓ Generated latency_bar_chart_zoomed.png (emphasizes differences)${NC}" + + if [ -f "./output/pooling_comparison/credits_combined.csv" ]; then + CREDIT_MAX=$(awk -F, 'BEGIN {max=0} NR>1 {for(i=2;i<=NF;i++){if($i>max)max=$i}} END {print (max==0?1:max)}' "./output/pooling_comparison/credits_combined.csv") + graph "./output/pooling_comparison/credits_combined.csv" \ + --title "Connection Pooling CPU Credits Comparison" \ + --yrange=0:$CREDIT_MAX \ + -o "./output/pooling_comparison/credits_comparison.png" + echo -e "${GREEN}✓ Generated credits_comparison.png (all modes)${NC}" + fi +fi + +# Calculate and display summary +echo "" +echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" +echo -e "${CYAN}Summary${NC}" +echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + +printf "%-30s %-15s %-15s %-15s\n" "Mode" "Avg Latency" "Min Latency" "Max Latency" +echo "───────────────────────────────────────────────────────────────────────────" + +for MODE in "${MODES[@]}"; do + CSV_FILE="./output/pooling_comparison/${MODE}/simulation_latency.csv" + if [ -f "$CSV_FILE" ]; then + # Calculate stats using awk + STATS=$(awk -F, 'NR>1 { + sum+=$2; + count++; + if(NR==2 || $2max) max=$2 + } + END { + printf "%.2f %.2f %.2f", sum/count, min, max + }' "$CSV_FILE") + + read AVG MIN MAX <<< "$STATS" + + MODE_LABEL="" + case $MODE in + direct) MODE_LABEL="Direct (50ms overhead)" ;; + session) MODE_LABEL="Session Pooling (no overhead)" ;; + transaction) MODE_LABEL="Transaction (8ms overhead)" ;; + esac + + printf "%-30s %10.2f ms %10.2f ms %10.2f ms\n" "$MODE_LABEL" "$AVG" "$MIN" "$MAX" + fi +done + + + +OVERALL_ELAPSED=$(($SECONDS - $OVERALL_START)) +echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Comparison Complete! (Total Duration: ${OVERALL_ELAPSED}s) ║${NC}" +echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e "Results saved in: ${BLUE}./output/pooling_comparison/${NC}" diff --git a/SimNextgenApp.Demo/Program.cs b/SimNextgenApp.Demo/Program.cs index 94432dc..4eeedd2 100644 --- a/SimNextgenApp.Demo/Program.cs +++ b/SimNextgenApp.Demo/Program.cs @@ -423,6 +423,123 @@ ); }, seriesOption, azureSizeOption, azureDbBurstDurationOption, azureInitialCreditsOption, azureGrafanaOption); +// ---- Demo: azure-pgsql-pooling ---- +var azurePgsqlPoolingCommand = new Command("azure-pgsql-pooling", "Compare PostgreSQL connection pooling strategies on Azure B-series"); + +var poolModeOption = new Option( + name: "--mode", + description: "Pooling mode: direct, session, transaction.", + getDefaultValue: () => "direct" +); + +var poolSizeOption = new Option( + name: "--pool-size", + description: "Connection pool size (ignored for direct mode).", + getDefaultValue: () => 20 +); + +var poolingSeriesOption = new Option( + name: "--series", + description: "The Azure instance series. Currently supported: B (Burstable).", + getDefaultValue: () => "B" +); + +var poolingSizeOption = new Option( + name: "--size", + description: "The Azure instance size. B-series: 1ms, 2s, 2ms, 4ms, 8ms.", + getDefaultValue: () => "2ms" +); + +var poolingDurationOption = new Option( + name: "--duration", + description: "Total run duration in seconds.", + getDefaultValue: () => 300.0 +); + +var poolingInitialCreditsOption = new Option( + name: "--initial-credits", + description: "Initial CPU credits for the burstable instance.", + getDefaultValue: () => 60.0 +); + +var poolingGrafanaOption = new Option( + name: "--grafana", + description: "Enable OpenTelemetry export to Grafana Cloud (requires API key configuration).", + getDefaultValue: () => false +); + +azurePgsqlPoolingCommand.AddOption(poolModeOption); +azurePgsqlPoolingCommand.AddOption(poolSizeOption); +azurePgsqlPoolingCommand.AddOption(poolingSeriesOption); +azurePgsqlPoolingCommand.AddOption(poolingSizeOption); +azurePgsqlPoolingCommand.AddOption(poolingDurationOption); +azurePgsqlPoolingCommand.AddOption(poolingInitialCreditsOption); +azurePgsqlPoolingCommand.AddOption(poolingGrafanaOption); + +azurePgsqlPoolingCommand.SetHandler((string mode, int poolSize, string series, string size, double duration, double initialCredits, bool enableGrafana) => +{ + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + // Create a logger factory that uses Serilog + loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); + + // Parse pooling mode with user-friendly names + PoolingMode poolMode; + try + { + poolMode = mode.ToLowerInvariant() switch + { + "direct" => PoolingMode.Direct, + "session" => PoolingMode.SessionPooling, + "transaction" => PoolingMode.TransactionPooling, + _ => throw new ArgumentException($"Invalid pooling mode '{mode}'. Valid options: direct, session, transaction") + }; + } + catch (ArgumentException ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return; + } + + // Get Azure DB spec + AzureDbInstanceSpec spec; + try + { + spec = AzureDbRegistry.GetSpec(series, size); + } + catch (ArgumentException ex) + { + Console.WriteLine($"Error: {ex.Message}"); + Console.WriteLine(); + Console.WriteLine("Currently supported Azure Database instances:"); + Console.WriteLine(" B-series (Burstable): B.1ms, B.2s, B.2ms, B.4ms, B.8ms"); + return; + } + + var dbBehavior = new AzureDbBehavior(spec, initialCredits); + + Console.WriteLine($"====== Running Azure PostgreSQL Pooling Demo ======"); + Console.WriteLine($"Instance: {series}.{size}"); + Console.WriteLine($"Pooling Mode: {poolMode}"); + Console.WriteLine($"Pool Size: {(poolMode == PoolingMode.Direct ? "N/A (Direct)" : poolSize.ToString())}"); + Console.WriteLine($"Initial Credits: {initialCredits}"); + Console.WriteLine($"Duration: {duration} seconds"); + + AzurePgsqlPoolingScenario.RunDemo( + loggerFactory, + duration, + dbBehavior, + poolMode, + poolSize, + genSeed: 1234, + enableGrafana: enableGrafana + ); +}, poolModeOption, poolSizeOption, poolingSeriesOption, poolingSizeOption, poolingDurationOption, poolingInitialCreditsOption, poolingGrafanaOption); + // ---- Group commands ---- var demoCommand = new Command("demo", "Run a simulation demo"); demoCommand.AddCommand(simpleGenCommand); @@ -431,6 +548,7 @@ demoCommand.AddCommand(simpleRestaurantCommand); demoCommand.AddCommand(awsRdsBurstCommand); demoCommand.AddCommand(azureDbBurstCommand); +demoCommand.AddCommand(azurePgsqlPoolingCommand); rootCommand.AddCommand(demoCommand); diff --git a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs new file mode 100644 index 0000000..83e5f86 --- /dev/null +++ b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs @@ -0,0 +1,275 @@ +using Microsoft.Extensions.Logging; +using SimNextgenApp.Configurations; +using SimNextgenApp.Core; +using SimNextgenApp.Core.Strategies; +using SimNextgenApp.Core.Utilities; +using SimNextgenApp.Demo.AzureDbSample; +using SimNextgenApp.Demo.CustomModels; +using SimNextgenApp.Observability; +using SimNextgenApp.Observability.Exporters; + +namespace SimNextgenApp.Demo.Scenarios; + +/// +/// Simulates PostgreSQL connection pooling strategies on Azure B-series instances. +/// Compares Direct, Session, and Transaction pooling modes. +/// +internal static class AzurePgsqlPoolingScenario +{ + public static void RunDemo( + ILoggerFactory loggerFactory, + double runDuration, + AzureDbBehavior dbBehavior, + PoolingMode poolMode, + int poolSize, + int genSeed, + bool enableGrafana = false) + { + SimulationTelemetry? telemetry = null; + ILoggerFactory activeLoggerFactory = loggerFactory; + + if (enableGrafana) + { + var grafanaApiKey = Environment.GetEnvironmentVariable("GRAFANA_API_KEY"); + var grafanaRegion = Environment.GetEnvironmentVariable("GRAFANA_REGION") ?? "us-central-0"; + + if (string.IsNullOrWhiteSpace(grafanaApiKey)) + { + var tempLogger = loggerFactory.CreateLogger("Azure-Pgsql-Pooling"); + tempLogger.LogError("GRAFANA_API_KEY environment variable not set!"); + tempLogger.LogWarning("Continuing simulation without Grafana export..."); + } + else + { + try + { + telemetry = SimulationTelemetry.Create() + .WithServiceInfo("Azure-Pgsql-Pooling", "1.0.0") + .WithOtlpExporter( + OtlpBackend.GrafanaCloud, + apiKey: grafanaApiKey, + region: grafanaRegion + ) + .WithLogging(includeConsoleExporter: false, includeOtlpExporter: true) + .Build(); + + // Connect the database behavior to emit metrics + dbBehavior.SetMeter(telemetry.Meter); + + // Use the OpenTelemetry-configured logger factory + if (telemetry.LoggerFactory != null) + { + activeLoggerFactory = telemetry.LoggerFactory; + } + } + catch (Exception ex) + { + var tempLogger = loggerFactory.CreateLogger("Azure-Pgsql-Pooling"); + tempLogger.LogError(ex, "Failed to configure Grafana Cloud export"); + tempLogger.LogWarning("Continuing simulation without Grafana export..."); + } + } + } + + // Create two separate loggers for proper lifecycle management + var programLogger = activeLoggerFactory.CreateLogger("Azure-Pgsql-Pooling"); + var cleanupLogger = loggerFactory.CreateLogger("Azure-Pgsql-Pooling-Cleanup"); + + programLogger.LogInformation("--- Preparing Azure PostgreSQL Pooling Simulation ---"); + programLogger.LogInformation($"Pooling Mode: {poolMode}"); + programLogger.LogInformation($"Pool Size: {(poolMode == PoolingMode.Direct ? "N/A (Direct)" : poolSize.ToString())}"); + + if (telemetry != null) + { + programLogger.LogInformation("Grafana Cloud OpenTelemetry export enabled!"); + } + + // Create connection pool (if not direct mode) + ConnectionPool? pool = poolMode != PoolingMode.Direct ? new ConnectionPool(poolSize, poolMode) : null; + + // Configure Generator with PostgreSQL query creation logic + // High traffic: 20 req/sec (0.05s inter-arrival) to test connection overhead impact + Func interArrivalFunc = (rnd) => + TimeSpan.FromSeconds(-0.05 * Math.Log(1.0 - rnd.NextDouble())); + + Func createLoad = (rnd) => + { + var query = new PostgresQuery + { + PoolMode = poolMode + }; + + if (poolMode == PoolingMode.Direct) + { + // Direct mode: Always new connection + query.IsNewConnection = true; + query.ConnectionId = Guid.NewGuid().ToString(); + } + else + { + // Pool mode: Try to acquire connection from pool + // Use the load's Id (not random Guid) so we can release it later + var connId = pool!.AcquireConnection(query.Id.ToString()); + query.IsNewConnection = (connId == null); // New if pool exhausted + query.ConnectionId = connId ?? Guid.NewGuid().ToString(); + } + + return query; + }; + + var generatorConfig = new GeneratorStaticConfig( + interArrivalFunc, + createLoad + ); + + // Configure Server (using PostgreSQL-aware AzureDbBehavior) + var serverConfig = new ServerStaticConfig(dbBehavior.GetServiceTime) + { + Capacity = 1 + }; + + var queueConfig = new QueueStaticConfig(); + + // Create Model + var model = new SimpleMmckModel( + generatorConfig, + genSeed, + queueConfig, + serverConfig, + numberOfServers: dbBehavior.Spec.VCores, + serverSeedBase: 100, + systemCapacityK: 50, + activeLoggerFactory + ); + + // Create Engine with time unit validation + var timeUnit = SimulationTimeUnit.Seconds; + + long runDurationInUnits = TimeUnitConverter.ConvertToSimulationUnits( + TimeSpan.FromSeconds(runDuration), + timeUnit + ); + + var profile = new SimulationProfile( + model, + new DurationRunStrategy(runDurationInUnits, null), + "Azure PostgreSQL Pooling Simulation", + timeUnit, + activeLoggerFactory + ); + + var engine = new SimulationEngine(profile); + + // Validate TimeUnit Precision + programLogger.LogInformation("\n--- Validating TimeUnit Precision ---"); + var validation = SimulationProfileValidator.ValidateTimeUnit( + timeUnit, + new Dictionary> + { + ["Inter-arrival time"] = interArrivalFunc + }, + sampleSize: 1000, + truncationThreshold: 0.05 + ); + + SimulationProfileValidator.LogValidationResult(validation, programLogger); + + if (!validation.IsValid) + { + programLogger.LogWarning($"TIP: Auto-switching from {timeUnit} to {validation.RecommendedUnit} to prevent precision loss."); + + // Re-create profile with recommended unit + timeUnit = validation.RecommendedUnit; + runDurationInUnits = TimeUnitConverter.ConvertToSimulationUnits( + TimeSpan.FromSeconds(runDuration), + timeUnit + ); + + profile = new SimulationProfile( + model, + new DurationRunStrategy(runDurationInUnits, null), + "Azure PostgreSQL Pooling Simulation", + timeUnit, + activeLoggerFactory + ); + + engine = new SimulationEngine(profile); + } + + // Connect physics to engine + dbBehavior.SetContext(engine); + + // Release connections back to pool when service completes + if (pool != null && poolMode != PoolingMode.Direct) + { + foreach (var server in model.ServiceChannels) + { + server.LoadDeparted += (load, departureTime) => + { + if (load is PostgresQuery query) + { + // Release connection back to the pool + pool.ReleaseConnection(query.Id.ToString()); + } + }; + } + } + + programLogger.LogInformation("Starting Simulation. Watch console for CSV output..."); + + // Observe the simulation if telemetry is enabled + SimulationObserver? simObserver = null; + if (telemetry != null) + { + simObserver = telemetry.ObserveSimulation(engine); + } + + try + { + engine.Run(); + } + finally + { + // Flush telemetry data + if (telemetry != null) + { + programLogger.LogInformation("Flushing metrics to Grafana Cloud..."); + + try + { + programLogger.LogInformation("Calling Flush() with 5s timeout..."); + bool flushSuccess = telemetry.Flush(5000); + + if (flushSuccess) + { + programLogger.LogInformation("Flush completed successfully"); + } + else + { + programLogger.LogWarning("Flush timed out before all telemetry was exported"); + } + } + catch (Exception ex) + { + programLogger.LogError(ex, "FLUSH FAILED"); + } + + try + { + simObserver?.Dispose(); + telemetry.Dispose(); + cleanupLogger.LogInformation("Telemetry disposed successfully"); + } + catch (Exception ex) + { + cleanupLogger.LogError(ex, "Telemetry disposal failed"); + } + } + + dbBehavior.FinalizeExport("output"); + + cleanupLogger.LogInformation("Simulation Complete."); + cleanupLogger.LogInformation($"Check 'output/' directory for CSV results."); + } + } +} From aba254723be319694379fac32df32e6f6410d7d2 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 15:45:58 +0800 Subject: [PATCH 02/32] fix(AzureDbBehavior): adjust PostgreSQL connection overhead handling and update throttling logic --- .../AzureDbSample/AzureDbBehavior.cs | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs index a4176fa..730025e 100644 --- a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs +++ b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs @@ -106,43 +106,31 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) _lastUpdateTimeInSimUnits = currentTimeInSimUnits; } - // 2. Burn Logic (Look Ahead) - double estimatedBurstCost = spec.FastSecs * BurnRatePerSec; - bool isThrottled = IsBurstable && Credits < estimatedBurstCost; - - // 3. Determine Service Time (with PostgreSQL connection overhead if applicable) - double baseTime; + // 2. Determine PostgreSQL overhead (if applicable) BEFORE throttling decision + double connectionOverhead = 0.0; if (load is PostgresQuery query) { - // Get base execution time (burst or throttled) - double executionTime = isThrottled ? spec.SlowSecs : spec.FastSecs; - if (query.IsNewConnection) { - // Full connection setup overhead (direct or pool miss) - baseTime = executionTime + ConnectionOverheadSecs; + connectionOverhead = ConnectionOverheadSecs; // 50ms for new connection } else if (query.PoolMode == PoolingMode.TransactionPooling) { - // Transaction pooling: connection state reset overhead - // (DISCARD ALL, temp table cleanup, session var reset) - baseTime = executionTime + TransactionResetOverheadSecs; - } - else - { - // Session pooling: no overhead (connection reused as-is) - baseTime = executionTime; + connectionOverhead = TransactionResetOverheadSecs; // 8ms for DISCARD ALL } + // Session pooling: no overhead } - else - { - // Fallback for non-PostgresQuery loads - baseTime = isThrottled ? spec.SlowSecs : spec.FastSecs; - } + + // 3. Burn Logic (Look Ahead) - include overhead in throttling decision + double estimatedBurstCost = (spec.FastSecs + connectionOverhead) * BurnRatePerSec; + bool isThrottled = IsBurstable && Credits < estimatedBurstCost; + + // 4. Determine Service Time + double baseTime = (isThrottled ? spec.SlowSecs : spec.FastSecs) + connectionOverhead; double actualDuration = -baseTime * Math.Log(1.0 - rnd.NextDouble()); - // 4. Pay the Bill + // 5. Pay the Bill // Azure: No unlimited mode - hard throttle when credits depleted if (IsBurstable) { @@ -160,7 +148,7 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) } } - // 5. Export Data (CSV) + // 6. Export Data (CSV) // Convert current simulation time to seconds for CSV export double nowInSeconds = TimeUnitConverter.ConvertFromSimulationUnits( currentTimeInSimUnits, From 04991c667830a5acf649768b082a2ee1d6e4d919 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 15:46:13 +0800 Subject: [PATCH 03/32] refactor(ConnectionPool): simplify constructor by removing pooling mode parameter --- SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs | 11 +++++++---- .../Scenarios/AzurePgsqlPoolingScenario.cs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs b/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs index 1580e26..6ce3814 100644 --- a/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs +++ b/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs @@ -2,19 +2,17 @@ namespace SimNextgenApp.Demo.AzureDbSample; /// /// Simulates a PgBouncer-style connection pool for PostgreSQL. -/// Manages connection lifecycle based on pooling mode. +/// Manages connection lifecycle for pooling modes. /// internal class ConnectionPool { private readonly int _poolSize; - private readonly PoolingMode _mode; private readonly HashSet _availableConnections; private readonly Dictionary _assignedConnections; // Query → Connection - public ConnectionPool(int poolSize, PoolingMode mode) + public ConnectionPool(int poolSize) { _poolSize = poolSize; - _mode = mode; _availableConnections = new HashSet(); _assignedConnections = new Dictionary(); @@ -57,6 +55,11 @@ public void ReleaseConnection(string queryId) } } + /// + /// Total capacity of the connection pool. + /// + public int Capacity => _poolSize; + /// /// Number of available connections in the pool. /// diff --git a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs index 83e5f86..bcdac1f 100644 --- a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs +++ b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs @@ -85,7 +85,7 @@ public static void RunDemo( } // Create connection pool (if not direct mode) - ConnectionPool? pool = poolMode != PoolingMode.Direct ? new ConnectionPool(poolSize, poolMode) : null; + ConnectionPool? pool = poolMode != PoolingMode.Direct ? new ConnectionPool(poolSize) : null; // Configure Generator with PostgreSQL query creation logic // High traffic: 20 req/sec (0.05s inter-arrival) to test connection overhead impact From 0a282e61acd60cf82fdf6b950ec5a004b6f52ac2 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 16:22:41 +0800 Subject: [PATCH 04/32] feat(AzureDbBehavior): implement deferred connection acquisition for PostgreSQL queries --- .../AzureDbSample/AzureDbBehavior.cs | 45 +++++++++++++++---- .../AzureDbSample/ConnectionPool.cs | 14 ++++-- .../AzureDbSample/PostgresQuery.cs | 3 +- .../Scenarios/AzurePgsqlPoolingScenario.cs | 25 ++++------- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs index 730025e..2f1bfee 100644 --- a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs +++ b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs @@ -12,6 +12,7 @@ namespace SimNextgenApp.Demo.AzureDbSample; internal class AzureDbBehavior(AzureDbInstanceSpec spec, double initialCredits = 10.0) { private IRunContext? _engineContext; + private ConnectionPool? _connectionPool; // Thread-safe storage for cross-thread access by OpenTelemetry's background metric collection // Using volatile read/write pattern via Volatile.Read() / Volatile.Write() @@ -56,6 +57,15 @@ public void SetContext(IRunContext context) _lastUpdateTimeInSimUnits = context.ClockTime; } + /// + /// Sets the connection pool for deferred connection acquisition. + /// Must be called before simulation starts if using pooling mode. + /// + public void SetConnectionPool(ConnectionPool? pool) + { + _connectionPool = pool; + } + /// /// Sets up OpenTelemetry metrics for exporting to Grafana Cloud or other OTLP backends. /// @@ -106,31 +116,50 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) _lastUpdateTimeInSimUnits = currentTimeInSimUnits; } - // 2. Determine PostgreSQL overhead (if applicable) BEFORE throttling decision + // 2. DEFERRED CONNECTION ACQUISITION (if applicable) + // Acquire connection from pool NOW (when service starts), not at load creation double connectionOverhead = 0.0; - if (load is PostgresQuery query) + if (load is PostgresQuery query && string.IsNullOrEmpty(query.ConnectionId)) + { + // Connection not yet assigned - acquire from pool now + if (_connectionPool != null && query.PoolMode != PoolingMode.Direct) + { + var connId = _connectionPool.AcquireConnection(query.Id.ToString()); + query.IsNewConnection = connId == null; // New if pool exhausted + query.ConnectionId = connId ?? Guid.NewGuid().ToString(); + } + else + { + // Direct mode or no pool configured + query.IsNewConnection = true; + query.ConnectionId = Guid.NewGuid().ToString(); + } + } + + // 3. Determine PostgreSQL overhead based on connection type + if (load is PostgresQuery q) { - if (query.IsNewConnection) + if (q.IsNewConnection) { connectionOverhead = ConnectionOverheadSecs; // 50ms for new connection } - else if (query.PoolMode == PoolingMode.TransactionPooling) + else if (q.PoolMode == PoolingMode.TransactionPooling) { connectionOverhead = TransactionResetOverheadSecs; // 8ms for DISCARD ALL } // Session pooling: no overhead } - // 3. Burn Logic (Look Ahead) - include overhead in throttling decision + // 4. Burn Logic (Look Ahead) - include overhead in throttling decision double estimatedBurstCost = (spec.FastSecs + connectionOverhead) * BurnRatePerSec; bool isThrottled = IsBurstable && Credits < estimatedBurstCost; - // 4. Determine Service Time + // 5. Determine Service Time double baseTime = (isThrottled ? spec.SlowSecs : spec.FastSecs) + connectionOverhead; double actualDuration = -baseTime * Math.Log(1.0 - rnd.NextDouble()); - // 5. Pay the Bill + // 6. Pay the Bill // Azure: No unlimited mode - hard throttle when credits depleted if (IsBurstable) { @@ -148,7 +177,7 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) } } - // 6. Export Data (CSV) + // 7. Export Data (CSV) // Convert current simulation time to seconds for CSV export double nowInSeconds = TimeUnitConverter.ConvertFromSimulationUnits( currentTimeInSimUnits, diff --git a/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs b/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs index 6ce3814..37f7bf8 100644 --- a/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs +++ b/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs @@ -1,8 +1,9 @@ namespace SimNextgenApp.Demo.AzureDbSample; /// -/// Simulates a PgBouncer-style connection pool for PostgreSQL. -/// Manages connection lifecycle for pooling modes. +/// Simulates a PgBouncer-style connection pool for PostgreSQL with HARD LIMIT semantics. +/// Connection acquisition happens when server starts processing (deferred acquisition), +/// matching real PgBouncer behavior where requests queue for available connections. /// internal class ConnectionPool { @@ -26,12 +27,19 @@ public ConnectionPool(int poolSize) /// /// Attempts to acquire a connection from the pool. /// Returns connection ID if successful, null if pool is exhausted. + /// + /// HARD LIMIT: When pool is exhausted (returns null), caller must open + /// a new connection with overhead penalty. This matches PgBouncer behavior + /// when max_client_conn is reached. + /// + /// Called at SERVICE START (deferred acquisition), not at load creation. + /// Requests naturally queue in SimQueue before reaching this point. /// public string? AcquireConnection(string queryId) { if (_availableConnections.Count == 0) { - // Pool exhausted - query must wait + // Pool exhausted - caller must open new connection (with overhead) return null; } diff --git a/SimNextgenApp.Demo/AzureDbSample/PostgresQuery.cs b/SimNextgenApp.Demo/AzureDbSample/PostgresQuery.cs index 48158ed..cf07d44 100644 --- a/SimNextgenApp.Demo/AzureDbSample/PostgresQuery.cs +++ b/SimNextgenApp.Demo/AzureDbSample/PostgresQuery.cs @@ -16,8 +16,9 @@ internal class PostgresQuery : MyLoad /// /// The connection ID assigned to this query (for tracking pool usage). + /// Null when using deferred acquisition (assigned at service start). /// - public string ConnectionId { get; set; } = string.Empty; + public string? ConnectionId { get; set; } /// /// The pooling mode used for this query. diff --git a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs index bcdac1f..45eb7dc 100644 --- a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs +++ b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs @@ -94,26 +94,16 @@ public static void RunDemo( Func createLoad = (rnd) => { + // DEFERRED ACQUISITION: Connection will be acquired when service starts, + // not at load creation. This matches real PgBouncer behavior where + // requests queue for connections. var query = new PostgresQuery { - PoolMode = poolMode + PoolMode = poolMode, + ConnectionId = null, // ← Not assigned yet (deferred) + IsNewConnection = false // ← Will be determined at service start }; - if (poolMode == PoolingMode.Direct) - { - // Direct mode: Always new connection - query.IsNewConnection = true; - query.ConnectionId = Guid.NewGuid().ToString(); - } - else - { - // Pool mode: Try to acquire connection from pool - // Use the load's Id (not random Guid) so we can release it later - var connId = pool!.AcquireConnection(query.Id.ToString()); - query.IsNewConnection = (connId == null); // New if pool exhausted - query.ConnectionId = connId ?? Guid.NewGuid().ToString(); - } - return query; }; @@ -199,6 +189,9 @@ public static void RunDemo( // Connect physics to engine dbBehavior.SetContext(engine); + // Set connection pool for deferred acquisition (if using pooling mode) + dbBehavior.SetConnectionPool(pool); + // Release connections back to pool when service completes if (pool != null && poolMode != PoolingMode.Direct) { From 6350fb2edf8d028acf71ded73dedf1333b3edc6a Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 18:46:08 +0800 Subject: [PATCH 05/32] fix(ConnectionPool): validate pool size for non-direct pooling modes to prevent connection acquisition failures --- SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs | 5 +++++ SimNextgenApp.Demo/Program.cs | 6 ++++++ SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs b/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs index 37f7bf8..acd9da6 100644 --- a/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs +++ b/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs @@ -13,6 +13,11 @@ internal class ConnectionPool public ConnectionPool(int poolSize) { + if (poolSize <= 0) + { + throw new ArgumentException($"Pool size must be positive (got {poolSize}). Use Direct mode if you don't want pooling.", nameof(poolSize)); + } + _poolSize = poolSize; _availableConnections = new HashSet(); _assignedConnections = new Dictionary(); diff --git a/SimNextgenApp.Demo/Program.cs b/SimNextgenApp.Demo/Program.cs index 4eeedd2..017f27a 100644 --- a/SimNextgenApp.Demo/Program.cs +++ b/SimNextgenApp.Demo/Program.cs @@ -498,6 +498,12 @@ "transaction" => PoolingMode.TransactionPooling, _ => throw new ArgumentException($"Invalid pooling mode '{mode}'. Valid options: direct, session, transaction") }; + + // Validate pool size for pooling modes + if (poolMode != PoolingMode.Direct && poolSize <= 0) + { + throw new ArgumentException($"Pool size must be positive for {poolMode} mode (got {poolSize}). Use --mode direct if you don't want pooling."); + } } catch (ArgumentException ex) { diff --git a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs index 45eb7dc..6c179a1 100644 --- a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs +++ b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs @@ -84,6 +84,12 @@ public static void RunDemo( programLogger.LogInformation("Grafana Cloud OpenTelemetry export enabled!"); } + // Validate pool size for pooling modes + if (poolMode != PoolingMode.Direct && poolSize <= 0) + { + throw new ArgumentException($"Pool size must be positive for {poolMode} mode (got {poolSize}). This would cause all requests to fail connection acquisition.", nameof(poolSize)); + } + // Create connection pool (if not direct mode) ConnectionPool? pool = poolMode != PoolingMode.Direct ? new ConnectionPool(poolSize) : null; From 02c27761baa420cdb8253b7d787883ad0512102f Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 19:05:46 +0800 Subject: [PATCH 06/32] feat(AzurePgsqlPoolingScenario): implement random session hold time for session pooling connections --- .../Scenarios/AzurePgsqlPoolingScenario.cs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs index 6c179a1..3cd1865 100644 --- a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs +++ b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs @@ -201,14 +201,37 @@ public static void RunDemo( // Release connections back to pool when service completes if (pool != null && poolMode != PoolingMode.Direct) { + // Random number generator for session hold time + var holdTimeRandom = new Random(genSeed + 999); + foreach (var server in model.ServiceChannels) { server.LoadDeparted += (load, departureTime) => { if (load is PostgresQuery query) { - // Release connection back to the pool - pool.ReleaseConnection(query.Id.ToString()); + if (poolMode == PoolingMode.SessionPooling) + { + // SESSION POOLING: Hold connection for random time (simulates client session) + // Client might run more queries on the same connection before releasing + // Mean hold time: 100ms (typical think time between queries in a session) + double holdTimeSecs = -0.1 * Math.Log(1.0 - holdTimeRandom.NextDouble()); + long holdTimeUnits = TimeUnitConverter.ConvertToSimulationUnits( + TimeSpan.FromSeconds(holdTimeSecs), + engine.TimeUnit + ); + + // Schedule delayed release event + long releaseTime = departureTime + holdTimeUnits; + var releaseEvent = new ConnectionReleaseEvent(pool, query.Id.ToString()); + engine.Schedule(releaseEvent, releaseTime); + } + else + { + // TRANSACTION POOLING: Release immediately + // Connection returned to pool right away for next transaction + pool.ReleaseConnection(query.Id.ToString()); + } } }; } From a4a9c7e72136a1b2c703acc13132427c5567ab1a Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 19:13:15 +0800 Subject: [PATCH 07/32] feat(AzureDbBehavior): implement simulated connection wait time for PostgreSQL pooling --- .../AzureDbSample/AzureDbBehavior.cs | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs index 2f1bfee..9ac1aec 100644 --- a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs +++ b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs @@ -118,25 +118,51 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) // 2. DEFERRED CONNECTION ACQUISITION (if applicable) // Acquire connection from pool NOW (when service starts), not at load creation - double connectionOverhead = 0.0; + double poolWaitTime = 0.0; if (load is PostgresQuery query && string.IsNullOrEmpty(query.ConnectionId)) { // Connection not yet assigned - acquire from pool now if (_connectionPool != null && query.PoolMode != PoolingMode.Direct) { var connId = _connectionPool.AcquireConnection(query.Id.ToString()); - query.IsNewConnection = connId == null; // New if pool exhausted - query.ConnectionId = connId ?? Guid.NewGuid().ToString(); + + if (connId != null) + { + // SUCCESS: Connection acquired from pool + query.IsNewConnection = false; + query.ConnectionId = connId; + } + else + { + // POOL EXHAUSTED: Request must WAIT for connection to become available + // This models PgBouncer behavior where clients queue when pool is full + + // Estimate wait time based on pooling mode: + // - Session pooling: Wait for session hold time (~100ms average) + // - Transaction pooling: Wait for query completion (~100ms average) + // Use exponential distribution for realistic variability + double meanWaitSecs = 0.100; // 100ms average wait + poolWaitTime = -meanWaitSecs * Math.Log(1.0 - rnd.NextDouble()); + + // After waiting, acquire connection (now guaranteed to succeed in this model) + // In reality, PgBouncer would wake up waiting client when connection freed + query.IsNewConnection = false; + query.ConnectionId = $"conn_waited_{Guid.NewGuid()}"; + + // NOTE: Connection is NOT actually in pool (we're simulating the wait) + // The release event will be no-op for this connection ID + } } else { - // Direct mode or no pool configured + // Direct mode: Always create new connection (no pool) query.IsNewConnection = true; query.ConnectionId = Guid.NewGuid().ToString(); } } // 3. Determine PostgreSQL overhead based on connection type + double connectionOverhead = 0.0; if (load is PostgresQuery q) { if (q.IsNewConnection) @@ -154,8 +180,8 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) double estimatedBurstCost = (spec.FastSecs + connectionOverhead) * BurnRatePerSec; bool isThrottled = IsBurstable && Credits < estimatedBurstCost; - // 5. Determine Service Time - double baseTime = (isThrottled ? spec.SlowSecs : spec.FastSecs) + connectionOverhead; + // 5. Determine Service Time (execution + overhead + pool wait time) + double baseTime = (isThrottled ? spec.SlowSecs : spec.FastSecs) + connectionOverhead + poolWaitTime; double actualDuration = -baseTime * Math.Log(1.0 - rnd.NextDouble()); From 7238f53c09273dbcf0f6c1228c7af29352e3b688 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 19:19:38 +0800 Subject: [PATCH 08/32] feat(AzurePgsqlPoolingScenario): add validation for positive pool size in command options --- SimNextgenApp.Demo/Program.cs | 14 ++++++++------ .../Scenarios/AzurePgsqlPoolingScenario.cs | 6 ------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/SimNextgenApp.Demo/Program.cs b/SimNextgenApp.Demo/Program.cs index 017f27a..fa61e3f 100644 --- a/SimNextgenApp.Demo/Program.cs +++ b/SimNextgenApp.Demo/Program.cs @@ -438,6 +438,14 @@ getDefaultValue: () => 20 ); +poolSizeOption.AddValidator(result => +{ + if (result.GetValueOrDefault() <= 0) + { + result.ErrorMessage = "Pool size must be positive (e.g., --pool-size 20). A pool size of 0 or negative would cause all requests to fail."; + } +}); + var poolingSeriesOption = new Option( name: "--series", description: "The Azure instance series. Currently supported: B (Burstable).", @@ -498,12 +506,6 @@ "transaction" => PoolingMode.TransactionPooling, _ => throw new ArgumentException($"Invalid pooling mode '{mode}'. Valid options: direct, session, transaction") }; - - // Validate pool size for pooling modes - if (poolMode != PoolingMode.Direct && poolSize <= 0) - { - throw new ArgumentException($"Pool size must be positive for {poolMode} mode (got {poolSize}). Use --mode direct if you don't want pooling."); - } } catch (ArgumentException ex) { diff --git a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs index 3cd1865..f152bae 100644 --- a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs +++ b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs @@ -84,12 +84,6 @@ public static void RunDemo( programLogger.LogInformation("Grafana Cloud OpenTelemetry export enabled!"); } - // Validate pool size for pooling modes - if (poolMode != PoolingMode.Direct && poolSize <= 0) - { - throw new ArgumentException($"Pool size must be positive for {poolMode} mode (got {poolSize}). This would cause all requests to fail connection acquisition.", nameof(poolSize)); - } - // Create connection pool (if not direct mode) ConnectionPool? pool = poolMode != PoolingMode.Direct ? new ConnectionPool(poolSize) : null; From 3214c02fbf0facd42f6624c8feaf421d69f5880e Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 19:26:29 +0800 Subject: [PATCH 09/32] feat(pooling-scripts): enhance precision in latency calculations using awk --- .../AzureDbSample/pool-size-comparison.sh | 15 ++++++++++----- .../AzureDbSample/pooling-comparison.sh | 7 +++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh index d5679fb..4a389e1 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Exit on error, undefined variables, and pipe failures +set -euo pipefail + # SYNOPSIS # Compare PostgreSQL connection pool performance across different pool sizes. # DESCRIPTION @@ -193,8 +196,8 @@ else # Generate zoomed bar chart (emphasizes differences) MIN_LATENCY=$(awk -F, 'NR>1 {if(NR==2 || $21 {if(NR==2 || $2>max) max=$2} END {printf "%.0f", max}' "./output/pool_size_comparison/latency_summary.csv") - RANGE_MIN=$(echo "$MIN_LATENCY - 5" | bc) - RANGE_MAX=$(echo "$MAX_LATENCY + 5" | bc) + RANGE_MIN=$(awk "BEGIN {printf \"%.0f\", $MIN_LATENCY - 5}") + RANGE_MAX=$(awk "BEGIN {printf \"%.0f\", $MAX_LATENCY + 5}") graph "./output/pool_size_comparison/latency_summary.csv" \ --bar \ @@ -262,12 +265,14 @@ LAST_AVG="" for POOL_SIZE in "${POOL_SIZES[@]}"; do CURRENT_AVG=$(awk -F, -v size="$POOL_SIZE" 'NR>1 && $1==size {print $2}' "./output/pool_size_comparison/latency_summary.csv") if [ -n "$LAST_AVG" ]; then - IMPROVEMENT=$(echo "scale=2; ($LAST_AVG - $CURRENT_AVG) / $LAST_AVG * 100" | bc) - IS_NEGATIVE=$(echo "$IMPROVEMENT < 0" | bc) + # Calculate improvement percentage using awk (no bc dependency) + IMPROVEMENT=$(awk "BEGIN {printf \"%.2f\", ($LAST_AVG - $CURRENT_AVG) / $LAST_AVG * 100}") + IS_NEGATIVE=$(awk "BEGIN {print ($IMPROVEMENT < 0) ? 1 : 0}") + if [ "$IS_NEGATIVE" -eq 1 ]; then echo -e " • ${YELLOW}Pool size ${POOL_SIZE}: Performance degraded (${IMPROVEMENT}% worse)${NC}" else - IS_SMALL=$(echo "$IMPROVEMENT < 5" | bc) + IS_SMALL=$(awk "BEGIN {print ($IMPROVEMENT < 5) ? 1 : 0}") if [ "$IS_SMALL" -eq 1 ]; then echo -e " • ${YELLOW}Pool size ${POOL_SIZE}: Diminishing returns (<5% improvement)${NC}" fi diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh index 5d713f3..185e6de 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Exit on error, undefined variables, and pipe failures +set -euo pipefail + # SYNOPSIS # Run PostgreSQL connection pooling comparison across all three modes. # DESCRIPTION @@ -249,8 +252,8 @@ EOF # Generate zoomed bar chart (emphasizes differences) MIN_LATENCY=$(awk -F, 'NR>1 {if(NR==2 || $21 {if(NR==2 || $2>max) max=$2} END {printf "%.0f", max}' "./output/pooling_comparison/latency_summary.csv") - RANGE_MIN=$(echo "$MIN_LATENCY - 0.1" | bc) - RANGE_MAX=$(echo "$MAX_LATENCY + 0.1" | bc) + RANGE_MIN=$(awk "BEGIN {printf \"%.1f\", $MIN_LATENCY - 0.1}") + RANGE_MAX=$(awk "BEGIN {printf \"%.1f\", $MAX_LATENCY + 0.1}") graph "./output/pooling_comparison/latency_summary.csv" \ --bar \ From b9e3c82ccdc5a61317fc65b84acbfa9da52d0a2b Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 19:46:59 +0800 Subject: [PATCH 10/32] feat(ConnectionReleaseEvent): add event for delayed connection release in session pooling mode --- .../AzureDbSample/ConnectionReleaseEvent.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 SimNextgenApp.Demo/AzureDbSample/ConnectionReleaseEvent.cs diff --git a/SimNextgenApp.Demo/AzureDbSample/ConnectionReleaseEvent.cs b/SimNextgenApp.Demo/AzureDbSample/ConnectionReleaseEvent.cs new file mode 100644 index 0000000..0c2e791 --- /dev/null +++ b/SimNextgenApp.Demo/AzureDbSample/ConnectionReleaseEvent.cs @@ -0,0 +1,26 @@ +using SimNextgenApp.Core; +using SimNextgenApp.Events; + +namespace SimNextgenApp.Demo.AzureDbSample; + +/// +/// Event for delayed connection release in session pooling mode. +/// Simulates client holding connection between queries in a session. +/// +internal class ConnectionReleaseEvent : AbstractEvent +{ + private readonly ConnectionPool _pool; + private readonly string _queryId; + + public ConnectionReleaseEvent(ConnectionPool pool, string queryId) + { + _pool = pool; + _queryId = queryId; + } + + public override void Execute(IRunContext context) + { + // Release connection back to pool after session hold time + _pool.ReleaseConnection(_queryId); + } +} From 922e2e7f3764379f38c6742017a2602e96389913 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 19:47:08 +0800 Subject: [PATCH 11/32] fix(pool-size-comparison): handle errors during pool size execution and exit gracefully --- SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 index 6ed0c51..8df1188 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 @@ -109,7 +109,11 @@ function Run-PoolSize { # Run all pool sizes foreach ($PoolSize in $PoolSizes) { - Run-PoolSize -PoolSize $PoolSize + $result = Run-PoolSize -PoolSize $PoolSize + if (-not $result) { + Write-ColorOutput "Exiting due to error" "Red" + exit 1 + } } # Generate summary statistics From af01fa173b888935569ceed1fc3c9a4edd9235e9 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 20:04:59 +0800 Subject: [PATCH 12/32] feat(AzureDbBehavior): update connection acquisition logic to bypass pool when exhausted --- .../AzureDbSample/AzureDbBehavior.cs | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs index 9ac1aec..5f458e2 100644 --- a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs +++ b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs @@ -118,7 +118,6 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) // 2. DEFERRED CONNECTION ACQUISITION (if applicable) // Acquire connection from pool NOW (when service starts), not at load creation - double poolWaitTime = 0.0; if (load is PostgresQuery query && string.IsNullOrEmpty(query.ConnectionId)) { // Connection not yet assigned - acquire from pool now @@ -134,23 +133,13 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) } else { - // POOL EXHAUSTED: Request must WAIT for connection to become available - // This models PgBouncer behavior where clients queue when pool is full - - // Estimate wait time based on pooling mode: - // - Session pooling: Wait for session hold time (~100ms average) - // - Transaction pooling: Wait for query completion (~100ms average) - // Use exponential distribution for realistic variability - double meanWaitSecs = 0.100; // 100ms average wait - poolWaitTime = -meanWaitSecs * Math.Log(1.0 - rnd.NextDouble()); - - // After waiting, acquire connection (now guaranteed to succeed in this model) - // In reality, PgBouncer would wake up waiting client when connection freed - query.IsNewConnection = false; - query.ConnectionId = $"conn_waited_{Guid.NewGuid()}"; - - // NOTE: Connection is NOT actually in pool (we're simulating the wait) - // The release event will be no-op for this connection ID + // POOL EXHAUSTED: Open new connection outside pool + // This models real-world scenario where clients must bypass PgBouncer + // when max_client_conn is reached and open direct connections. + // Connection lifecycle is consistent: marked as new (pays 50ms overhead), + // not tracked in pool, so release becomes natural no-op. + query.IsNewConnection = true; + query.ConnectionId = $"direct_{Guid.NewGuid()}"; } } else @@ -180,8 +169,8 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) double estimatedBurstCost = (spec.FastSecs + connectionOverhead) * BurnRatePerSec; bool isThrottled = IsBurstable && Credits < estimatedBurstCost; - // 5. Determine Service Time (execution + overhead + pool wait time) - double baseTime = (isThrottled ? spec.SlowSecs : spec.FastSecs) + connectionOverhead + poolWaitTime; + // 5. Determine Service Time (execution + overhead) + double baseTime = (isThrottled ? spec.SlowSecs : spec.FastSecs) + connectionOverhead; double actualDuration = -baseTime * Math.Log(1.0 - rnd.NextDouble()); From bd9adf304d8867b0a4f9f8a9fec0ecbc17914478 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 20:10:00 +0800 Subject: [PATCH 13/32] feat(AzureDbBehavior): refine burn logic and service time estimation for improved throttling accuracy --- .../AzureDbSample/AzureDbBehavior.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs index 5f458e2..4e37340 100644 --- a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs +++ b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs @@ -165,14 +165,21 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) // Session pooling: no overhead } - // 4. Burn Logic (Look Ahead) - include overhead in throttling decision - double estimatedBurstCost = (spec.FastSecs + connectionOverhead) * BurnRatePerSec; + // 4. Burn Logic (Look Ahead) - estimate total cost for throttling decision + // Use mean execution time + deterministic overhead for cost estimation + double meanExecutionTime = spec.FastSecs; + double estimatedBurstCost = (meanExecutionTime + connectionOverhead) * BurnRatePerSec; bool isThrottled = IsBurstable && Credits < estimatedBurstCost; - // 5. Determine Service Time (execution + overhead) - double baseTime = (isThrottled ? spec.SlowSecs : spec.FastSecs) + connectionOverhead; + // 5. Determine Service Time using shifted exponential distribution + // Query execution time is exponential (variable) + double executionTimeMean = isThrottled ? spec.SlowSecs : spec.FastSecs; + double queryExecutionTime = -executionTimeMean * Math.Log(1.0 - rnd.NextDouble()); - double actualDuration = -baseTime * Math.Log(1.0 - rnd.NextDouble()); + // Connection overhead is deterministic (fixed) - additive, not part of exponential mean + // This ensures: (a) overhead is always exactly 50ms/8ms, not random + // (b) total duration is never less than overhead + double actualDuration = queryExecutionTime + connectionOverhead; // 6. Pay the Bill // Azure: No unlimited mode - hard throttle when credits depleted From d062de6ce179cfb77a9f39385962c274c5212c42 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 20:15:56 +0800 Subject: [PATCH 14/32] fix(azure-pgsql-pooling): improve pool size validation logic for session and transaction pooling modes --- SimNextgenApp.Demo/Program.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/SimNextgenApp.Demo/Program.cs b/SimNextgenApp.Demo/Program.cs index fa61e3f..913ab7b 100644 --- a/SimNextgenApp.Demo/Program.cs +++ b/SimNextgenApp.Demo/Program.cs @@ -438,14 +438,6 @@ getDefaultValue: () => 20 ); -poolSizeOption.AddValidator(result => -{ - if (result.GetValueOrDefault() <= 0) - { - result.ErrorMessage = "Pool size must be positive (e.g., --pool-size 20). A pool size of 0 or negative would cause all requests to fail."; - } -}); - var poolingSeriesOption = new Option( name: "--series", description: "The Azure instance series. Currently supported: B (Burstable).", @@ -513,6 +505,15 @@ return; } + // Validate pool size only when pooling is enabled (not in direct mode) + if (poolMode != PoolingMode.Direct && poolSize <= 0) + { + Console.WriteLine("Error: Pool size must be positive when using session or transaction pooling."); + Console.WriteLine($"Got: --pool-size {poolSize}"); + Console.WriteLine("Hint: Pool size is only ignored for --mode direct."); + return; + } + // Get Azure DB spec AzureDbInstanceSpec spec; try From 09231567b50f498382e4106a848146603abca2c8 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 20:24:38 +0800 Subject: [PATCH 15/32] feat: ensure script directory resolution for relative paths in simulation scripts --- SimNextgenApp.Demo/AwsRdsSample/simulation.ps1 | 4 ++++ .../AzureDbSample/pool-size-comparison.ps1 | 4 ++++ .../AzureDbSample/pool-size-comparison.sh | 12 +++++++----- .../AzureDbSample/pooling-comparison.ps1 | 4 ++++ .../AzureDbSample/pooling-comparison.sh | 12 +++++++----- SimNextgenApp.Demo/AzureDbSample/simulation.ps1 | 4 ++++ SimNextgenApp.Demo/AzureDbSample/simulation.sh | 4 ++++ 7 files changed, 34 insertions(+), 10 deletions(-) diff --git a/SimNextgenApp.Demo/AwsRdsSample/simulation.ps1 b/SimNextgenApp.Demo/AwsRdsSample/simulation.ps1 index 6e7eb51..5bfb7cc 100644 --- a/SimNextgenApp.Demo/AwsRdsSample/simulation.ps1 +++ b/SimNextgenApp.Demo/AwsRdsSample/simulation.ps1 @@ -29,6 +29,10 @@ param( $RemainingArgs ) +# Resolve script directory and cd into it to ensure relative paths work +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Set-Location $ScriptDir + Write-Host "Building project and starting SNA simulation..." -ForegroundColor Cyan $startTime = Get-Date diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 index 8df1188..bf29f3a 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 @@ -40,6 +40,10 @@ param( [int[]]$PoolSizes = @(5, 10, 20, 50, 100) ) +# Resolve script directory and cd into it to ensure relative paths work +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Set-Location $ScriptDir + # Color output functions function Write-ColorOutput { param([string]$Message, [string]$Color = "White") diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh index 4a389e1..c48e10d 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh @@ -3,6 +3,10 @@ # Exit on error, undefined variables, and pipe failures set -euo pipefail +# Resolve script directory and cd into it to ensure relative paths work +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + # SYNOPSIS # Compare PostgreSQL connection pool performance across different pool sizes. # DESCRIPTION @@ -87,16 +91,14 @@ run_pool_size() { POOL_START=$SECONDS - # Run simulation with session pooling - dotnet run --project ../SimNextgenApp.Demo.csproj -- demo azure-pgsql-pooling \ + # Run simulation with session pooling - use if ! pattern to handle errors properly with set -e + if ! dotnet run --project ../SimNextgenApp.Demo.csproj -- demo azure-pgsql-pooling \ --mode session \ --pool-size "$POOL_SIZE" \ --series "$SERIES" \ --size "$SIZE" \ --duration "$DURATION" \ - --initial-credits "$INITIAL_CREDITS" - - if [ $? -ne 0 ]; then + --initial-credits "$INITIAL_CREDITS"; then echo -e "${RED}Simulation failed for pool size ${POOL_SIZE}${NC}" return 1 fi diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 index 719de02..b99f81a 100644 --- a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 @@ -32,6 +32,10 @@ param( [int]$InitialCredits = 60 ) +# Resolve script directory and cd into it to ensure relative paths work +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Set-Location $ScriptDir + Write-Host "╔════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan Write-Host "║ PostgreSQL Connection Pooling Comparison ║" -ForegroundColor Cyan Write-Host "╚════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh index 185e6de..5723747 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh @@ -3,6 +3,10 @@ # Exit on error, undefined variables, and pipe failures set -euo pipefail +# Resolve script directory and cd into it to ensure relative paths work +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + # SYNOPSIS # Run PostgreSQL connection pooling comparison across all three modes. # DESCRIPTION @@ -88,16 +92,14 @@ run_mode() { MODE_START=$SECONDS - # Run simulation - dotnet run --project ../SimNextgenApp.Demo.csproj -- demo azure-pgsql-pooling \ + # Run simulation - use if ! pattern to handle errors properly with set -e + if ! dotnet run --project ../SimNextgenApp.Demo.csproj -- demo azure-pgsql-pooling \ --mode "$MODE" \ --pool-size "$POOL_SIZE" \ --series "$SERIES" \ --size "$SIZE" \ --duration "$DURATION" \ - --initial-credits "$INITIAL_CREDITS" - - if [ $? -ne 0 ]; then + --initial-credits "$INITIAL_CREDITS"; then echo -e "${RED}Simulation failed for ${MODE}${NC}" return 1 fi diff --git a/SimNextgenApp.Demo/AzureDbSample/simulation.ps1 b/SimNextgenApp.Demo/AzureDbSample/simulation.ps1 index cceea76..2858b22 100755 --- a/SimNextgenApp.Demo/AzureDbSample/simulation.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/simulation.ps1 @@ -26,6 +26,10 @@ param( $RemainingArgs ) +# Resolve script directory and cd into it to ensure relative paths work +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Set-Location $ScriptDir + Write-Host "Building project and starting SNA simulation..." -ForegroundColor Cyan $startTime = Get-Date diff --git a/SimNextgenApp.Demo/AzureDbSample/simulation.sh b/SimNextgenApp.Demo/AzureDbSample/simulation.sh index 9766c2d..5c7eba8 100755 --- a/SimNextgenApp.Demo/AzureDbSample/simulation.sh +++ b/SimNextgenApp.Demo/AzureDbSample/simulation.sh @@ -1,5 +1,9 @@ #!/bin/bash +# Resolve script directory and cd into it to ensure relative paths work +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + # SYNOPSIS # Run the SNA simulation and optionally plot results. # DESCRIPTION From d027e796743b1502983496da6b9897092b74400f Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 20:26:50 +0800 Subject: [PATCH 16/32] feat(ConnectionPool): update acquisition logic to implement spillover model for exhausted pool --- SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs b/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs index acd9da6..708b8f7 100644 --- a/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs +++ b/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs @@ -33,9 +33,14 @@ public ConnectionPool(int poolSize) /// Attempts to acquire a connection from the pool. /// Returns connection ID if successful, null if pool is exhausted. /// - /// HARD LIMIT: When pool is exhausted (returns null), caller must open - /// a new connection with overhead penalty. This matches PgBouncer behavior - /// when max_client_conn is reached. + /// SPILLOVER MODEL: When pool is exhausted (returns null), caller opens + /// a new direct connection to the database, bypassing the pool and paying + /// full connection overhead (50ms). This models scenarios where applications + /// fall back to direct connections when the pool is saturated, allowing + /// temporary exceedance of the pool size under load spikes. + /// + /// Note: This differs from PgBouncer's default queue-and-wait behavior. + /// Use this model to simulate spillover capacity in high-traffic scenarios. /// /// Called at SERVICE START (deferred acquisition), not at load creation. /// Requests naturally queue in SimQueue before reaching this point. @@ -44,7 +49,7 @@ public ConnectionPool(int poolSize) { if (_availableConnections.Count == 0) { - // Pool exhausted - caller must open new connection (with overhead) + // Pool exhausted - caller opens direct connection bypassing pool (spillover model) return null; } From 9d28ba2630de5a836beda643cf11ee67d9244a09 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 20:51:00 +0800 Subject: [PATCH 17/32] feat(AzureDbBehavior): enhance connection acquisition logic for pooling modes --- .../AzureDbSample/AzureDbBehavior.cs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs index 4e37340..99da271 100644 --- a/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs +++ b/SimNextgenApp.Demo/AzureDbSample/AzureDbBehavior.cs @@ -121,8 +121,22 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) if (load is PostgresQuery query && string.IsNullOrEmpty(query.ConnectionId)) { // Connection not yet assigned - acquire from pool now - if (_connectionPool != null && query.PoolMode != PoolingMode.Direct) + if (query.PoolMode == PoolingMode.Direct) { + // Direct mode: Always create new connection (no pool) + query.IsNewConnection = true; + query.ConnectionId = Guid.NewGuid().ToString(); + } + else + { + // Session or Transaction pooling mode - pool MUST be configured + if (_connectionPool == null) + { + throw new InvalidOperationException( + $"Connection pool not configured for {query.PoolMode} mode. " + + "Call SetConnectionPool() before running simulation with pooling enabled."); + } + var connId = _connectionPool.AcquireConnection(query.Id.ToString()); if (connId != null) @@ -142,12 +156,6 @@ public TimeSpan GetServiceTime(MyLoad load, Random rnd) query.ConnectionId = $"direct_{Guid.NewGuid()}"; } } - else - { - // Direct mode: Always create new connection (no pool) - query.IsNewConnection = true; - query.ConnectionId = Guid.NewGuid().ToString(); - } } // 3. Determine PostgreSQL overhead based on connection type From 550a94265c97d10636fbeb773448cd7ddb8f537f Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 20:52:59 +0800 Subject: [PATCH 18/32] refactor(ConnectionPool): update documentation to clarify spillover semantics and connection acquisition behavior --- SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs b/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs index 708b8f7..a1cc51d 100644 --- a/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs +++ b/SimNextgenApp.Demo/AzureDbSample/ConnectionPool.cs @@ -1,9 +1,10 @@ namespace SimNextgenApp.Demo.AzureDbSample; /// -/// Simulates a PgBouncer-style connection pool for PostgreSQL with HARD LIMIT semantics. -/// Connection acquisition happens when server starts processing (deferred acquisition), -/// matching real PgBouncer behavior where requests queue for available connections. +/// Simulates a connection pool for PostgreSQL with SPILLOVER semantics. +/// When the pool is exhausted, callers can open direct connections to the database, +/// temporarily exceeding the pool size (unlike true hard-limit pools that queue requests). +/// Connection acquisition happens when server starts processing (deferred acquisition). /// internal class ConnectionPool { From 930ecea3ad7cc060e63eb2a18bd409a116c71ec0 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Fri, 10 Apr 2026 20:59:16 +0800 Subject: [PATCH 19/32] feat(pooling-comparison): merge latency and credits data by query index to preserve all data points --- .../AzureDbSample/pooling-comparison.ps1 | 70 ++++--------------- .../AzureDbSample/pooling-comparison.sh | 38 +++++----- 2 files changed, 32 insertions(+), 76 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 index b99f81a..aca5e41 100644 --- a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 @@ -132,39 +132,19 @@ if ((Test-Path $latencyDirect) -and (Test-Path $latencySession) -and (Test-Path $sessionData = Import-Csv $latencySession $transactionData = Import-Csv $latencyTransaction - # Create hashtables indexed by rounded time - $directHash = @{} - $sessionHash = @{} - $transactionHash = @{} - - foreach ($row in $directData) { - $time = [math]::Round([double]$row."Simulation Time (s)", 2) - $directHash[$time] = $row."Latency (ms)" - } - - foreach ($row in $sessionData) { - $time = [math]::Round([double]$row."Simulation Time (s)", 2) - $sessionHash[$time] = $row."Latency (ms)" - } - - foreach ($row in $transactionData) { - $time = [math]::Round([double]$row."Simulation Time (s)", 2) - $transactionHash[$time] = $row."Latency (ms)" - } - - # Merge data + # Merge by row index (nth query) instead of timestamp to avoid data loss + # Event times differ across modes due to different service times, so timestamp-based + # merging would drop most rows. Row-index merge preserves all data points. $mergedLatency = @() - $mergedLatency += "Simulation Time (s),Direct (ms),Session (ms),Transaction (ms)" + $mergedLatency += "Query Index,Direct Time (s),Direct (ms),Session Time (s),Session (ms),Transaction Time (s),Transaction (ms)" - $allTimes = $directHash.Keys | Sort-Object - foreach ($time in $allTimes) { - if ($sessionHash.ContainsKey($time) -and $transactionHash.ContainsKey($time)) { - $mergedLatency += "$time,$($directHash[$time]),$($sessionHash[$time]),$($transactionHash[$time])" - } + $maxCount = [Math]::Min($directData.Count, [Math]::Min($sessionData.Count, $transactionData.Count)) + for ($i = 0; $i -lt $maxCount; $i++) { + $mergedLatency += "$($i+1),$($directData[$i].'Simulation Time (s)'),$($directData[$i].'Latency (ms)'),$($sessionData[$i].'Simulation Time (s)'),$($sessionData[$i].'Latency (ms)'),$($transactionData[$i].'Simulation Time (s)'),$($transactionData[$i].'Latency (ms)')" } $mergedLatency | Out-File "./output/pooling_comparison/latency_combined.csv" -Encoding UTF8 - Write-Host "✓ Created latency_combined.csv" -ForegroundColor Green + Write-Host "✓ Created latency_combined.csv (merged by query index)" -ForegroundColor Green } # Merge credits CSVs @@ -180,39 +160,17 @@ if ((Test-Path $creditsDirect) -and (Test-Path $creditsSession) -and (Test-Path $sessionData = Import-Csv $creditsSession $transactionData = Import-Csv $creditsTransaction - # Create hashtables indexed by rounded time - $directHash = @{} - $sessionHash = @{} - $transactionHash = @{} - - foreach ($row in $directData) { - $time = [math]::Round([double]$row."Simulation Time (s)", 2) - $directHash[$time] = $row."Credits" - } - - foreach ($row in $sessionData) { - $time = [math]::Round([double]$row."Simulation Time (s)", 2) - $sessionHash[$time] = $row."Credits" - } - - foreach ($row in $transactionData) { - $time = [math]::Round([double]$row."Simulation Time (s)", 2) - $transactionHash[$time] = $row."Credits" - } - - # Merge data + # Merge by row index (nth query) instead of timestamp to avoid data loss $mergedCredits = @() - $mergedCredits += "Simulation Time (s),Direct,Session,Transaction" + $mergedCredits += "Query Index,Direct Time (s),Direct,Session Time (s),Session,Transaction Time (s),Transaction" - $allTimes = $directHash.Keys | Sort-Object - foreach ($time in $allTimes) { - if ($sessionHash.ContainsKey($time) -and $transactionHash.ContainsKey($time)) { - $mergedCredits += "$time,$($directHash[$time]),$($sessionHash[$time]),$($transactionHash[$time])" - } + $maxCount = [Math]::Min($directData.Count, [Math]::Min($sessionData.Count, $transactionData.Count)) + for ($i = 0; $i -lt $maxCount; $i++) { + $mergedCredits += "$($i+1),$($directData[$i].'Simulation Time (s)'),$($directData[$i].'Credits'),$($sessionData[$i].'Simulation Time (s)'),$($sessionData[$i].'Credits'),$($transactionData[$i].'Simulation Time (s)'),$($transactionData[$i].'Credits')" } $mergedCredits | Out-File "./output/pooling_comparison/credits_combined.csv" -Encoding UTF8 - Write-Host "✓ Created credits_combined.csv" -ForegroundColor Green + Write-Host "✓ Created credits_combined.csv (merged by query index)" -ForegroundColor Green } # Generate individual graphs for PowerPoint overlay (with distinct colors!) diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh index 5723747..2fbe625 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh @@ -138,19 +138,19 @@ LATENCY_TRANSACTION="./output/pooling_comparison/transaction/simulation_latency. if [ -f "$LATENCY_DIRECT" ] && [ -f "$LATENCY_SESSION" ] && [ -f "$LATENCY_TRANSACTION" ]; then echo -e "${BLUE}Merging latency data...${NC}" - # Create merged latency CSV using a three-pass approach + # Merge by row index (nth query) instead of timestamp to avoid data loss + # Event times differ across modes due to different service times, so timestamp-based + # joins would drop most rows. Row-index merge preserves all data points. { - echo "Simulation Time (s),Direct (ms),Session (ms),Transaction (ms)" - - # Build associative arrays using join - join -t, -j1 -o 1.1,1.2,2.2 \ - <(tail -n +2 "$LATENCY_DIRECT" | awk -F, '{printf "%.2f,%s\n", $1, $2}' | sort -t, -k1 -n) \ - <(tail -n +2 "$LATENCY_SESSION" | awk -F, '{printf "%.2f,%s\n", $1, $2}' | sort -t, -k1 -n) \ - | join -t, -j1 -o 1.1,1.2,1.3,2.2 - \ - <(tail -n +2 "$LATENCY_TRANSACTION" | awk -F, '{printf "%.2f,%s\n", $1, $2}' | sort -t, -k1 -n) + echo "Query Index,Direct Time (s),Direct (ms),Session Time (s),Session (ms),Transaction Time (s),Transaction (ms)" + + paste -d, \ + <(tail -n +2 "$LATENCY_DIRECT" | awk -F, '{print NR "," $1 "," $2}') \ + <(tail -n +2 "$LATENCY_SESSION" | awk -F, '{print $1 "," $2}') \ + <(tail -n +2 "$LATENCY_TRANSACTION" | awk -F, '{print $1 "," $2}') } > "./output/pooling_comparison/latency_combined.csv" - echo -e "${GREEN}✓ Created latency_combined.csv${NC}" + echo -e "${GREEN}✓ Created latency_combined.csv (merged by query index)${NC}" fi # Merge credits CSVs @@ -161,19 +161,17 @@ CREDITS_TRANSACTION="./output/pooling_comparison/transaction/simulation_credits. if [ -f "$CREDITS_DIRECT" ] && [ -f "$CREDITS_SESSION" ] && [ -f "$CREDITS_TRANSACTION" ]; then echo -e "${BLUE}Merging credits data...${NC}" - # Create merged credits CSV using a three-pass approach + # Merge by row index (nth query) instead of timestamp to avoid data loss { - echo "Simulation Time (s),Direct,Session,Transaction" - - # Build associative arrays using join - join -t, -j1 -o 1.1,1.2,2.2 \ - <(tail -n +2 "$CREDITS_DIRECT" | awk -F, '{printf "%.2f,%s\n", $1, $2}' | sort -t, -k1 -n) \ - <(tail -n +2 "$CREDITS_SESSION" | awk -F, '{printf "%.2f,%s\n", $1, $2}' | sort -t, -k1 -n) \ - | join -t, -j1 -o 1.1,1.2,1.3,2.2 - \ - <(tail -n +2 "$CREDITS_TRANSACTION" | awk -F, '{printf "%.2f,%s\n", $1, $2}' | sort -t, -k1 -n) + echo "Query Index,Direct Time (s),Direct,Session Time (s),Session,Transaction Time (s),Transaction" + + paste -d, \ + <(tail -n +2 "$CREDITS_DIRECT" | awk -F, '{print NR "," $1 "," $2}') \ + <(tail -n +2 "$CREDITS_SESSION" | awk -F, '{print $1 "," $2}') \ + <(tail -n +2 "$CREDITS_TRANSACTION" | awk -F, '{print $1 "," $2}') } > "./output/pooling_comparison/credits_combined.csv" - echo -e "${GREEN}✓ Created credits_combined.csv${NC}" + echo -e "${GREEN}✓ Created credits_combined.csv (merged by query index)${NC}" fi # Generate individual graphs for PowerPoint overlay (with distinct colors!) From fda6b66cf5673a754db7cd2d1f0a0e2bc4d671ed Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 06:12:00 +0800 Subject: [PATCH 20/32] refactor(pool-size-comparison): remove unnecessary sorting of latencies before statistics calculation --- SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 | 3 --- 1 file changed, 3 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 index bf29f3a..1f1db35 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 @@ -135,9 +135,6 @@ foreach ($PoolSize in $PoolSizes) { $Latencies = $Data | ForEach-Object { [double]$_.'Latency (ms)' } if ($Latencies.Count -gt 0) { - $SortedLatencies = $Latencies | Sort-Object - $Count = $SortedLatencies.Count - $Stats = [PSCustomObject]@{ PoolSize = $PoolSize AvgLatency = [Math]::Round(($Latencies | Measure-Object -Average).Average, 2) From 4d33ec2eed241c7b4b348eb04b07744a4d0af75a Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 07:01:57 +0800 Subject: [PATCH 21/32] refactor(pooling-comparison): update latency calculations to use median instead of average for robustness --- .../AzureDbSample/pool-size-comparison.ps1 | 15 +++- .../AzureDbSample/pool-size-comparison.sh | 27 +++++-- .../AzureDbSample/pooling-comparison.ps1 | 72 +++++++----------- .../AzureDbSample/pooling-comparison.sh | 75 +++++++------------ SimNextgenApp.Demo/Program.cs | 18 ++--- 5 files changed, 98 insertions(+), 109 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 index 1f1db35..b7a0afd 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 @@ -180,7 +180,6 @@ if (-not $GraphAvailable) { --xlabel "Pool Size" ` --ylabel "Latency (ms)" ` --bar ` - --bar-label ` -o "$OutputDir/latency_bar_chart.png" Write-ColorOutput "✓ Generated latency_bar_chart.png (full scale)" "Green" @@ -192,7 +191,6 @@ if (-not $GraphAvailable) { graph "$OutputDir/latency_summary.csv" ` --bar ` - --bar-label ` --title "Average Latency vs Pool Size (Zoomed)" ` --xlabel "Pool Size" ` --ylabel "Latency (ms)" ` @@ -234,9 +232,18 @@ foreach ($Stats in $SummaryData) { Write-Host "" Write-ColorOutput "Recommendations:" "Blue" -# Find optimal pool size (lowest average latency) -$OptimalStats = $SummaryData | Sort-Object AvgLatency | Select-Object -First 1 +# Find minimum latency +$MinLatency = ($SummaryData | Measure-Object -Property AvgLatency -Minimum).Minimum + +# Find smallest pool size that achieves near-optimal performance (within 1% of minimum) +$Threshold = $MinLatency * 1.01 +$OptimalStats = $SummaryData | + Where-Object { $_.AvgLatency -le $Threshold } | + Sort-Object PoolSize | + Select-Object -First 1 + Write-ColorOutput " • Optimal pool size: $($OptimalStats.PoolSize) (avg latency: $($OptimalStats.AvgLatency)ms)" "Green" +Write-ColorOutput " • Note: Smallest pool size achieving near-optimal performance (≤1% of minimum)" "Cyan" # Check for diminishing returns (when improvement < 5%) for ($i = 1; $i -lt $SummaryData.Count; $i++) { diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh index c48e10d..096640a 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh @@ -65,7 +65,7 @@ while [[ $# -gt 0 ]]; do done echo -e "${CYAN}╔════════════════════════════════════════════════════════════════╗${NC}" -echo -e "${CYAN}║ PostgreSQL Pool Size Optimization Analysis ║${NC}" +echo -e "${CYAN}║ PostgreSQL Pool Size Optimization Analysis ║${NC}" echo -e "${CYAN}╚════════════════════════════════════════════════════════════════╝${NC}" echo "" echo -e "${BLUE}Configuration:${NC}" @@ -191,7 +191,6 @@ else --xlabel "Pool Size" \ --ylabel "Latency (ms)" \ --bar \ - --bar-label \ -o "./output/pool_size_comparison/latency_bar_chart.png" echo -e "${GREEN}✓ Generated latency_bar_chart.png (full scale)${NC}" @@ -203,7 +202,6 @@ else graph "./output/pool_size_comparison/latency_summary.csv" \ --bar \ - --bar-label \ --title "Average Latency vs Pool Size (Zoomed)" \ --xlabel "Pool Size" \ --ylabel "Latency (ms)" \ @@ -256,11 +254,26 @@ done echo "" echo -e "${BLUE}Recommendations:${NC}" -# Find optimal pool size (lowest average latency) -OPTIMAL_SIZE=$(awk -F, 'NR>1 {if(NR==2 || $21 {if(NR==2 || $21 {if(NR==2 || $21 { + threshold = minlat * 1.01; + if ($2 <= threshold) { + if (optimal == "" || $1 < optimal) { + optimal = $1; + optlat = $2; + } + } +} END { + printf "%s", optimal +}' "./output/pool_size_comparison/latency_summary.csv") + +OPTIMAL_LATENCY=$(awk -F, -v size="$OPTIMAL_SIZE" 'NR>1 && $1==size {printf "%.2f", $2}' "./output/pool_size_comparison/latency_summary.csv") echo -e " • ${GREEN}Optimal pool size: ${OPTIMAL_SIZE} (avg latency: ${OPTIMAL_LATENCY}ms)${NC}" +echo -e " • ${CYAN}Note: Smallest pool size achieving near-optimal performance (≤1% of minimum)${NC}" # Check for diminishing returns (when improvement < 5%) LAST_AVG="" @@ -286,7 +299,7 @@ done OVERALL_ELAPSED=$(($SECONDS - $OVERALL_START)) echo "" echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}" -echo -e "${GREEN}║ Pool Size Comparison Complete! (Total: ${OVERALL_ELAPSED}s) ║${NC}" +echo -e "${GREEN}║ Pool Size Comparison Complete! (Total: ${OVERALL_ELAPSED}s) ║${NC}" echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}" echo "" echo -e "Results saved in: ${BLUE}./output/pool_size_comparison/${NC}" diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 index aca5e41..b85bd38 100644 --- a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 @@ -216,67 +216,53 @@ if (-not (Get-Command graph -ErrorAction SilentlyContinue)) { } } - # Also generate combined overlay graph (all colors in one) - $latencyCombined = "./output/pooling_comparison/latency_combined.csv" - if (Test-Path $latencyCombined) { - $combinedData = Import-Csv $latencyCombined - $latencyMax = ($combinedData | ForEach-Object { - [math]::Max([math]::Max([double]$_."Direct (ms)", [double]$_."Session (ms)"), [double]$_."Transaction (ms)") - } | Measure-Object -Maximum).Maximum - - if ($null -eq $latencyMax) { $latencyMax = 1 } - - graph $latencyCombined --title "Connection Pooling Latency Comparison" --yrange="0:$latencyMax" -o "./output/pooling_comparison/latency_comparison.png" - Write-Host "✓ Generated latency_comparison.png (all modes)" -ForegroundColor Green - } - - $creditsCombined = "./output/pooling_comparison/credits_combined.csv" - if (Test-Path $creditsCombined) { - $combinedData = Import-Csv $creditsCombined - $creditMax = ($combinedData | ForEach-Object { - [math]::Max([math]::Max([double]$_.Direct, [double]$_.Session), [double]$_.Transaction) - } | Measure-Object -Maximum).Maximum - - if ($null -eq $creditMax) { $creditMax = 1 } - - graph $creditsCombined --title "Connection Pooling CPU Credits Comparison" --yrange="0:$creditMax" -o "./output/pooling_comparison/credits_comparison.png" - Write-Host "✓ Generated credits_comparison.png (all modes)" -ForegroundColor Green - } - # Generate summary bar charts Write-Host "Generating summary bar charts..." -ForegroundColor Blue - # Calculate average latencies + # Calculate median latencies (more robust than mean for latency comparisons) $directData = Import-Csv "./output/pooling_comparison/direct/simulation_latency.csv" $sessionData = Import-Csv "./output/pooling_comparison/session/simulation_latency.csv" $transactionData = Import-Csv "./output/pooling_comparison/transaction/simulation_latency.csv" - $directAvg = ($directData | ForEach-Object { [double]$_."Latency (ms)" } | Measure-Object -Average).Average - $sessionAvg = ($sessionData | ForEach-Object { [double]$_."Latency (ms)" } | Measure-Object -Average).Average - $transactionAvg = ($transactionData | ForEach-Object { [double]$_."Latency (ms)" } | Measure-Object -Average).Average + function Get-Median { + param([double[]]$values) + $sorted = $values | Sort-Object + $count = $sorted.Count + if ($count -eq 0) { return 0 } + $mid = [math]::Floor($count / 2) + if ($count % 2 -eq 1) { + return $sorted[$mid] + } else { + return ($sorted[$mid - 1] + $sorted[$mid]) / 2 + } + } + + $directMedian = Get-Median ($directData | ForEach-Object { [double]$_."Latency (ms)" }) + $sessionMedian = Get-Median ($sessionData | ForEach-Object { [double]$_."Latency (ms)" }) + $transactionMedian = Get-Median ($transactionData | ForEach-Object { [double]$_."Latency (ms)" }) # Create summary CSV for bar chart $summaryCsv = @" -Mode,Average Latency (ms) -Direct,$([math]::Round($directAvg, 2)) -Session,$([math]::Round($sessionAvg, 2)) -Transaction,$([math]::Round($transactionAvg, 2)) +Mode,Median Latency (ms) +Direct,$([math]::Round($directMedian, 2)) +Session,$([math]::Round($sessionMedian, 2)) +Transaction,$([math]::Round($transactionMedian, 2)) "@ $summaryCsv | Out-File "./output/pooling_comparison/latency_summary.csv" -Encoding UTF8 # Generate bar chart (full scale from 0) - graph "./output/pooling_comparison/latency_summary.csv" --bar --bar-label --title "Average Latency Comparison" --ylabel "Latency (ms)" -o "./output/pooling_comparison/latency_bar_chart.png" + graph "./output/pooling_comparison/latency_summary.csv" --bar --title "Median Latency Comparison (P50)" --ylabel "Latency (ms)" -o "./output/pooling_comparison/latency_bar_chart.png" Write-Host "✓ Generated latency_bar_chart.png (full scale)" -ForegroundColor Green # Generate zoomed bar chart (emphasizes differences) $summaryData = Import-Csv "./output/pooling_comparison/latency_summary.csv" - $latencies = $summaryData | ForEach-Object { [double]$_."Average Latency (ms)" } + $latencies = $summaryData | ForEach-Object { [double]$_."Median Latency (ms)" } $minLatency = ($latencies | Measure-Object -Minimum).Minimum $maxLatency = ($latencies | Measure-Object -Maximum).Maximum - $rangeMin = [math]::Floor($minLatency - 0.1) - $rangeMax = [math]::Ceiling($maxLatency + 0.1) + $rangeMin = [math]::Floor($minLatency - 1.0) + $rangeMax = [math]::Ceiling($maxLatency + 1.0) - graph "./output/pooling_comparison/latency_summary.csv" --bar --bar-label --title "Average Latency Comparison (Zoomed)" --ylabel "Latency (ms)" --yrange="$rangeMin`:$rangeMax" -o "./output/pooling_comparison/latency_bar_chart_zoomed.png" + graph "./output/pooling_comparison/latency_summary.csv" --bar --title "Median Latency Comparison (Zoomed)" --ylabel "Latency (ms)" --yrange="$rangeMin`:$rangeMax" -o "./output/pooling_comparison/latency_bar_chart_zoomed.png" Write-Host "✓ Generated latency_bar_chart_zoomed.png (emphasizes differences)" -ForegroundColor Green } @@ -287,7 +273,7 @@ Write-Host "Summary" -ForegroundColor Cyan Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan $summaryFormat = "{0,-30} {1,15} {2,15} {3,15}" -Write-Host ($summaryFormat -f "Mode", "Avg Latency", "Min Latency", "Max Latency") +Write-Host ($summaryFormat -f "Mode", "Median (P50)", "Min Latency", "Max Latency") Write-Host "───────────────────────────────────────────────────────────────────────────" foreach ($modeInfo in $modes) { @@ -298,7 +284,7 @@ foreach ($modeInfo in $modes) { $data = Import-Csv $csvFile $latencies = $data | ForEach-Object { [double]$_."Latency (ms)" } - $avg = ($latencies | Measure-Object -Average).Average + $median = Get-Median $latencies $min = ($latencies | Measure-Object -Minimum).Minimum $max = ($latencies | Measure-Object -Maximum).Maximum @@ -308,7 +294,7 @@ foreach ($modeInfo in $modes) { "transaction" { "Transaction (8ms overhead)" } } - Write-Host ($summaryFormat -f $modeLabel, "$([math]::Round($avg, 2)) ms", "$([math]::Round($min, 2)) ms", "$([math]::Round($max, 2)) ms") + Write-Host ($summaryFormat -f $modeLabel, "$([math]::Round($median, 2)) ms", "$([math]::Round($min, 2)) ms", "$([math]::Round($max, 2)) ms") } } diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh index 2fbe625..410a04e 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh @@ -65,7 +65,7 @@ while [[ $# -gt 0 ]]; do done echo -e "${CYAN}╔════════════════════════════════════════════════════════════════╗${NC}" -echo -e "${CYAN}║ PostgreSQL Connection Pooling Comparison ║${NC}" +echo -e "${CYAN}║ PostgreSQL Connection Pooling Comparison ║${NC}" echo -e "${CYAN}╚════════════════════════════════════════════════════════════════╝${NC}" echo "" echo -e "${BLUE}Configuration:${NC}" @@ -214,37 +214,27 @@ else fi done - # Also generate combined overlay graph (all colors in one) - if [ -f "./output/pooling_comparison/latency_combined.csv" ]; then - LATENCY_MAX=$(awk -F, 'BEGIN {max=0} NR>1 {for(i=2;i<=NF;i++){if($i>max)max=$i}} END {print (max==0?1:max)}' "./output/pooling_comparison/latency_combined.csv") - graph "./output/pooling_comparison/latency_combined.csv" \ - --title "Connection Pooling Latency Comparison" \ - --yrange=0:$LATENCY_MAX \ - -o "./output/pooling_comparison/latency_comparison.png" - echo -e "${GREEN}✓ Generated latency_comparison.png (all modes)${NC}" - fi - # Generate summary bar charts echo -e "${BLUE}Generating summary bar charts...${NC}" - # Calculate average latencies - DIRECT_AVG=$(awk -F, 'NR>1 {sum+=$2; count++} END {printf "%.2f", sum/count}' "./output/pooling_comparison/direct/simulation_latency.csv") - SESSION_AVG=$(awk -F, 'NR>1 {sum+=$2; count++} END {printf "%.2f", sum/count}' "./output/pooling_comparison/session/simulation_latency.csv") - TRANSACTION_AVG=$(awk -F, 'NR>1 {sum+=$2; count++} END {printf "%.2f", sum/count}' "./output/pooling_comparison/transaction/simulation_latency.csv") + # Calculate median latencies + # Use sort for portability + DIRECT_MEDIAN=$(awk -F, 'NR>1 {print $2}' "./output/pooling_comparison/direct/simulation_latency.csv" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') + SESSION_MEDIAN=$(awk -F, 'NR>1 {print $2}' "./output/pooling_comparison/session/simulation_latency.csv" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') + TRANSACTION_MEDIAN=$(awk -F, 'NR>1 {print $2}' "./output/pooling_comparison/transaction/simulation_latency.csv" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') # Create summary CSV for bar chart cat > "./output/pooling_comparison/latency_summary.csv" << EOF -Mode,Average Latency (ms) -Direct,$DIRECT_AVG -Session,$SESSION_AVG -Transaction,$TRANSACTION_AVG +Mode,Median Latency (ms) +Direct,$DIRECT_MEDIAN +Session,$SESSION_MEDIAN +Transaction,$TRANSACTION_MEDIAN EOF # Generate bar chart (full scale from 0) graph "./output/pooling_comparison/latency_summary.csv" \ --bar \ - --bar-label \ - --title "Average Latency Comparison" \ + --title "Median Latency Comparison (P50)" \ --ylabel "Latency (ms)" \ -o "./output/pooling_comparison/latency_bar_chart.png" echo -e "${GREEN}✓ Generated latency_bar_chart.png (full scale)${NC}" @@ -252,26 +242,16 @@ EOF # Generate zoomed bar chart (emphasizes differences) MIN_LATENCY=$(awk -F, 'NR>1 {if(NR==2 || $21 {if(NR==2 || $2>max) max=$2} END {printf "%.0f", max}' "./output/pooling_comparison/latency_summary.csv") - RANGE_MIN=$(awk "BEGIN {printf \"%.1f\", $MIN_LATENCY - 0.1}") - RANGE_MAX=$(awk "BEGIN {printf \"%.1f\", $MAX_LATENCY + 0.1}") + RANGE_MIN=$(awk "BEGIN {printf \"%.1f\", $MIN_LATENCY - 1.0}") + RANGE_MAX=$(awk "BEGIN {printf \"%.1f\", $MAX_LATENCY + 1.0}") graph "./output/pooling_comparison/latency_summary.csv" \ --bar \ - --bar-label \ - --title "Average Latency Comparison (Zoomed)" \ + --title "Median Latency Comparison (Zoomed)" \ --ylabel "Latency (ms)" \ --yrange=$RANGE_MIN:$RANGE_MAX \ -o "./output/pooling_comparison/latency_bar_chart_zoomed.png" echo -e "${GREEN}✓ Generated latency_bar_chart_zoomed.png (emphasizes differences)${NC}" - - if [ -f "./output/pooling_comparison/credits_combined.csv" ]; then - CREDIT_MAX=$(awk -F, 'BEGIN {max=0} NR>1 {for(i=2;i<=NF;i++){if($i>max)max=$i}} END {print (max==0?1:max)}' "./output/pooling_comparison/credits_combined.csv") - graph "./output/pooling_comparison/credits_combined.csv" \ - --title "Connection Pooling CPU Credits Comparison" \ - --yrange=0:$CREDIT_MAX \ - -o "./output/pooling_comparison/credits_comparison.png" - echo -e "${GREEN}✓ Generated credits_comparison.png (all modes)${NC}" - fi fi # Calculate and display summary @@ -280,33 +260,36 @@ echo -e "${CYAN}═════════════════════ echo -e "${CYAN}Summary${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" -printf "%-30s %-15s %-15s %-15s\n" "Mode" "Avg Latency" "Min Latency" "Max Latency" +printf "%-30s %-15s %-15s %-15s\n" "Mode" "Median (P50)" "Min Latency" "Max Latency" echo "───────────────────────────────────────────────────────────────────────────" for MODE in "${MODES[@]}"; do CSV_FILE="./output/pooling_comparison/${MODE}/simulation_latency.csv" if [ -f "$CSV_FILE" ]; then - # Calculate stats using awk - STATS=$(awk -F, 'NR>1 { - sum+=$2; - count++; + # Calculate stats using awk (median + min/max) + # Use sort for portability + MIN_MAX=$(awk -F, 'NR>1 { if(NR==2 || $2max) max=$2 } END { - printf "%.2f %.2f %.2f", sum/count, min, max + printf "%.2f %.2f", min, max }' "$CSV_FILE") - read AVG MIN MAX <<< "$STATS" + MEDIAN=$(awk -F, 'NR>1 {print $2}' "$CSV_FILE" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') + + STATS="$MEDIAN $MIN_MAX" + + read MEDIAN MIN MAX <<< "$STATS" MODE_LABEL="" case $MODE in - direct) MODE_LABEL="Direct (50ms overhead)" ;; - session) MODE_LABEL="Session Pooling (no overhead)" ;; - transaction) MODE_LABEL="Transaction (8ms overhead)" ;; + direct) MODE_LABEL="Direct" ;; + session) MODE_LABEL="Session Pooling" ;; + transaction) MODE_LABEL="Transaction" ;; esac - printf "%-30s %10.2f ms %10.2f ms %10.2f ms\n" "$MODE_LABEL" "$AVG" "$MIN" "$MAX" + printf "%-30s %10.2f ms %10.2f ms %10.2f ms\n" "$MODE_LABEL" "$MEDIAN" "$MIN" "$MAX" fi done @@ -314,7 +297,7 @@ done OVERALL_ELAPSED=$(($SECONDS - $OVERALL_START)) echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}" -echo -e "${GREEN}║ Comparison Complete! (Total Duration: ${OVERALL_ELAPSED}s) ║${NC}" +echo -e "${GREEN}║ Comparison Complete! (Total Duration: ${OVERALL_ELAPSED}s) ║${NC}" echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}" echo "" echo -e "Results saved in: ${BLUE}./output/pooling_comparison/${NC}" diff --git a/SimNextgenApp.Demo/Program.cs b/SimNextgenApp.Demo/Program.cs index 913ab7b..2503c93 100644 --- a/SimNextgenApp.Demo/Program.cs +++ b/SimNextgenApp.Demo/Program.cs @@ -93,8 +93,8 @@ // Set handler mmckCommand.SetHandler( - (int servers, int capacity, double arrivalSecs, double serviceSecs, - double duration, double warmup, int genSeed, int serverSeedBase) => + (servers, capacity, arrivalSecs, serviceSecs, + duration, warmup, genSeed, serverSeedBase) => { Console.WriteLine($"====== Running MMCK Demo (c={servers}, K={capacity}) ======"); SimpleMmck.RunDemo( @@ -251,8 +251,8 @@ simpleRestaurantCommand.AddOption(stopProbabilityOption); simpleRestaurantCommand.SetHandler( - (List tables, List waiters, Point entranceLocation, Point kitchenLocation, - double customerArrivalMin, double stopProbability) => + (tables, waiters, entranceLocation, kitchenLocation, + customerArrivalMin, stopProbability) => { Log.Logger = new LoggerConfiguration() .MinimumLevel.Verbose() @@ -319,7 +319,7 @@ awsRdsBurstCommand.AddOption(unlimitedCreditsOption); awsRdsBurstCommand.AddOption(grafanaOption); -awsRdsBurstCommand.SetHandler((string family, string size, double duration, double initialCredits, bool isUnlimitedCredits, bool enableGrafana) => +awsRdsBurstCommand.SetHandler((family, size, duration, initialCredits, isUnlimitedCredits, enableGrafana) => { Log.Logger = new LoggerConfiguration() .MinimumLevel.Verbose() @@ -383,12 +383,12 @@ azureDbBurstCommand.AddOption(azureInitialCreditsOption); azureDbBurstCommand.AddOption(azureGrafanaOption); -azureDbBurstCommand.SetHandler((string series, string size, double duration, double initialCredits, bool enableGrafana) => +azureDbBurstCommand.SetHandler((series, size, duration, initialCredits, enableGrafana) => { Log.Logger = new LoggerConfiguration() .MinimumLevel.Verbose() .Enrich.FromLogContext() - .WriteTo.Console() + //.WriteTo.Console() .CreateLogger(); // Create a logger factory that uses Serilog @@ -476,12 +476,12 @@ azurePgsqlPoolingCommand.AddOption(poolingInitialCreditsOption); azurePgsqlPoolingCommand.AddOption(poolingGrafanaOption); -azurePgsqlPoolingCommand.SetHandler((string mode, int poolSize, string series, string size, double duration, double initialCredits, bool enableGrafana) => +azurePgsqlPoolingCommand.SetHandler((mode, poolSize, series, size, duration, initialCredits, enableGrafana) => { Log.Logger = new LoggerConfiguration() .MinimumLevel.Verbose() .Enrich.FromLogContext() - .WriteTo.Console() + //.WriteTo.Console() .CreateLogger(); // Create a logger factory that uses Serilog From 0b950badd8142c33b71f767184cd16e7a6e6d1d1 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 07:29:40 +0800 Subject: [PATCH 22/32] feat(pool-size-comparison): update latency calculations to use median instead of average for improved accuracy --- .../AzureDbSample/pool-size-comparison.ps1 | 58 ++++++++++++------ .../AzureDbSample/pool-size-comparison.sh | 61 +++++++++++-------- .../Scenarios/AzurePgsqlPoolingScenario.cs | 4 +- 3 files changed, 79 insertions(+), 44 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 index b7a0afd..ab9d056 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 @@ -125,6 +125,20 @@ Write-ColorOutput "════════════════════ Write-ColorOutput "Generating Summary Statistics" "Cyan" Write-ColorOutput "═══════════════════════════════════════════════════════════════" "Cyan" +# Helper function to calculate median +function Get-Median { + param([double[]]$values) + $sorted = $values | Sort-Object + $count = $sorted.Count + if ($count -eq 0) { return 0 } + $mid = [math]::Floor($count / 2) + if ($count % 2 -eq 1) { + return $sorted[$mid] + } else { + return ($sorted[$mid - 1] + $sorted[$mid]) / 2 + } +} + # Create summary data structures $SummaryData = @() @@ -137,7 +151,7 @@ foreach ($PoolSize in $PoolSizes) { if ($Latencies.Count -gt 0) { $Stats = [PSCustomObject]@{ PoolSize = $PoolSize - AvgLatency = [Math]::Round(($Latencies | Measure-Object -Average).Average, 2) + MedianLatency = [Math]::Round((Get-Median $Latencies), 2) MinLatency = [Math]::Round(($Latencies | Measure-Object -Minimum).Minimum, 2) MaxLatency = [Math]::Round(($Latencies | Measure-Object -Maximum).Maximum, 2) } @@ -152,7 +166,7 @@ $SummaryData | Export-Csv "$OutputDir/latency_vs_pool_size.csv" -NoTypeInformati Write-ColorOutput "✓ Created latency_vs_pool_size.csv" "Green" # Export simplified summary for charts -$SummaryData | Select-Object @{Name='Pool Size';Expression={$_.PoolSize}}, @{Name='Average Latency (ms)';Expression={$_.AvgLatency}} | +$SummaryData | Select-Object @{Name='Pool Size';Expression={$_.PoolSize}}, @{Name='Median Latency (ms)';Expression={$_.MedianLatency}} | Export-Csv "$OutputDir/latency_summary.csv" -NoTypeInformation Write-ColorOutput "✓ Created latency_summary.csv" "Green" @@ -166,9 +180,9 @@ if (-not $GraphAvailable) { } else { Write-ColorOutput "Generating comparison charts..." "Blue" - # Generate line chart: Average Latency vs Pool Size + # Generate line chart: Median Latency vs Pool Size graph "$OutputDir/latency_summary.csv" ` - --title "Average Latency vs Pool Size (Session Pooling)" ` + --title "Median Latency vs Pool Size (Session Pooling)" ` --xlabel "Pool Size" ` --ylabel "Latency (ms)" ` -o "$OutputDir/latency_vs_pool_size.png" @@ -176,7 +190,7 @@ if (-not $GraphAvailable) { # Generate bar chart for easier reading (full scale from 0) graph "$OutputDir/latency_summary.csv" ` - --title "Average Latency vs Pool Size (Session Pooling)" ` + --title "Median Latency vs Pool Size (Session Pooling)" ` --xlabel "Pool Size" ` --ylabel "Latency (ms)" ` --bar ` @@ -184,14 +198,14 @@ if (-not $GraphAvailable) { Write-ColorOutput "✓ Generated latency_bar_chart.png (full scale)" "Green" # Generate zoomed bar chart (emphasizes differences) - $MinLatency = ($SummaryData | Measure-Object -Property AvgLatency -Minimum).Minimum - $MaxLatency = ($SummaryData | Measure-Object -Property AvgLatency -Maximum).Maximum + $MinLatency = ($SummaryData | Measure-Object -Property MedianLatency -Minimum).Minimum + $MaxLatency = ($SummaryData | Measure-Object -Property MedianLatency -Maximum).Maximum $RangeMin = $MinLatency - 5 $RangeMax = $MaxLatency + 5 graph "$OutputDir/latency_summary.csv" ` --bar ` - --title "Average Latency vs Pool Size (Zoomed)" ` + --title "Median Latency vs Pool Size (Zoomed)" ` --xlabel "Pool Size" ` --ylabel "Latency (ms)" ` --yrange "$RangeMin`:$RangeMax" ` @@ -221,40 +235,48 @@ Write-ColorOutput "════════════════════ Write-ColorOutput "Summary: Latency vs Pool Size" "Cyan" Write-ColorOutput "═══════════════════════════════════════════════════════════════" "Cyan" -Write-Host $("{0,-12} {1,-15} {2,-15} {3,-15}" -f "Pool Size", "Avg Latency", "Min Latency", "Max Latency") +Write-Host $("{0,-12} {1,-15} {2,-15} {3,-15}" -f "Pool Size", "Median (P50)", "Min Latency", "Max Latency") Write-Host "────────────────────────────────────────────────────────────────────" foreach ($Stats in $SummaryData) { Write-Host $("{0,-12} {1,10:F2} ms {2,10:F2} ms {3,10:F2} ms" -f ` - $Stats.PoolSize, $Stats.AvgLatency, $Stats.MinLatency, $Stats.MaxLatency) + $Stats.PoolSize, $Stats.MedianLatency, $Stats.MinLatency, $Stats.MaxLatency) } Write-Host "" Write-ColorOutput "Recommendations:" "Blue" # Find minimum latency -$MinLatency = ($SummaryData | Measure-Object -Property AvgLatency -Minimum).Minimum +$MinLatency = ($SummaryData | Measure-Object -Property MedianLatency -Minimum).Minimum # Find smallest pool size that achieves near-optimal performance (within 1% of minimum) $Threshold = $MinLatency * 1.01 $OptimalStats = $SummaryData | - Where-Object { $_.AvgLatency -le $Threshold } | + Where-Object { $_.MedianLatency -le $Threshold } | Sort-Object PoolSize | Select-Object -First 1 -Write-ColorOutput " • Optimal pool size: $($OptimalStats.PoolSize) (avg latency: $($OptimalStats.AvgLatency)ms)" "Green" +Write-ColorOutput " • Optimal pool size: $($OptimalStats.PoolSize) (median latency: $($OptimalStats.MedianLatency)ms)" "Green" Write-ColorOutput " • Note: Smallest pool size achieving near-optimal performance (≤1% of minimum)" "Cyan" # Check for diminishing returns (when improvement < 5%) -for ($i = 1; $i -lt $SummaryData.Count; $i++) { - $PrevAvg = $SummaryData[$i-1].AvgLatency - $CurrentAvg = $SummaryData[$i].AvgLatency +# Sort pool sizes numerically for meaningful comparison +$SortedData = $SummaryData | Sort-Object PoolSize + +for ($i = 1; $i -lt $SortedData.Count; $i++) { + # Skip the optimal pool size (already highlighted above) + if ($SortedData[$i].PoolSize -eq $OptimalStats.PoolSize) { + continue + } + + $PrevAvg = $SortedData[$i-1].MedianLatency + $CurrentAvg = $SortedData[$i].MedianLatency $Improvement = ($PrevAvg - $CurrentAvg) / $PrevAvg * 100 if ($Improvement -lt 0) { - Write-ColorOutput " • Pool size $($SummaryData[$i].PoolSize): Performance degraded ($([Math]::Round($Improvement, 2))% worse)" "Yellow" + Write-ColorOutput " • Pool size $($SortedData[$i].PoolSize): Performance degraded ($([Math]::Round($Improvement, 2))% worse)" "Yellow" } elseif ($Improvement -lt 5) { - Write-ColorOutput " • Pool size $($SummaryData[$i].PoolSize): Diminishing returns (<5% improvement)" "Yellow" + Write-ColorOutput " • Pool size $($SortedData[$i].PoolSize): Diminishing returns (<5% improvement)" "Yellow" } } diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh index 096640a..7dce941 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh @@ -131,24 +131,24 @@ echo -e "${CYAN}═════════════════════ # Create summary CSV for latency vs pool size cat > "./output/pool_size_comparison/latency_vs_pool_size.csv" << 'EOF' -Pool Size,Average Latency (ms),Min Latency (ms),Max Latency (ms) +Pool Size,Median Latency (ms),Min Latency (ms),Max Latency (ms) EOF for POOL_SIZE in "${POOL_SIZES[@]}"; do CSV_FILE="./output/pool_size_comparison/pool_${POOL_SIZE}/simulation_latency.csv" if [ -f "$CSV_FILE" ]; then - # Calculate statistics using awk - STATS=$(awk -F, 'NR>1 { - sum+=$2; - count++; + # Calculate median and min/max using sort for portability + MIN_MAX=$(awk -F, 'NR>1 { if(NR==2 || $2max) max=$2; } END { - printf "%.2f,%.2f,%.2f", sum/count, min, max; + printf "%.2f,%.2f", min, max; }' "$CSV_FILE") - echo "${POOL_SIZE},${STATS}" >> "./output/pool_size_comparison/latency_vs_pool_size.csv" + MEDIAN=$(awk -F, 'NR>1 {print $2}' "$CSV_FILE" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') + + echo "${POOL_SIZE},${MEDIAN},${MIN_MAX}" >> "./output/pool_size_comparison/latency_vs_pool_size.csv" fi done @@ -156,14 +156,14 @@ echo -e "${GREEN}✓ Created latency_vs_pool_size.csv${NC}" # Create simplified summary for bar charts cat > "./output/pool_size_comparison/latency_summary.csv" << 'EOF' -Pool Size,Average Latency (ms) +Pool Size,Median Latency (ms) EOF for POOL_SIZE in "${POOL_SIZES[@]}"; do CSV_FILE="./output/pool_size_comparison/pool_${POOL_SIZE}/simulation_latency.csv" if [ -f "$CSV_FILE" ]; then - AVG=$(awk -F, 'NR>1 {sum+=$2; count++} END {printf "%.2f", sum/count}' "$CSV_FILE") - echo "${POOL_SIZE},${AVG}" >> "./output/pool_size_comparison/latency_summary.csv" + MEDIAN=$(awk -F, 'NR>1 {print $2}' "$CSV_FILE" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') + echo "${POOL_SIZE},${MEDIAN}" >> "./output/pool_size_comparison/latency_summary.csv" fi done @@ -177,9 +177,9 @@ if ! command -v graph &> /dev/null; then else echo -e "${BLUE}Generating comparison charts...${NC}" - # Generate line chart: Average Latency vs Pool Size + # Generate line chart: Median Latency vs Pool Size graph "./output/pool_size_comparison/latency_summary.csv" \ - --title "Average Latency vs Pool Size (Session Pooling)" \ + --title "Median Latency vs Pool Size (Session Pooling)" \ --xlabel "Pool Size" \ --ylabel "Latency (ms)" \ -o "./output/pool_size_comparison/latency_vs_pool_size.png" @@ -187,7 +187,7 @@ else # Generate bar chart for easier reading (full scale from 0) graph "./output/pool_size_comparison/latency_summary.csv" \ - --title "Average Latency vs Pool Size (Session Pooling)" \ + --title "Median Latency vs Pool Size (Session Pooling)" \ --xlabel "Pool Size" \ --ylabel "Latency (ms)" \ --bar \ @@ -202,7 +202,7 @@ else graph "./output/pool_size_comparison/latency_summary.csv" \ --bar \ - --title "Average Latency vs Pool Size (Zoomed)" \ + --title "Median Latency vs Pool Size (Zoomed)" \ --xlabel "Pool Size" \ --ylabel "Latency (ms)" \ --yrange=$RANGE_MIN:$RANGE_MAX \ @@ -230,24 +230,26 @@ echo -e "${CYAN}═════════════════════ echo -e "${CYAN}Summary: Latency vs Pool Size${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" -printf "%-12s %-15s %-15s %-15s\n" "Pool Size" "Avg Latency" "Min Latency" "Max Latency" +printf "%-12s %-15s %-15s %-15s\n" "Pool Size" "Median (P50)" "Min Latency" "Max Latency" echo "────────────────────────────────────────────────────────────────────" for POOL_SIZE in "${POOL_SIZES[@]}"; do CSV_FILE="./output/pool_size_comparison/pool_${POOL_SIZE}/simulation_latency.csv" if [ -f "$CSV_FILE" ]; then - STATS=$(awk -F, 'NR>1 { - sum+=$2; - count++; + # Calculate min/max + MIN_MAX=$(awk -F, 'NR>1 { if(NR==2 || $2max) max=$2 } END { - printf "%.2f %.2f %.2f", sum/count, min, max + printf "%.2f %.2f", min, max }' "$CSV_FILE") - read AVG MIN MAX <<< "$STATS" - printf "%-12s %10.2f ms %10.2f ms %10.2f ms\n" "$POOL_SIZE" "$AVG" "$MIN" "$MAX" + # Calculate median using sort for portability + MEDIAN=$(awk -F, 'NR>1 {print $2}' "$CSV_FILE" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') + + read MIN MAX <<< "$MIN_MAX" + printf "%-12s %10.2f ms %10.2f ms %10.2f ms\n" "$POOL_SIZE" "$MEDIAN" "$MIN" "$MAX" fi done @@ -272,12 +274,23 @@ OPTIMAL_SIZE=$(awk -F, -v minlat="$MIN_LATENCY" 'NR>1 { OPTIMAL_LATENCY=$(awk -F, -v size="$OPTIMAL_SIZE" 'NR>1 && $1==size {printf "%.2f", $2}' "./output/pool_size_comparison/latency_summary.csv") -echo -e " • ${GREEN}Optimal pool size: ${OPTIMAL_SIZE} (avg latency: ${OPTIMAL_LATENCY}ms)${NC}" +echo -e " • ${GREEN}Optimal pool size: ${OPTIMAL_SIZE} (median latency: ${OPTIMAL_LATENCY}ms)${NC}" echo -e " • ${CYAN}Note: Smallest pool size achieving near-optimal performance (≤1% of minimum)${NC}" # Check for diminishing returns (when improvement < 5%) +# Sort pool sizes numerically for meaningful comparison +IFS=$'\n' SORTED_POOL_SIZES=($(printf '%s\n' "${POOL_SIZES[@]}" | sort -n)) +unset IFS + LAST_AVG="" -for POOL_SIZE in "${POOL_SIZES[@]}"; do +for POOL_SIZE in "${SORTED_POOL_SIZES[@]}"; do + # Skip the optimal pool size (already highlighted above) + if [ "$POOL_SIZE" -eq "$OPTIMAL_SIZE" ]; then + CURRENT_AVG=$(awk -F, -v size="$POOL_SIZE" 'NR>1 && $1==size {print $2}' "./output/pool_size_comparison/latency_summary.csv") + LAST_AVG=$CURRENT_AVG + continue + fi + CURRENT_AVG=$(awk -F, -v size="$POOL_SIZE" 'NR>1 && $1==size {print $2}' "./output/pool_size_comparison/latency_summary.csv") if [ -n "$LAST_AVG" ]; then # Calculate improvement percentage using awk (no bc dependency) @@ -299,7 +312,7 @@ done OVERALL_ELAPSED=$(($SECONDS - $OVERALL_START)) echo "" echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}" -echo -e "${GREEN}║ Pool Size Comparison Complete! (Total: ${OVERALL_ELAPSED}s) ║${NC}" +echo -e "${GREEN}║ Pool Size Comparison Complete! (Total: ${OVERALL_ELAPSED}s) ║${NC}" echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}" echo "" echo -e "Results saved in: ${BLUE}./output/pool_size_comparison/${NC}" diff --git a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs index f152bae..1c937d5 100644 --- a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs +++ b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs @@ -88,9 +88,9 @@ public static void RunDemo( ConnectionPool? pool = poolMode != PoolingMode.Direct ? new ConnectionPool(poolSize) : null; // Configure Generator with PostgreSQL query creation logic - // High traffic: 20 req/sec (0.05s inter-arrival) to test connection overhead impact + // High traffic: 50 req/sec (0.02s inter-arrival) to test connection overhead impact Func interArrivalFunc = (rnd) => - TimeSpan.FromSeconds(-0.05 * Math.Log(1.0 - rnd.NextDouble())); + TimeSpan.FromSeconds(-0.02 * Math.Log(1.0 - rnd.NextDouble())); Func createLoad = (rnd) => { From 11e12620ca7d58817bb223994ee25cfb3e2cfa5e Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 07:41:48 +0800 Subject: [PATCH 23/32] refactor(pool-size-comparison): update threshold for optimal pool size to 5% of minimum latency for consistency --- SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 | 7 ++++--- SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 index ab9d056..383a793 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 @@ -249,15 +249,16 @@ Write-ColorOutput "Recommendations:" "Blue" # Find minimum latency $MinLatency = ($SummaryData | Measure-Object -Property MedianLatency -Minimum).Minimum -# Find smallest pool size that achieves near-optimal performance (within 1% of minimum) -$Threshold = $MinLatency * 1.01 +# Find smallest pool size that achieves near-optimal performance (within 5% of minimum) +# Uses same 5% threshold as "diminishing returns" for consistency +$Threshold = $MinLatency * 1.05 $OptimalStats = $SummaryData | Where-Object { $_.MedianLatency -le $Threshold } | Sort-Object PoolSize | Select-Object -First 1 Write-ColorOutput " • Optimal pool size: $($OptimalStats.PoolSize) (median latency: $($OptimalStats.MedianLatency)ms)" "Green" -Write-ColorOutput " • Note: Smallest pool size achieving near-optimal performance (≤1% of minimum)" "Cyan" +Write-ColorOutput " • Note: Smallest pool size achieving near-optimal performance (≤5% of minimum)" "Cyan" # Check for diminishing returns (when improvement < 5%) # Sort pool sizes numerically for meaningful comparison diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh index 7dce941..7188055 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh @@ -259,9 +259,10 @@ echo -e "${BLUE}Recommendations:${NC}" # Find minimum latency MIN_LATENCY=$(awk -F, 'NR>1 {if(NR==2 || $21 { - threshold = minlat * 1.01; + threshold = minlat * 1.05; if ($2 <= threshold) { if (optimal == "" || $1 < optimal) { optimal = $1; @@ -275,7 +276,7 @@ OPTIMAL_SIZE=$(awk -F, -v minlat="$MIN_LATENCY" 'NR>1 { OPTIMAL_LATENCY=$(awk -F, -v size="$OPTIMAL_SIZE" 'NR>1 && $1==size {printf "%.2f", $2}' "./output/pool_size_comparison/latency_summary.csv") echo -e " • ${GREEN}Optimal pool size: ${OPTIMAL_SIZE} (median latency: ${OPTIMAL_LATENCY}ms)${NC}" -echo -e " • ${CYAN}Note: Smallest pool size achieving near-optimal performance (≤1% of minimum)${NC}" +echo -e " • ${CYAN}Note: Smallest pool size achieving near-optimal performance (≤5% of minimum)${NC}" # Check for diminishing returns (when improvement < 5%) # Sort pool sizes numerically for meaningful comparison From e0519792da6cfca9d1e021273f1c9c7a182583e8 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 07:41:58 +0800 Subject: [PATCH 24/32] fix(pooling-comparison): ensure latency and credits values are treated as doubles for accurate maximum calculations --- SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 index b85bd38..f67ec18 100644 --- a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 @@ -197,7 +197,7 @@ if (-not (Get-Command graph -ErrorAction SilentlyContinue)) { $latencyFile = "$modeDir/simulation_latency.csv" if (Test-Path $latencyFile) { $latency = Import-Csv $latencyFile - $latencyMax = ($latency | ForEach-Object { $_."Latency (ms)" } | Measure-Object -Maximum).Maximum + $latencyMax = ($latency | ForEach-Object { [double]$_."Latency (ms)" } | Measure-Object -Maximum).Maximum if ($null -eq $latencyMax) { $latencyMax = 1 } graph $latencyFile --title "Latency - $mode" --color $color --yrange="0:$latencyMax" -o "$modeDir/latency.png" @@ -208,7 +208,7 @@ if (-not (Get-Command graph -ErrorAction SilentlyContinue)) { $creditsFile = "$modeDir/simulation_credits.csv" if (Test-Path $creditsFile) { $credits = Import-Csv $creditsFile - $creditMax = ($credits | Measure-Object -Property "Credits" -Maximum).Maximum + $creditMax = ($credits | ForEach-Object { [double]$_.Credits } | Measure-Object -Maximum).Maximum if ($null -eq $creditMax) { $creditMax = 1 } graph $creditsFile --title "Credits - $mode" --color $color --yrange="0:$creditMax" -o "$modeDir/credits.png" From 2de54b9e8332c1be6d3415c8ce652f018b309eca Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 07:44:35 +0800 Subject: [PATCH 25/32] refactor(pooling-simulation): update comments to clarify spillover model behavior in connection acquisition --- SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs index 1c937d5..73971dc 100644 --- a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs +++ b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs @@ -95,8 +95,8 @@ public static void RunDemo( Func createLoad = (rnd) => { // DEFERRED ACQUISITION: Connection will be acquired when service starts, - // not at load creation. This matches real PgBouncer behavior where - // requests queue for connections. + // not at load creation. If the pool is exhausted, the simulation uses a + // spillover model (opens direct connection) rather than queuing/blocking. var query = new PostgresQuery { PoolMode = poolMode, From 83071ee13f816421b47f72c0488a96f7785d07b4 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 07:56:04 +0800 Subject: [PATCH 26/32] refactor(scenarios): update log messages to clarify CSV output location --- SimNextgenApp.Demo/Scenarios/AwsRdsBurstScenario.cs | 2 +- SimNextgenApp.Demo/Scenarios/AzureDbBurstScenario.cs | 2 +- SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SimNextgenApp.Demo/Scenarios/AwsRdsBurstScenario.cs b/SimNextgenApp.Demo/Scenarios/AwsRdsBurstScenario.cs index 35abdf1..ba3f021 100644 --- a/SimNextgenApp.Demo/Scenarios/AwsRdsBurstScenario.cs +++ b/SimNextgenApp.Demo/Scenarios/AwsRdsBurstScenario.cs @@ -172,7 +172,7 @@ public static void RunDemo( rdsBehavior.SetContext(engine); // ========================================================= - programLogger.LogInformation("Starting Simulation. Watch console for CSV output..."); + programLogger.LogInformation("Starting Simulation. CSV results will be written to ./output/ directory..."); // Observe the simulation if telemetry is enabled SimulationObserver? simObserver = null; diff --git a/SimNextgenApp.Demo/Scenarios/AzureDbBurstScenario.cs b/SimNextgenApp.Demo/Scenarios/AzureDbBurstScenario.cs index 475f2aa..83002d1 100644 --- a/SimNextgenApp.Demo/Scenarios/AzureDbBurstScenario.cs +++ b/SimNextgenApp.Demo/Scenarios/AzureDbBurstScenario.cs @@ -172,7 +172,7 @@ public static void RunDemo( dbBehavior.SetContext(engine); // ========================================================= - programLogger.LogInformation("Starting Simulation. Watch console for CSV output..."); + programLogger.LogInformation("Starting Simulation. CSV results will be written to ./output/ directory..."); // Observe the simulation if telemetry is enabled SimulationObserver? simObserver = null; diff --git a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs index 73971dc..d9089fe 100644 --- a/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs +++ b/SimNextgenApp.Demo/Scenarios/AzurePgsqlPoolingScenario.cs @@ -231,7 +231,7 @@ public static void RunDemo( } } - programLogger.LogInformation("Starting Simulation. Watch console for CSV output..."); + programLogger.LogInformation("Starting Simulation. CSV results will be written to ./output/ directory..."); // Observe the simulation if telemetry is enabled SimulationObserver? simObserver = null; From 1514f296a1efbdafc3c34a3265715d864e35b14a Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 08:04:41 +0800 Subject: [PATCH 27/32] refactor(pooling-comparison): update scripts to generate median latency comparison charts and simplify output handling --- .../AzureDbSample/AzureDbInstanceSpec.cs | 10 +- .../AzureDbSample/pooling-comparison.ps1 | 106 ++---------------- .../AzureDbSample/pooling-comparison.sh | 95 ++-------------- 3 files changed, 21 insertions(+), 190 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/AzureDbInstanceSpec.cs b/SimNextgenApp.Demo/AzureDbSample/AzureDbInstanceSpec.cs index d6542d8..36986f5 100644 --- a/SimNextgenApp.Demo/AzureDbSample/AzureDbInstanceSpec.cs +++ b/SimNextgenApp.Demo/AzureDbSample/AzureDbInstanceSpec.cs @@ -64,11 +64,11 @@ internal static class AzureDbRegistry // B-series - Burstable (20% baseline for all) // Azure starts with ~30 credits per core (initial bank for boot-up) // Format: Series, Size, VCores, FastSecs, EarnRatePerHour, MaxCredits, BaselineFraction - new BurstableInstanceSpec("B", "1ms", 1, 0.050, 12, 288, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.250 - new BurstableInstanceSpec("B", "2s", 2, 0.045, 24, 576, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.225 - new BurstableInstanceSpec("B", "2ms", 2, 0.045, 24, 576, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.225 - new BurstableInstanceSpec("B", "4ms", 4, 0.040, 48, 1152, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.200 - new BurstableInstanceSpec("B", "8ms", 8, 0.035, 96, 2304, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.175 + new BurstableInstanceSpec("B", "1ms", 1, 0.080, 12, 288, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.250 + new BurstableInstanceSpec("B", "2s", 2, 0.080, 24, 576, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.225 + new BurstableInstanceSpec("B", "2ms", 2, 0.080, 24, 576, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.225 + new BurstableInstanceSpec("B", "4ms", 4, 0.080, 48, 1152, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.200 + new BurstableInstanceSpec("B", "8ms", 8, 0.080, 96, 2304, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.175 // Future: D-series (General Purpose), E-series (Memory Optimized) }; diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 index f67ec18..c695b4b 100644 --- a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 @@ -114,116 +114,25 @@ foreach ($modeInfo in $modes) { } } -# Merge CSVs and generate comparison graphs +# Generate comparison graphs Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan -Write-Host "Merging Results & Generating Comparison Graphs" -ForegroundColor Cyan +Write-Host "Generating Median Latency Comparison Charts" -ForegroundColor Cyan Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan -# Merge latency CSVs -$latencyDirect = "./output/pooling_comparison/direct/simulation_latency.csv" -$latencySession = "./output/pooling_comparison/session/simulation_latency.csv" -$latencyTransaction = "./output/pooling_comparison/transaction/simulation_latency.csv" - -if ((Test-Path $latencyDirect) -and (Test-Path $latencySession) -and (Test-Path $latencyTransaction)) { - Write-Host "Merging latency data..." -ForegroundColor Blue - - # Load all three CSV files - $directData = Import-Csv $latencyDirect - $sessionData = Import-Csv $latencySession - $transactionData = Import-Csv $latencyTransaction - - # Merge by row index (nth query) instead of timestamp to avoid data loss - # Event times differ across modes due to different service times, so timestamp-based - # merging would drop most rows. Row-index merge preserves all data points. - $mergedLatency = @() - $mergedLatency += "Query Index,Direct Time (s),Direct (ms),Session Time (s),Session (ms),Transaction Time (s),Transaction (ms)" - - $maxCount = [Math]::Min($directData.Count, [Math]::Min($sessionData.Count, $transactionData.Count)) - for ($i = 0; $i -lt $maxCount; $i++) { - $mergedLatency += "$($i+1),$($directData[$i].'Simulation Time (s)'),$($directData[$i].'Latency (ms)'),$($sessionData[$i].'Simulation Time (s)'),$($sessionData[$i].'Latency (ms)'),$($transactionData[$i].'Simulation Time (s)'),$($transactionData[$i].'Latency (ms)')" - } - - $mergedLatency | Out-File "./output/pooling_comparison/latency_combined.csv" -Encoding UTF8 - Write-Host "✓ Created latency_combined.csv (merged by query index)" -ForegroundColor Green -} - -# Merge credits CSVs -$creditsDirect = "./output/pooling_comparison/direct/simulation_credits.csv" -$creditsSession = "./output/pooling_comparison/session/simulation_credits.csv" -$creditsTransaction = "./output/pooling_comparison/transaction/simulation_credits.csv" - -if ((Test-Path $creditsDirect) -and (Test-Path $creditsSession) -and (Test-Path $creditsTransaction)) { - Write-Host "Merging credits data..." -ForegroundColor Blue - - # Load all three CSV files - $directData = Import-Csv $creditsDirect - $sessionData = Import-Csv $creditsSession - $transactionData = Import-Csv $creditsTransaction - - # Merge by row index (nth query) instead of timestamp to avoid data loss - $mergedCredits = @() - $mergedCredits += "Query Index,Direct Time (s),Direct,Session Time (s),Session,Transaction Time (s),Transaction" - - $maxCount = [Math]::Min($directData.Count, [Math]::Min($sessionData.Count, $transactionData.Count)) - for ($i = 0; $i -lt $maxCount; $i++) { - $mergedCredits += "$($i+1),$($directData[$i].'Simulation Time (s)'),$($directData[$i].'Credits'),$($sessionData[$i].'Simulation Time (s)'),$($sessionData[$i].'Credits'),$($transactionData[$i].'Simulation Time (s)'),$($transactionData[$i].'Credits')" - } - - $mergedCredits | Out-File "./output/pooling_comparison/credits_combined.csv" -Encoding UTF8 - Write-Host "✓ Created credits_combined.csv (merged by query index)" -ForegroundColor Green -} - -# Generate individual graphs for PowerPoint overlay (with distinct colors!) +# Check if graph-cli is available if (-not (Get-Command graph -ErrorAction SilentlyContinue)) { Write-Host "graph-cli not found. Install with: pip install graph-cli" -ForegroundColor Yellow Write-Host "Skipping graph generation" -ForegroundColor Yellow - Write-Host "(Merged CSV files are still available for manual plotting)" -ForegroundColor Yellow + Write-Host "(CSV files are still available in ./output/pooling_comparison/)" -ForegroundColor Yellow } else { - Write-Host "Generating individual charts (with distinct colors for overlay)..." -ForegroundColor Blue - - # Generate individual latency and credits graphs with distinct colors - foreach ($modeInfo in $modes) { - $mode = $modeInfo.Name - $modeDir = "./output/pooling_comparison/$mode" - - # Set color based on mode - $color = switch ($mode) { - "direct" { "red" } # Red = worst (highest overhead) - "session" { "green" } # Green = best (no overhead) - "transaction" { "orange" } # Orange = middle (8ms overhead) - } - - # Generate latency graph - $latencyFile = "$modeDir/simulation_latency.csv" - if (Test-Path $latencyFile) { - $latency = Import-Csv $latencyFile - $latencyMax = ($latency | ForEach-Object { [double]$_."Latency (ms)" } | Measure-Object -Maximum).Maximum - if ($null -eq $latencyMax) { $latencyMax = 1 } - - graph $latencyFile --title "Latency - $mode" --color $color --yrange="0:$latencyMax" -o "$modeDir/latency.png" - Write-Host "✓ Generated $mode latency graph ($color)" -ForegroundColor Green - } - - # Generate credits graph - $creditsFile = "$modeDir/simulation_credits.csv" - if (Test-Path $creditsFile) { - $credits = Import-Csv $creditsFile - $creditMax = ($credits | ForEach-Object { [double]$_.Credits } | Measure-Object -Maximum).Maximum - if ($null -eq $creditMax) { $creditMax = 1 } - - graph $creditsFile --title "Credits - $mode" --color $color --yrange="0:$creditMax" -o "$modeDir/credits.png" - Write-Host "✓ Generated $mode credits graph ($color)" -ForegroundColor Green - } - } - - # Generate summary bar charts - Write-Host "Generating summary bar charts..." -ForegroundColor Blue + Write-Host "Generating median latency bar charts..." -ForegroundColor Blue - # Calculate median latencies (more robust than mean for latency comparisons) + # Load simulation data $directData = Import-Csv "./output/pooling_comparison/direct/simulation_latency.csv" $sessionData = Import-Csv "./output/pooling_comparison/session/simulation_latency.csv" $transactionData = Import-Csv "./output/pooling_comparison/transaction/simulation_latency.csv" + # Helper function to calculate median function Get-Median { param([double[]]$values) $sorted = $values | Sort-Object @@ -237,6 +146,7 @@ if (-not (Get-Command graph -ErrorAction SilentlyContinue)) { } } + # Calculate median latencies $directMedian = Get-Median ($directData | ForEach-Object { [double]$_."Latency (ms)" }) $sessionMedian = Get-Median ($sessionData | ForEach-Object { [double]$_."Latency (ms)" }) $transactionMedian = Get-Median ($transactionData | ForEach-Object { [double]$_."Latency (ms)" }) diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh index 410a04e..66690ce 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh @@ -121,101 +121,22 @@ run_mode() { } # Run all three modes -run_mode "direct" "Direct Connections (50ms overhead)" -run_mode "session" "Session Pooling (no overhead)" -run_mode "transaction" "Transaction Pooling (8ms overhead)" +run_mode "direct" "Direct Connections" +run_mode "session" "Session Pooling" +run_mode "transaction" "Transaction Pooling" -# Merge CSVs and generate comparison graphs +# Generate comparison graphs echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" -echo -e "${CYAN}Merging Results & Generating Comparison Graphs${NC}" +echo -e "${CYAN}Generating Median Latency Comparison Charts${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" -# Merge latency CSVs -LATENCY_DIRECT="./output/pooling_comparison/direct/simulation_latency.csv" -LATENCY_SESSION="./output/pooling_comparison/session/simulation_latency.csv" -LATENCY_TRANSACTION="./output/pooling_comparison/transaction/simulation_latency.csv" - -if [ -f "$LATENCY_DIRECT" ] && [ -f "$LATENCY_SESSION" ] && [ -f "$LATENCY_TRANSACTION" ]; then - echo -e "${BLUE}Merging latency data...${NC}" - - # Merge by row index (nth query) instead of timestamp to avoid data loss - # Event times differ across modes due to different service times, so timestamp-based - # joins would drop most rows. Row-index merge preserves all data points. - { - echo "Query Index,Direct Time (s),Direct (ms),Session Time (s),Session (ms),Transaction Time (s),Transaction (ms)" - - paste -d, \ - <(tail -n +2 "$LATENCY_DIRECT" | awk -F, '{print NR "," $1 "," $2}') \ - <(tail -n +2 "$LATENCY_SESSION" | awk -F, '{print $1 "," $2}') \ - <(tail -n +2 "$LATENCY_TRANSACTION" | awk -F, '{print $1 "," $2}') - } > "./output/pooling_comparison/latency_combined.csv" - - echo -e "${GREEN}✓ Created latency_combined.csv (merged by query index)${NC}" -fi - -# Merge credits CSVs -CREDITS_DIRECT="./output/pooling_comparison/direct/simulation_credits.csv" -CREDITS_SESSION="./output/pooling_comparison/session/simulation_credits.csv" -CREDITS_TRANSACTION="./output/pooling_comparison/transaction/simulation_credits.csv" - -if [ -f "$CREDITS_DIRECT" ] && [ -f "$CREDITS_SESSION" ] && [ -f "$CREDITS_TRANSACTION" ]; then - echo -e "${BLUE}Merging credits data...${NC}" - - # Merge by row index (nth query) instead of timestamp to avoid data loss - { - echo "Query Index,Direct Time (s),Direct,Session Time (s),Session,Transaction Time (s),Transaction" - - paste -d, \ - <(tail -n +2 "$CREDITS_DIRECT" | awk -F, '{print NR "," $1 "," $2}') \ - <(tail -n +2 "$CREDITS_SESSION" | awk -F, '{print $1 "," $2}') \ - <(tail -n +2 "$CREDITS_TRANSACTION" | awk -F, '{print $1 "," $2}') - } > "./output/pooling_comparison/credits_combined.csv" - - echo -e "${GREEN}✓ Created credits_combined.csv (merged by query index)${NC}" -fi - -# Generate individual graphs for PowerPoint overlay (with distinct colors!) +# Check if graph-cli is available if ! command -v graph &> /dev/null; then echo -e "${YELLOW}graph-cli not found. Install with: pip install graph-cli${NC}" echo -e "${YELLOW}Skipping graph generation${NC}" - echo -e "${YELLOW}(Merged CSV files are still available for manual plotting)${NC}" + echo -e "${YELLOW}(CSV files are still available in ./output/pooling_comparison/)${NC}" else - echo -e "${BLUE}Generating individual charts (with distinct colors for overlay)...${NC}" - - # Generate individual latency graphs with distinct colors - for MODE in "${MODES[@]}"; do - MODE_DIR="./output/pooling_comparison/${MODE}" - - # Set color based on mode - case $MODE in - direct) COLOR="red" ;; # Red = worst (highest overhead) - session) COLOR="green" ;; # Green = best (no overhead) - transaction) COLOR="orange" ;; # Orange = middle (8ms overhead) - esac - - if [ -f "${MODE_DIR}/simulation_latency.csv" ]; then - LATENCY_MAX=$(awk -F, 'BEGIN {max=0} NR>1 {if($2>max) max=$2} END {print (max==0?1:max)}' "${MODE_DIR}/simulation_latency.csv") - graph "${MODE_DIR}/simulation_latency.csv" \ - --title "Latency - ${MODE}" \ - --color "$COLOR" \ - --yrange=0:$LATENCY_MAX \ - -o "${MODE_DIR}/latency.png" - echo -e "${GREEN}✓ Generated ${MODE} latency graph (${COLOR})${NC}" - fi - - if [ -f "${MODE_DIR}/simulation_credits.csv" ]; then - CREDIT_MAX=$(awk -F, 'BEGIN {max=0} NR>1 {if($2>max) max=$2} END {print (max==0?1:max)}' "${MODE_DIR}/simulation_credits.csv") - graph "${MODE_DIR}/simulation_credits.csv" \ - --title "Credits - ${MODE}" \ - --color "$COLOR" \ - --yrange=0:$CREDIT_MAX \ - -o "${MODE_DIR}/credits.png" - echo -e "${GREEN}✓ Generated ${MODE} credits graph (${COLOR})${NC}" - fi - done - - # Generate summary bar charts - echo -e "${BLUE}Generating summary bar charts...${NC}" + echo -e "${BLUE}Generating median latency bar charts...${NC}" # Calculate median latencies # Use sort for portability From 92d815600ba5bacea4de732c650f5e11bae578a8 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 08:12:36 +0800 Subject: [PATCH 28/32] refactor(azure-db-registry): remove redundant comments from BurstableInstanceSpec instances --- .../AzureDbSample/AzureDbInstanceSpec.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/AzureDbInstanceSpec.cs b/SimNextgenApp.Demo/AzureDbSample/AzureDbInstanceSpec.cs index 36986f5..23f12f4 100644 --- a/SimNextgenApp.Demo/AzureDbSample/AzureDbInstanceSpec.cs +++ b/SimNextgenApp.Demo/AzureDbSample/AzureDbInstanceSpec.cs @@ -64,11 +64,11 @@ internal static class AzureDbRegistry // B-series - Burstable (20% baseline for all) // Azure starts with ~30 credits per core (initial bank for boot-up) // Format: Series, Size, VCores, FastSecs, EarnRatePerHour, MaxCredits, BaselineFraction - new BurstableInstanceSpec("B", "1ms", 1, 0.080, 12, 288, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.250 - new BurstableInstanceSpec("B", "2s", 2, 0.080, 24, 576, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.225 - new BurstableInstanceSpec("B", "2ms", 2, 0.080, 24, 576, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.225 - new BurstableInstanceSpec("B", "4ms", 4, 0.080, 48, 1152, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.200 - new BurstableInstanceSpec("B", "8ms", 8, 0.080, 96, 2304, 0.20), // 0.20 = 20% baseline → SlowSecs = 0.175 + new BurstableInstanceSpec("B", "1ms", 1, 0.080, 12, 288, 0.20), + new BurstableInstanceSpec("B", "2s", 2, 0.080, 24, 576, 0.20), + new BurstableInstanceSpec("B", "2ms", 2, 0.080, 24, 576, 0.20), + new BurstableInstanceSpec("B", "4ms", 4, 0.080, 48, 1152, 0.20), + new BurstableInstanceSpec("B", "8ms", 8, 0.080, 96, 2304, 0.20), // Future: D-series (General Purpose), E-series (Memory Optimized) }; From 859324a31f5b03b4bc359cf6fe8c587c7034bdf6 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 09:10:39 +0800 Subject: [PATCH 29/32] refactor(azure-db-registry): enhance comments for BurstableInstanceSpec to clarify performance characteristics --- .../AzureDbSample/AzureDbInstanceSpec.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/AzureDbInstanceSpec.cs b/SimNextgenApp.Demo/AzureDbSample/AzureDbInstanceSpec.cs index 23f12f4..0ea44f2 100644 --- a/SimNextgenApp.Demo/AzureDbSample/AzureDbInstanceSpec.cs +++ b/SimNextgenApp.Demo/AzureDbSample/AzureDbInstanceSpec.cs @@ -64,11 +64,25 @@ internal static class AzureDbRegistry // B-series - Burstable (20% baseline for all) // Azure starts with ~30 credits per core (initial bank for boot-up) // Format: Series, Size, VCores, FastSecs, EarnRatePerHour, MaxCredits, BaselineFraction + + // IMPORTANT: FastSecs is identical across all B-series sizes (0.080s) because a single-threaded + // query runs at the same speed regardless of vCore count - each vCore has identical performance. + // Size-based performance differentiation comes from CONCURRENCY CAPACITY (VCores), not query speed. + // + // Example throughput at 50 req/sec arrival rate: + // B.1ms: 1 vCore × 12.5 qps/core = 12.5 qps capacity -> 400% utilization (oversaturated) + // B.2ms: 2 vCores × 12.5 qps/core = 25 qps capacity -> 200% utilization (saturated) + // B.4ms: 4 vCores × 12.5 qps/core = 50 qps capacity -> 100% utilization (critical point) + // B.8ms: 8 vCores × 12.5 qps/core = 100 qps capacity -> 50% utilization (comfortable) + // + // This models reality: larger instances win through PARALLELISM, not faster individual queries. + // See: AzurePgsqlPoolingScenario.cs (numberOfServers: VCores) + new BurstableInstanceSpec("B", "1ms", 1, 0.080, 12, 288, 0.20), new BurstableInstanceSpec("B", "2s", 2, 0.080, 24, 576, 0.20), new BurstableInstanceSpec("B", "2ms", 2, 0.080, 24, 576, 0.20), new BurstableInstanceSpec("B", "4ms", 4, 0.080, 48, 1152, 0.20), - new BurstableInstanceSpec("B", "8ms", 8, 0.080, 96, 2304, 0.20), + new BurstableInstanceSpec("B", "8ms", 8, 0.080, 96, 2304, 0.20), // Future: D-series (General Purpose), E-series (Memory Optimized) }; From f1a755e6481b832999f7cbb3f2f291a89d01b2d7 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 09:12:59 +0800 Subject: [PATCH 30/32] refactor(pooling-comparison): check for required CSV files before generating graphs --- .../AzureDbSample/pooling-comparison.ps1 | 20 ++++++++++++++----- .../AzureDbSample/pooling-comparison.sh | 20 ++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 index c695b4b..ea22ece 100644 --- a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.ps1 @@ -119,8 +119,18 @@ Write-Host "══════════════════════ Write-Host "Generating Median Latency Comparison Charts" -ForegroundColor Cyan Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan -# Check if graph-cli is available -if (-not (Get-Command graph -ErrorAction SilentlyContinue)) { +# Check if required CSV files exist before generating graphs +$latencyDirect = "./output/pooling_comparison/direct/simulation_latency.csv" +$latencySession = "./output/pooling_comparison/session/simulation_latency.csv" +$latencyTransaction = "./output/pooling_comparison/transaction/simulation_latency.csv" + +if ((-not (Test-Path $latencyDirect)) -or (-not (Test-Path $latencySession)) -or (-not (Test-Path $latencyTransaction))) { + Write-Host "Skipping graph generation: one or more simulation CSVs missing" -ForegroundColor Yellow + Write-Host "Expected files:" -ForegroundColor Yellow + Write-Host " - $latencyDirect" -ForegroundColor Yellow + Write-Host " - $latencySession" -ForegroundColor Yellow + Write-Host " - $latencyTransaction" -ForegroundColor Yellow +} elseif (-not (Get-Command graph -ErrorAction SilentlyContinue)) { Write-Host "graph-cli not found. Install with: pip install graph-cli" -ForegroundColor Yellow Write-Host "Skipping graph generation" -ForegroundColor Yellow Write-Host "(CSV files are still available in ./output/pooling_comparison/)" -ForegroundColor Yellow @@ -128,9 +138,9 @@ if (-not (Get-Command graph -ErrorAction SilentlyContinue)) { Write-Host "Generating median latency bar charts..." -ForegroundColor Blue # Load simulation data - $directData = Import-Csv "./output/pooling_comparison/direct/simulation_latency.csv" - $sessionData = Import-Csv "./output/pooling_comparison/session/simulation_latency.csv" - $transactionData = Import-Csv "./output/pooling_comparison/transaction/simulation_latency.csv" + $directData = Import-Csv $latencyDirect + $sessionData = Import-Csv $latencySession + $transactionData = Import-Csv $latencyTransaction # Helper function to calculate median function Get-Median { diff --git a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh index 66690ce..0c57588 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pooling-comparison.sh @@ -130,8 +130,18 @@ echo -e "${CYAN}═════════════════════ echo -e "${CYAN}Generating Median Latency Comparison Charts${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" -# Check if graph-cli is available -if ! command -v graph &> /dev/null; then +# Check if required CSV files exist before generating graphs +LATENCY_DIRECT="./output/pooling_comparison/direct/simulation_latency.csv" +LATENCY_SESSION="./output/pooling_comparison/session/simulation_latency.csv" +LATENCY_TRANSACTION="./output/pooling_comparison/transaction/simulation_latency.csv" + +if [ ! -f "$LATENCY_DIRECT" ] || [ ! -f "$LATENCY_SESSION" ] || [ ! -f "$LATENCY_TRANSACTION" ]; then + echo -e "${YELLOW}Skipping graph generation: one or more simulation CSVs missing${NC}" + echo -e "${YELLOW}Expected files:${NC}" + echo -e "${YELLOW} - $LATENCY_DIRECT${NC}" + echo -e "${YELLOW} - $LATENCY_SESSION${NC}" + echo -e "${YELLOW} - $LATENCY_TRANSACTION${NC}" +elif ! command -v graph &> /dev/null; then echo -e "${YELLOW}graph-cli not found. Install with: pip install graph-cli${NC}" echo -e "${YELLOW}Skipping graph generation${NC}" echo -e "${YELLOW}(CSV files are still available in ./output/pooling_comparison/)${NC}" @@ -140,9 +150,9 @@ else # Calculate median latencies # Use sort for portability - DIRECT_MEDIAN=$(awk -F, 'NR>1 {print $2}' "./output/pooling_comparison/direct/simulation_latency.csv" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') - SESSION_MEDIAN=$(awk -F, 'NR>1 {print $2}' "./output/pooling_comparison/session/simulation_latency.csv" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') - TRANSACTION_MEDIAN=$(awk -F, 'NR>1 {print $2}' "./output/pooling_comparison/transaction/simulation_latency.csv" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') + DIRECT_MEDIAN=$(awk -F, 'NR>1 {print $2}' "$LATENCY_DIRECT" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') + SESSION_MEDIAN=$(awk -F, 'NR>1 {print $2}' "$LATENCY_SESSION" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') + TRANSACTION_MEDIAN=$(awk -F, 'NR>1 {print $2}' "$LATENCY_TRANSACTION" | sort -n | awk '{a[NR]=$1} END {n=NR; mid=int(n/2); if(n%2==1) printf "%.2f", a[mid+1]; else printf "%.2f", (a[mid]+a[mid+1])/2}') # Create summary CSV for bar chart cat > "./output/pooling_comparison/latency_summary.csv" << EOF From 7aea02b7e45f54bbbb2aabe195b24311d73c67d5 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 09:15:41 +0800 Subject: [PATCH 31/32] refactor(pool-size-comparison): add checks for summary data and optimal pool size determination --- .../AzureDbSample/pool-size-comparison.ps1 | 72 ++++++++++-------- .../AzureDbSample/pool-size-comparison.sh | 76 +++++++++++-------- 2 files changed, 86 insertions(+), 62 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 index 383a793..928716a 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 @@ -246,38 +246,50 @@ foreach ($Stats in $SummaryData) { Write-Host "" Write-ColorOutput "Recommendations:" "Blue" -# Find minimum latency -$MinLatency = ($SummaryData | Measure-Object -Property MedianLatency -Minimum).Minimum - -# Find smallest pool size that achieves near-optimal performance (within 5% of minimum) -# Uses same 5% threshold as "diminishing returns" for consistency -$Threshold = $MinLatency * 1.05 -$OptimalStats = $SummaryData | - Where-Object { $_.MedianLatency -le $Threshold } | - Sort-Object PoolSize | - Select-Object -First 1 - -Write-ColorOutput " • Optimal pool size: $($OptimalStats.PoolSize) (median latency: $($OptimalStats.MedianLatency)ms)" "Green" -Write-ColorOutput " • Note: Smallest pool size achieving near-optimal performance (≤5% of minimum)" "Cyan" - -# Check for diminishing returns (when improvement < 5%) -# Sort pool sizes numerically for meaningful comparison -$SortedData = $SummaryData | Sort-Object PoolSize - -for ($i = 1; $i -lt $SortedData.Count; $i++) { - # Skip the optimal pool size (already highlighted above) - if ($SortedData[$i].PoolSize -eq $OptimalStats.PoolSize) { - continue - } +# Check if summary data exists +if ($SummaryData.Count -eq 0) { + Write-ColorOutput "Warning: No summary data available" "Yellow" + Write-ColorOutput "Cannot generate recommendations. Check that simulations completed successfully." "Yellow" +} else { + # Find minimum latency + $MinLatency = ($SummaryData | Measure-Object -Property MedianLatency -Minimum).Minimum + + # Find smallest pool size that achieves near-optimal performance (within 5% of minimum) + # Uses same 5% threshold as "diminishing returns" for consistency + $Threshold = $MinLatency * 1.05 + $OptimalStats = $SummaryData | + Where-Object { $_.MedianLatency -le $Threshold } | + Sort-Object PoolSize | + Select-Object -First 1 + + # Guard against null OptimalStats + if ($null -eq $OptimalStats) { + Write-ColorOutput "Warning: Could not determine optimal pool size" "Yellow" + Write-ColorOutput "Check that summary data has valid latency values" "Yellow" + } else { + Write-ColorOutput " • Optimal pool size: $($OptimalStats.PoolSize) (median latency: $($OptimalStats.MedianLatency)ms)" "Green" + Write-ColorOutput " • Note: Smallest pool size achieving near-optimal performance (≤5% of minimum)" "Cyan" - $PrevAvg = $SortedData[$i-1].MedianLatency - $CurrentAvg = $SortedData[$i].MedianLatency - $Improvement = ($PrevAvg - $CurrentAvg) / $PrevAvg * 100 + # Check for diminishing returns (when improvement < 5%) + # Sort pool sizes numerically for meaningful comparison + $SortedData = $SummaryData | Sort-Object PoolSize - if ($Improvement -lt 0) { - Write-ColorOutput " • Pool size $($SortedData[$i].PoolSize): Performance degraded ($([Math]::Round($Improvement, 2))% worse)" "Yellow" - } elseif ($Improvement -lt 5) { - Write-ColorOutput " • Pool size $($SortedData[$i].PoolSize): Diminishing returns (<5% improvement)" "Yellow" + for ($i = 1; $i -lt $SortedData.Count; $i++) { + # Skip the optimal pool size (already highlighted above) + if ($SortedData[$i].PoolSize -eq $OptimalStats.PoolSize) { + continue + } + + $PrevAvg = $SortedData[$i-1].MedianLatency + $CurrentAvg = $SortedData[$i].MedianLatency + $Improvement = ($PrevAvg - $CurrentAvg) / $PrevAvg * 100 + + if ($Improvement -lt 0) { + Write-ColorOutput " • Pool size $($SortedData[$i].PoolSize): Performance degraded ($([Math]::Round($Improvement, 2))% worse)" "Yellow" + } elseif ($Improvement -lt 5) { + Write-ColorOutput " • Pool size $($SortedData[$i].PoolSize): Diminishing returns (<5% improvement)" "Yellow" + } + } } } diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh index 7188055..48ea0ec 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh @@ -256,37 +256,47 @@ done echo "" echo -e "${BLUE}Recommendations:${NC}" -# Find minimum latency -MIN_LATENCY=$(awk -F, 'NR>1 {if(NR==2 || $21 { - threshold = minlat * 1.05; - if ($2 <= threshold) { - if (optimal == "" || $1 < optimal) { - optimal = $1; - optlat = $2; +# Check if summary CSV exists and has data +if [ ! -f "./output/pool_size_comparison/latency_summary.csv" ] || [ ! -s "./output/pool_size_comparison/latency_summary.csv" ]; then + echo -e "${YELLOW}Warning: latency_summary.csv is missing or empty${NC}" + echo -e "${YELLOW}Cannot generate recommendations. Check that simulations completed successfully.${NC}" +else + # Find minimum latency + MIN_LATENCY=$(awk -F, 'NR>1 {if(NR==2 || $21 { + threshold = minlat * 1.05; + if ($2 <= threshold) { + if (optimal == "" || $1 < optimal) { + optimal = $1; + optlat = $2; + } } - } -} END { - printf "%s", optimal -}' "./output/pool_size_comparison/latency_summary.csv") - -OPTIMAL_LATENCY=$(awk -F, -v size="$OPTIMAL_SIZE" 'NR>1 && $1==size {printf "%.2f", $2}' "./output/pool_size_comparison/latency_summary.csv") - -echo -e " • ${GREEN}Optimal pool size: ${OPTIMAL_SIZE} (median latency: ${OPTIMAL_LATENCY}ms)${NC}" -echo -e " • ${CYAN}Note: Smallest pool size achieving near-optimal performance (≤5% of minimum)${NC}" - -# Check for diminishing returns (when improvement < 5%) -# Sort pool sizes numerically for meaningful comparison -IFS=$'\n' SORTED_POOL_SIZES=($(printf '%s\n' "${POOL_SIZES[@]}" | sort -n)) -unset IFS - -LAST_AVG="" -for POOL_SIZE in "${SORTED_POOL_SIZES[@]}"; do - # Skip the optimal pool size (already highlighted above) - if [ "$POOL_SIZE" -eq "$OPTIMAL_SIZE" ]; then + } END { + printf "%s", optimal + }' "./output/pool_size_comparison/latency_summary.csv") + + # Guard against empty OPTIMAL_SIZE + if [ -z "$OPTIMAL_SIZE" ]; then + echo -e "${YELLOW}Warning: Could not determine optimal pool size${NC}" + echo -e "${YELLOW}Check that latency_summary.csv has valid data rows${NC}" + else + OPTIMAL_LATENCY=$(awk -F, -v size="$OPTIMAL_SIZE" 'NR>1 && $1==size {printf "%.2f", $2}' "./output/pool_size_comparison/latency_summary.csv") + + echo -e " • ${GREEN}Optimal pool size: ${OPTIMAL_SIZE} (median latency: ${OPTIMAL_LATENCY}ms)${NC}" + echo -e " • ${CYAN}Note: Smallest pool size achieving near-optimal performance (≤5% of minimum)${NC}" + + # Check for diminishing returns (when improvement < 5%) + # Sort pool sizes numerically for meaningful comparison + IFS=$'\n' SORTED_POOL_SIZES=($(printf '%s\n' "${POOL_SIZES[@]}" | sort -n)) + unset IFS + + LAST_AVG="" + for POOL_SIZE in "${SORTED_POOL_SIZES[@]}"; do + # Skip the optimal pool size (already highlighted above) + if [ "$POOL_SIZE" -eq "$OPTIMAL_SIZE" ]; then CURRENT_AVG=$(awk -F, -v size="$POOL_SIZE" 'NR>1 && $1==size {print $2}' "./output/pool_size_comparison/latency_summary.csv") LAST_AVG=$CURRENT_AVG continue @@ -306,9 +316,11 @@ for POOL_SIZE in "${SORTED_POOL_SIZES[@]}"; do echo -e " • ${YELLOW}Pool size ${POOL_SIZE}: Diminishing returns (<5% improvement)${NC}" fi fi + fi + LAST_AVG=$CURRENT_AVG + done fi - LAST_AVG=$CURRENT_AVG -done +fi OVERALL_ELAPSED=$(($SECONDS - $OVERALL_START)) echo "" From 55992fc8bf888840b333d5a6d475e4a21458a996 Mon Sep 17 00:00:00 2001 From: goh-chunlin Date: Sat, 11 Apr 2026 09:23:44 +0800 Subject: [PATCH 32/32] refactor(pool-size-comparison): improve checks for summary data availability in recommendations --- .../AzureDbSample/pool-size-comparison.ps1 | 6 +++--- SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 index 928716a..f60813d 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.ps1 @@ -246,9 +246,9 @@ foreach ($Stats in $SummaryData) { Write-Host "" Write-ColorOutput "Recommendations:" "Blue" -# Check if summary data exists -if ($SummaryData.Count -eq 0) { - Write-ColorOutput "Warning: No summary data available" "Yellow" +# Check if summary data has actual rows (not just empty array) +if ($null -eq $SummaryData -or $SummaryData.Count -eq 0) { + Write-ColorOutput "Warning: No summary data available (simulations produced no data)" "Yellow" Write-ColorOutput "Cannot generate recommendations. Check that simulations completed successfully." "Yellow" } else { # Find minimum latency diff --git a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh index 48ea0ec..5643ea4 100755 --- a/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh +++ b/SimNextgenApp.Demo/AzureDbSample/pool-size-comparison.sh @@ -256,10 +256,13 @@ done echo "" echo -e "${BLUE}Recommendations:${NC}" -# Check if summary CSV exists and has data -if [ ! -f "./output/pool_size_comparison/latency_summary.csv" ] || [ ! -s "./output/pool_size_comparison/latency_summary.csv" ]; then - echo -e "${YELLOW}Warning: latency_summary.csv is missing or empty${NC}" +# Check if summary CSV exists and has data rows (not just header) +if [ ! -f "./output/pool_size_comparison/latency_summary.csv" ]; then + echo -e "${YELLOW}Warning: latency_summary.csv is missing${NC}" echo -e "${YELLOW}Cannot generate recommendations. Check that simulations completed successfully.${NC}" +elif ! tail -n +2 "./output/pool_size_comparison/latency_summary.csv" | grep -q .; then + echo -e "${YELLOW}Warning: latency_summary.csv has no data rows (only header)${NC}" + echo -e "${YELLOW}Cannot generate recommendations. Check that simulations produced data.${NC}" else # Find minimum latency MIN_LATENCY=$(awk -F, 'NR>1 {if(NR==2 || $2