using System.Web;
using BenchmarkDotNet.Attributes;
namespace BenchmarkTests;
///
/// HTTP endpoint benchmark tests for full-stack performance measurement.
///
/// IMPORTANT: These benchmarks require the NpgsqlRestTests server to be running.
/// Start the server with: dotnet run --project NpgsqlRestTests/Setup
///
/// Main endpoint tested: public.perf_test - returns all common PostgreSQL types
///
[MemoryDiagnoser]
public class HttpClientTests
{
private HttpClient _client = null!;
// Base URL for the test server
private const string BaseUrl = "http://localhost:5000";
// Pre-built query strings for different test scenarios
private string _perfTestUrl_10Rows = null!;
private string _perfTestUrl_100Rows = null!;
private string _perfTestUrl_1000Rows = null!;
[GlobalSetup]
public void Setup()
{
_client = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };
// Build query string for perf_test function
// Parameters: records, text, int, bigint, numeric, real, double, bool, date, timestamp, timestamptz, uuid, json, jsonb, int_array, text_array
_perfTestUrl_10Rows = BuildPerfTestUrl(10);
_perfTestUrl_100Rows = BuildPerfTestUrl(100);
_perfTestUrl_1000Rows = BuildPerfTestUrl(1000);
}
private static string BuildPerfTestUrl(int records)
{
var queryParams = new Dictionary
{
["records"] = records.ToString(),
["text"] = "BenchmarkText",
["int"] = "42",
["bigint"] = "9223372036854775000",
["numeric"] = "12345.6789",
["real"] = "3.14159",
["double"] = "2.718281828459045",
["bool"] = "true",
["date"] = "2024-01-15",
["timestamp"] = "2024-01-15T10:30:00",
["timestamptz"] = "2024-01-15T10:30:00Z",
["uuid"] = "550e8400-e29b-41d4-a716-446655440000",
["json"] = """{"key":"value","num":123}""",
["jsonb"] = """{"nested":{"data":true}}""",
["intArray"] = "{1,2,3,4,5}",
["textArray"] = "{\"a\",\"b\",\"c\"}"
};
var queryString = string.Join("&", queryParams.Select(p => $"{p.Key}={HttpUtility.UrlEncode(p.Value)}"));
return $"{BaseUrl}/api/perf-test?{queryString}";
}
[GlobalCleanup]
public void Cleanup()
{
_client.Dispose();
}
///
/// Benchmark: Full-type test with 10 rows
/// Tests all PostgreSQL types: text, int, bigint, numeric, real, double, bool,
/// date, time, timestamp, timestamptz, interval, uuid, json, jsonb, arrays, nullables
///
[Benchmark]
public async Task PerfTest_AllTypes_10Rows()
{
using var result = await _client.GetAsync(_perfTestUrl_10Rows);
return await result.Content.ReadAsStringAsync();
}
///
/// Benchmark: Full-type test with 100 rows
/// Tests serialization performance with moderate data volume
///
[Benchmark]
public async Task PerfTest_AllTypes_100Rows()
{
using var result = await _client.GetAsync(_perfTestUrl_100Rows);
return await result.Content.ReadAsStringAsync();
}
///
/// Benchmark: Full-type test with 1000 rows
/// Tests serialization performance at scale
///
[Benchmark]
public async Task PerfTest_AllTypes_1000Rows()
{
using var result = await _client.GetAsync(_perfTestUrl_1000Rows);
return await result.Content.ReadAsStringAsync();
}
///
/// Benchmark: Simple table (int + text only) for comparison
///
[Benchmark]
public async Task SimpleTable_100Rows()
{
using var result = await _client.GetAsync($"{BaseUrl}/api/case-get-long-table1/?records=100");
return await result.Content.ReadAsStringAsync();
}
///
/// Benchmark: Cached endpoint
/// Tests cache hit scenario
///
[Benchmark]
public async Task CachedSet_50Rows()
{
using var result = await _client.GetAsync($"{BaseUrl}/api/cache-get-set/?count=50");
return await result.Content.ReadAsStringAsync();
}
///
/// Benchmark: JSON record endpoint
/// Tests JSON type serialization
///
[Benchmark]
public async Task JsonRecord()
{
using var result = await _client.GetAsync($"{BaseUrl}/api/cache-get-json-record/?key=benchmark");
return await result.Content.ReadAsStringAsync();
}
}
///
/// Parameterized HTTP benchmarks with varying row counts.
/// Uses perf_test function to measure serialization scaling across all PostgreSQL types.
///
[MemoryDiagnoser]
public class HttpScalingBenchmarks
{
private HttpClient _client = null!;
private const string BaseUrl = "http://localhost:5000";
private string[] _prebuiltUrls = null!;
[Params(10, 50, 100, 250, 500, 1000)]
public int RowCount { get; set; }
[GlobalSetup]
public void Setup()
{
_client = new HttpClient { Timeout = TimeSpan.FromSeconds(120) };
// Pre-build URLs for each row count
_prebuiltUrls = new[] { 10, 50, 100, 250, 500, 1000 }
.Select(BuildPerfTestUrl)
.ToArray();
}
private static string BuildPerfTestUrl(int records)
{
return $"{BaseUrl}/api/perf-test?" +
$"records={records}&" +
$"text=ScalingTest&" +
$"int=100&" +
$"bigint=9223372036854775000&" +
$"numeric=999.9999&" +
$"real=1.5&" +
$"double=2.5&" +
$"bool=true&" +
$"date=2024-06-15&" +
$"timestamp=2024-06-15T12:00:00&" +
$"timestamptz=2024-06-15T12:00:00Z&" +
$"uuid=a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11&" +
$"json=" + HttpUtility.UrlEncode("""{"test":1}""") + "&" +
$"jsonb=" + HttpUtility.UrlEncode("""{"test":2}""") + "&" +
$"intArray=" + HttpUtility.UrlEncode("{1,2,3}") + "&" +
$"textArray=" + HttpUtility.UrlEncode("""{"x","y"}""");
}
[GlobalCleanup]
public void Cleanup()
{
_client.Dispose();
}
///
/// Benchmark: Full-type endpoint with parameterized row count
/// Measures how serialization time scales with data volume for all PostgreSQL types
///
[Benchmark]
public async Task PerfTest_AllTypes_Scaling()
{
var urlIndex = RowCount switch
{
10 => 0, 50 => 1, 100 => 2, 250 => 3, 500 => 4, 1000 => 5, _ => 0
};
using var result = await _client.GetAsync(_prebuiltUrls[urlIndex]);
return await result.Content.ReadAsStringAsync();
}
///
/// Benchmark: Simple table for scaling comparison
///
[Benchmark]
public async Task SimpleTable_Scaling()
{
using var result = await _client.GetAsync($"{BaseUrl}/api/case-get-long-table1/?records={RowCount}");
return await result.Content.ReadAsStringAsync();
}
}
///
/// Concurrent request benchmarks to test throughput under load.
///
[MemoryDiagnoser]
public class HttpConcurrencyBenchmarks
{
private HttpClient _client = null!;
private const string BaseUrl = "http://localhost:5000";
private string _perfTestUrl = null!;
[Params(1, 5, 10)]
public int ConcurrentRequests { get; set; }
[GlobalSetup]
public void Setup()
{
_client = new HttpClient { Timeout = TimeSpan.FromSeconds(120) };
_perfTestUrl = $"{BaseUrl}/api/perf-test?" +
$"records=50&" +
$"text=ConcurrentTest&" +
$"int=1&bigint=1&numeric=1.0&real=1.0&double=1.0&" +
$"bool=true&date=2024-01-01&" +
$"timestamp=2024-01-01T00:00:00×tamptz=2024-01-01T00:00:00Z&" +
$"uuid=00000000-0000-0000-0000-000000000001&" +
$"json=" + HttpUtility.UrlEncode("{}") + "&" +
$"jsonb=" + HttpUtility.UrlEncode("{}") + "&" +
$"intArray=" + HttpUtility.UrlEncode("{1}") + "&" +
$"textArray=" + HttpUtility.UrlEncode("""{"a"}""");
}
[GlobalCleanup]
public void Cleanup()
{
_client.Dispose();
}
///
/// Benchmark: Concurrent GET requests to full-type endpoint
/// Tests throughput under concurrent load
///
[Benchmark]
public async Task ConcurrentPerfTestRequests()
{
var tasks = new Task[ConcurrentRequests];
for (int i = 0; i < ConcurrentRequests; i++)
{
tasks[i] = _client.GetAsync(_perfTestUrl);
}
var results = await Task.WhenAll(tasks);
int successCount = 0;
foreach (var response in results)
{
if (response.IsSuccessStatusCode) successCount++;
response.Dispose();
}
return successCount;
}
///
/// Benchmark: Concurrent GET to simple table endpoint
///
[Benchmark]
public async Task ConcurrentSimpleTableRequests()
{
var tasks = new Task[ConcurrentRequests];
for (int i = 0; i < ConcurrentRequests; i++)
{
tasks[i] = _client.GetAsync($"{BaseUrl}/api/case-get-long-table1/?records=50");
}
var results = await Task.WhenAll(tasks);
int successCount = 0;
foreach (var response in results)
{
if (response.IsSuccessStatusCode) successCount++;
response.Dispose();
}
return successCount;
}
}
///
/// Type-specific endpoint benchmarks to test different PostgreSQL type serialization paths.
/// Compares full-type endpoint vs simple endpoints to measure type overhead.
///
[MemoryDiagnoser]
public class HttpTypeSerializationBenchmarks
{
private HttpClient _client = null!;
private const string BaseUrl = "http://localhost:5000";
private string _perfTestUrl = null!;
[GlobalSetup]
public void Setup()
{
_client = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };
_perfTestUrl = $"{BaseUrl}/api/perf-test?" +
$"records=100&" +
$"text=TypeTest&" +
$"int=42&bigint=123456789&numeric=99.99&real=3.14&double=2.718&" +
$"bool=true&date=2024-03-15&" +
$"timestamp=2024-03-15T14:30:00×tamptz=2024-03-15T14:30:00Z&" +
$"uuid=12345678-1234-1234-1234-123456789abc&" +
$"json=" + HttpUtility.UrlEncode("""{"field":"value"}""") + "&" +
$"jsonb=" + HttpUtility.UrlEncode("""{"nested":{"field":"value"}}""") + "&" +
$"intArray=" + HttpUtility.UrlEncode("{10,20,30,40,50}") + "&" +
$"textArray=" + HttpUtility.UrlEncode("""{"one","two","three"}""");
}
[GlobalCleanup]
public void Cleanup()
{
_client.Dispose();
}
///
/// Benchmark: Simple types only (int + text) as baseline
/// Tests: TypeCategory.Numeric and TypeCategory.Text serialization
///
[Benchmark(Baseline = true)]
public async Task SimpleTypes_IntText_100Rows()
{
using var result = await _client.GetAsync($"{BaseUrl}/api/case-get-long-table1/?records=100");
return await result.Content.ReadAsStringAsync();
}
///
/// Benchmark: All PostgreSQL types
/// Tests: Full type serialization including datetime, uuid, json, arrays, nullables
///
[Benchmark]
public async Task AllTypes_100Rows()
{
using var result = await _client.GetAsync(_perfTestUrl);
return await result.Content.ReadAsStringAsync();
}
///
/// Benchmark: Cached set (int + text)
/// Tests: Cached response path
///
[Benchmark]
public async Task CachedIntText_100Rows()
{
using var result = await _client.GetAsync($"{BaseUrl}/api/cache-get-set/?count=100");
return await result.Content.ReadAsStringAsync();
}
///
/// Benchmark: Set with null values
/// Tests: Null handling in serialization
///
[Benchmark]
public async Task SetWithNulls_100Rows()
{
using var result = await _client.GetAsync($"{BaseUrl}/api/cache-get-set-with-nulls/?count=100");
return await result.Content.ReadAsStringAsync();
}
///
/// Benchmark: JSON type field
/// Tests: TypeCategory.Json serialization (no escaping needed)
///
[Benchmark]
public async Task JsonField()
{
using var result = await _client.GetAsync($"{BaseUrl}/api/cache-get-json-record/?key=typetest");
return await result.Content.ReadAsStringAsync();
}
}