mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-06-13 14:38:43 +00:00
new tests
This commit is contained in:
@@ -7,6 +7,14 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Program.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Program.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\Libraries\Esiur\Esiur.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// ============================================================
|
||||
// Test 4: Fork-Join Queueing Test — CLIENT NODE (REPLICATED)
|
||||
//
|
||||
// Extends the original single-shot client to run K independent
|
||||
// replications of each (delay, α) configuration so that 95%
|
||||
// confidence intervals can be reported for the metrics in
|
||||
// Table III (λ, μ, R̄, δ̄, D̄, P99(D), queue length, batch B).
|
||||
//
|
||||
// Each replication uses an identical configuration; the server
|
||||
// runs StartUpdatesLocal back-to-back, and the client snapshots
|
||||
// the cumulative finished-queue length between replications so
|
||||
// that each replication's evaluation sees only its own items.
|
||||
//
|
||||
// Usage:
|
||||
// dotnet run -- --host 127.0.0.1 --port 10901 \
|
||||
// --trials 1000 \
|
||||
// --delays 5:10:20:30:50:100 \
|
||||
// --alphas 0.0:0.25:0.5:0.75:1.0 \
|
||||
// --replications 5 \
|
||||
// --output forkjoin_replicated.csv
|
||||
// ============================================================
|
||||
|
||||
using Esiur.Data;
|
||||
using Esiur.Protocol;
|
||||
using Esiur.Resource;
|
||||
using Esiur.Tests.Queueing.Client;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
// ---------- arguments ----------
|
||||
var host = GetArg(args, "--host", "127.0.0.1");
|
||||
var port = int.Parse(GetArg(args, "--port", "10901"));
|
||||
var trials = int.Parse(GetArg(args, "--trials", "1000"));
|
||||
var replications = int.Parse(GetArg(args, "--replications", "5"));
|
||||
var settleMs = int.Parse(GetArg(args, "--settle-ms", "1000"));
|
||||
var outputCsv = GetArg(args, "--output", "forkjoin_replicated.csv");
|
||||
var delays = GetArg(args, "--delays", "5:10:20:30:50:100")
|
||||
.Split(':').Select(int.Parse).ToArray();
|
||||
var alphas = GetArg(args, "--alphas", "0.0:0.25:0.5:0.75:1.0")
|
||||
.Split(':').Select(s => double.Parse(s, CultureInfo.InvariantCulture)).ToArray();
|
||||
|
||||
Console.WriteLine($"[Client-T4-R] Connecting to {host}:{port}");
|
||||
Console.WriteLine($"[Client-T4-R] trials/rep={trials} replications={replications} " +
|
||||
$"settle={settleMs}ms");
|
||||
Console.WriteLine($"[Client-T4-R] delays={string.Join(",", delays)}");
|
||||
Console.WriteLine($"[Client-T4-R] alphas={string.Join(",", alphas.Select(a => a.ToString("F2", CultureInfo.InvariantCulture)))}");
|
||||
Console.WriteLine($"[Client-T4-R] {delays.Length * alphas.Length} configurations × {replications} reps " +
|
||||
$"= {delays.Length * alphas.Length * replications} trial runs");
|
||||
|
||||
// ---------- connect ----------
|
||||
var wh = new Warehouse();
|
||||
var serviceResource = await wh.Get<EpResource>($"ep://{host}:{port}/sys/queueing");
|
||||
var service = (dynamic)serviceResource;
|
||||
|
||||
// ---------- replication coordinator state ----------
|
||||
//
|
||||
// The server's StartUpdatesLocal fires `trials` PropertyChanged events
|
||||
// across a single call. We count incoming events; when `trials` arrive,
|
||||
// the current replication is complete. We then slice off this rep's
|
||||
// portion of the cumulative finished-queue and hand it to QueueEval.
|
||||
//
|
||||
// `repDone` is signaled once per replication so the orchestrator coroutine
|
||||
// can drive the next call.
|
||||
|
||||
int eventsThisRep = 0;
|
||||
TaskCompletionSource<bool> repDone = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
int finishedQueueBaseline = 0; // cumulative length BEFORE current rep started
|
||||
|
||||
serviceResource.PropertyChanged += (object? sender, PropertyChangedEventArgs e) =>
|
||||
{
|
||||
int n = Interlocked.Increment(ref eventsThisRep);
|
||||
if (n == trials)
|
||||
{
|
||||
repDone.TrySetResult(true);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- main sweep ----------
|
||||
var rows = new List<ReplicatedResult>();
|
||||
|
||||
using var writer = new StreamWriter(outputCsv);
|
||||
writer.WriteLine(ReplicatedEvalAggregator.CsvHeader);
|
||||
writer.Flush();
|
||||
|
||||
foreach (var delay in delays)
|
||||
{
|
||||
foreach (var alpha in alphas)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"[Client-T4-R] >>> delay={delay} ms α={alpha:F2} " +
|
||||
$"(running {replications} replications) <<<");
|
||||
|
||||
var reps = new List<EsiurQueueEval.EvalResult>(replications);
|
||||
|
||||
for (int rep = 0; rep < replications; rep++)
|
||||
{
|
||||
// Reset per-rep state
|
||||
Interlocked.Exchange(ref eventsThisRep, 0);
|
||||
repDone = new TaskCompletionSource<bool>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
// Snapshot the cumulative finished-queue length right before this rep
|
||||
// so we can slice off only this rep's portion afterwards.
|
||||
var preQueue = service.DistributedResourceConnection.GetFinishedQueue();
|
||||
finishedQueueBaseline = preQueue.Count;
|
||||
|
||||
// Kick off the server-driven trial sequence (fire-and-forget;
|
||||
// completion is signalled via PropertyChanged → repDone).
|
||||
service.StartUpdatesLocal(delay, trials, alpha);
|
||||
|
||||
// Wait until `trials` PropertyChanged events have been received.
|
||||
await repDone.Task;
|
||||
|
||||
// The server completed `trials` events; slice off this rep's
|
||||
// portion of the cumulative finished-queue. GetFinishedQueue()
|
||||
// returns IReadOnlyList<AsyncQueueItem<T>>; we forward the
|
||||
// typed sliced subset directly to Evaluate which is generic
|
||||
// on T (the property's runtime payload type).
|
||||
var fullQueue = service.DistributedResourceConnection.GetFinishedQueue();
|
||||
var typedQueue = SliceQueue(fullQueue, finishedQueueBaseline);
|
||||
|
||||
var repResult = EsiurQueueEval.Evaluate(typedQueue);
|
||||
reps.Add(repResult);
|
||||
|
||||
Console.WriteLine($" rep {rep + 1}/{replications}: " +
|
||||
$"λ={repResult.LambdaEventsPerSecond:F1}/s " +
|
||||
$"R̄={repResult.Latency.ReadinessMs.Mean:F1}ms " +
|
||||
$"δ̄={repResult.Latency.HolMs.Mean:F1}ms " +
|
||||
$"D̄={repResult.Latency.EndToEndMs.Mean:F1}ms");
|
||||
|
||||
// Settle period between reps to let any straggler notifications drain
|
||||
// and to keep the per-rep arrivals statistically independent of any
|
||||
// residual server state from the previous rep.
|
||||
await Task.Delay(settleMs);
|
||||
}
|
||||
|
||||
var agg = ReplicatedEvalAggregator.Aggregate(delay, alpha, reps);
|
||||
rows.Add(agg);
|
||||
|
||||
ReplicatedEvalAggregator.PrintSummary(agg);
|
||||
|
||||
// Append to CSV immediately so partial progress is preserved
|
||||
// if the process is killed mid-sweep.
|
||||
writer.WriteLine(ReplicatedEvalAggregator.ToCsvRow(agg));
|
||||
writer.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"[Client-T4-R] Done. {rows.Count} configurations written to {outputCsv}");
|
||||
Environment.Exit(0);
|
||||
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
static string GetArg(string[] args, string key, string def)
|
||||
{
|
||||
int i = Array.IndexOf(args, key);
|
||||
return (i >= 0 && i + 1 < args.Length) ? args[i + 1] : def;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Slice the cumulative finished-queue down to only the items added
|
||||
// during the current replication.
|
||||
//
|
||||
// The queue is dynamically typed (returned from a dynamic-dispatched
|
||||
// member) and its element type is AsyncQueueItem<T> where T is the
|
||||
// runtime payload type of the observed property. We rely on the DLR
|
||||
// to bind the LINQ Skip<T>/ToList<T> generic methods at runtime, just
|
||||
// as the original code does with the Evaluate<T> call below it.
|
||||
// ----------------------------------------------------------------
|
||||
static dynamic SliceQueue(dynamic fullQueue, int skipCount)
|
||||
{
|
||||
return System.Linq.Enumerable.ToList(
|
||||
System.Linq.Enumerable.Skip(fullQueue, skipCount));
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Esiur.Tests.Queueing.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Point estimate accompanied by a 95% confidence-interval half-width
|
||||
/// (computed with Student's t for small samples). Use ToString() to
|
||||
/// render as "mean ± half" in print output.
|
||||
/// </summary>
|
||||
public readonly record struct MeanCi(double Mean, double Ci95HalfWidth, int N)
|
||||
{
|
||||
public static MeanCi From(IEnumerable<double> xs)
|
||||
{
|
||||
var arr = xs.ToArray();
|
||||
int n = arr.Length;
|
||||
if (n == 0) return new MeanCi(0, 0, 0);
|
||||
if (n == 1) return new MeanCi(arr[0], 0, 1);
|
||||
|
||||
double mean = arr.Average();
|
||||
double sumSq = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
double d = arr[i] - mean;
|
||||
sumSq += d * d;
|
||||
}
|
||||
double std = Math.Sqrt(sumSq / (n - 1));
|
||||
double sem = std / Math.Sqrt(n);
|
||||
|
||||
// Student's t two-sided 95% for small df. df = n - 1.
|
||||
// Values from standard tables; ≥10 falls back to normal (1.960).
|
||||
double t = (n - 1) switch
|
||||
{
|
||||
1 => 12.706,
|
||||
2 => 4.303,
|
||||
3 => 3.182,
|
||||
4 => 2.776,
|
||||
5 => 2.571,
|
||||
6 => 2.447,
|
||||
7 => 2.365,
|
||||
8 => 2.306,
|
||||
9 => 2.262,
|
||||
10 => 2.228,
|
||||
11 => 2.201,
|
||||
12 => 2.179,
|
||||
13 => 2.160,
|
||||
14 => 2.145,
|
||||
15 => 2.131,
|
||||
16 => 2.120,
|
||||
17 => 2.110,
|
||||
18 => 2.101,
|
||||
19 => 2.093,
|
||||
20 => 2.086,
|
||||
_ => 1.960 // normal approximation for df > 20
|
||||
};
|
||||
return new MeanCi(mean, t * sem, n);
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
N <= 1
|
||||
? Mean.ToString("F2", CultureInfo.InvariantCulture)
|
||||
: string.Create(CultureInfo.InvariantCulture,
|
||||
$"{Mean:F2}±{Ci95HalfWidth:F2}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregated result over K replications of the same (delay, alpha)
|
||||
/// configuration. Carries point estimates plus per-metric 95% CI
|
||||
/// half-widths for the headline metrics reported in the paper:
|
||||
/// arrival rate λ, service rate μ, mean readiness R̄, mean HOL δ̄,
|
||||
/// and mean end-to-end latency D̄.
|
||||
///
|
||||
/// The companion <see cref="EsiurQueueEval.EvalResult"/> field
|
||||
/// (PerRepMean) holds the existing-style averaged point estimates
|
||||
/// so downstream code that already consumed EvalResult continues
|
||||
/// to work unchanged.
|
||||
/// </summary>
|
||||
public sealed record ReplicatedResult(
|
||||
int Delay,
|
||||
double Alpha,
|
||||
int Replications,
|
||||
MeanCi Lambda,
|
||||
MeanCi Mu,
|
||||
MeanCi ReadinessMeanMs,
|
||||
MeanCi HolMeanMs,
|
||||
MeanCi EndToEndMeanMs,
|
||||
MeanCi EndToEndP99Ms,
|
||||
MeanCi QueueLengthMean,
|
||||
MeanCi BatchSizeMean,
|
||||
EsiurQueueEval.EvalResult PerRepMean);
|
||||
|
||||
public static class ReplicatedEvalAggregator
|
||||
{
|
||||
/// <summary>
|
||||
/// Combine K per-replication EvalResult objects into a single
|
||||
/// ReplicatedResult, computing point estimates and 95% CIs.
|
||||
/// </summary>
|
||||
public static ReplicatedResult Aggregate(
|
||||
int delay,
|
||||
double alpha,
|
||||
IReadOnlyList<EsiurQueueEval.EvalResult> reps)
|
||||
{
|
||||
if (reps == null) throw new ArgumentNullException(nameof(reps));
|
||||
if (reps.Count == 0) throw new ArgumentException("reps is empty.", nameof(reps));
|
||||
|
||||
var lambda = MeanCi.From(reps.Select(r => r.LambdaEventsPerSecond));
|
||||
var mu = MeanCi.From(reps.Select(r => r.MuEventsPerSecond));
|
||||
var readiness = MeanCi.From(reps.Select(r => r.Latency.ReadinessMs.Mean));
|
||||
var hol = MeanCi.From(reps.Select(r => r.Latency.HolMs.Mean));
|
||||
var e2eMean = MeanCi.From(reps.Select(r => r.Latency.EndToEndMs.Mean));
|
||||
var e2eP99 = MeanCi.From(reps.Select(r => r.Latency.EndToEndMs.P99));
|
||||
var qLen = MeanCi.From(reps.Select(r => r.QueueLength.Mean));
|
||||
var batch = MeanCi.From(reps.Select(
|
||||
r => r.FlushSizeStats?.Mean ?? double.NaN)
|
||||
.Where(v => !double.IsNaN(v)));
|
||||
|
||||
// Use the existing Average helper for the carry-along point estimates.
|
||||
var perRepMean = EsiurQueueEval.Average(reps);
|
||||
|
||||
return new ReplicatedResult(
|
||||
Delay: delay,
|
||||
Alpha: alpha,
|
||||
Replications: reps.Count,
|
||||
Lambda: lambda,
|
||||
Mu: mu,
|
||||
ReadinessMeanMs: readiness,
|
||||
HolMeanMs: hol,
|
||||
EndToEndMeanMs: e2eMean,
|
||||
EndToEndP99Ms: e2eP99,
|
||||
QueueLengthMean: qLen,
|
||||
BatchSizeMean: batch,
|
||||
PerRepMean: perRepMean);
|
||||
}
|
||||
|
||||
public static string CsvHeader =>
|
||||
"delay_ms,alpha,replications," +
|
||||
"lambda_mean,lambda_ci95," +
|
||||
"mu_mean,mu_ci95," +
|
||||
"readiness_mean_ms,readiness_ci95," +
|
||||
"hol_mean_ms,hol_ci95," +
|
||||
"e2e_mean_ms,e2e_ci95," +
|
||||
"e2e_p99_ms,e2e_p99_ci95," +
|
||||
"queue_len_mean,queue_len_ci95," +
|
||||
"batch_mean,batch_ci95";
|
||||
|
||||
public static string ToCsvRow(ReplicatedResult r)
|
||||
{
|
||||
var inv = CultureInfo.InvariantCulture;
|
||||
return string.Create(inv,
|
||||
$"{r.Delay},{r.Alpha:F3},{r.Replications}," +
|
||||
$"{r.Lambda.Mean:F3},{r.Lambda.Ci95HalfWidth:F3}," +
|
||||
$"{r.Mu.Mean:F3},{r.Mu.Ci95HalfWidth:F3}," +
|
||||
$"{r.ReadinessMeanMs.Mean:F3},{r.ReadinessMeanMs.Ci95HalfWidth:F3}," +
|
||||
$"{r.HolMeanMs.Mean:F3},{r.HolMeanMs.Ci95HalfWidth:F3}," +
|
||||
$"{r.EndToEndMeanMs.Mean:F3},{r.EndToEndMeanMs.Ci95HalfWidth:F3}," +
|
||||
$"{r.EndToEndP99Ms.Mean:F3},{r.EndToEndP99Ms.Ci95HalfWidth:F3}," +
|
||||
$"{r.QueueLengthMean.Mean:F3},{r.QueueLengthMean.Ci95HalfWidth:F3}," +
|
||||
$"{r.BatchSizeMean.Mean:F3},{r.BatchSizeMean.Ci95HalfWidth:F3}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Console-friendly compact summary, one configuration per call.
|
||||
/// </summary>
|
||||
public static void PrintSummary(ReplicatedResult r)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"=== Configuration: delay={r.Delay} ms, α={r.Alpha:F2}, " +
|
||||
$"replications={r.Replications} ===");
|
||||
Console.WriteLine("Metric | Mean ± 95% CI half-width");
|
||||
Console.WriteLine("----------------+----------------------------------------");
|
||||
Console.WriteLine($"λ (/s) | {r.Lambda}");
|
||||
Console.WriteLine($"μ (/s) | {r.Mu}");
|
||||
Console.WriteLine($"R̄ (ms) | {r.ReadinessMeanMs}");
|
||||
Console.WriteLine($"δ̄ (ms) | {r.HolMeanMs}");
|
||||
Console.WriteLine($"D̄ (ms) | {r.EndToEndMeanMs}");
|
||||
Console.WriteLine($"P99(D) (ms) | {r.EndToEndP99Ms}");
|
||||
Console.WriteLine($"Queue length | {r.QueueLengthMean}");
|
||||
Console.WriteLine($"Batch size B | {r.BatchSizeMean}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user