mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-06-13 14:38:43 +00:00
Fix
This commit is contained in:
@@ -85,10 +85,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{57E806
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client", "Client", "{21D42B96-99F9-4E48-A499-5170A5A9597F}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client", "Client", "{21D42B96-99F9-4E48-A499-5170A5A9597F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.NodeFanoutSweep.Server", "Tests\Distribution\NodeFanoutSweep\Server\Esiur.Tests.NodeFanoutSweep.Server.csproj", "{9FF626DF-1AD4-2BE1-F834-F49121D65085}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.NodeFanoutSweep.Client", "Tests\Distribution\NodeFanoutSweep\Client\Esiur.Tests.NodeFanoutSweep.Client.csproj", "{550A20AB-8E97-BCDD-9F54-27823663120A}"
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConcurrentAttachSweep", "ConcurrentAttachSweep", "{E713D25F-2602-44C9-AB9E-C9477FB2BA93}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConcurrentAttachSweep", "ConcurrentAttachSweep", "{E713D25F-2602-44C9-AB9E-C9477FB2BA93}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.ConcurrentAttachSweep", "Tests\Distribution\ConcurrentAttachSweep\Esiur.Tests.ConcurrentAttachSweep.csproj", "{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.ConcurrentAttachSweep", "Tests\Distribution\ConcurrentAttachSweep\Esiur.Tests.ConcurrentAttachSweep.csproj", "{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}"
|
||||||
@@ -105,6 +101,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{967F62
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.Deadlock.Server", "Tests\Distribution\Deadlock\Server\Esiur.Tests.Deadlock.Server.csproj", "{F2FE7C0B-58C1-D768-C37A-D428D2B85940}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.Deadlock.Server", "Tests\Distribution\Deadlock\Server\Esiur.Tests.Deadlock.Server.csproj", "{F2FE7C0B-58C1-D768-C37A-D428D2B85940}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.NodeFanoutSweep.Client", "Tests\Distribution\NodeFanoutSweep\Client\Esiur.Tests.NodeFanoutSweep.Client.csproj", "{550A20AB-8E97-BCDD-9F54-27823663120A}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.NodeFanoutSweep.Server", "Tests\Distribution\NodeFanoutSweep\Server\Esiur.Tests.NodeFanoutSweep.Server.csproj", "{9FF626DF-1AD4-2BE1-F834-F49121D65085}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -331,30 +331,6 @@ Global
|
|||||||
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|x64.Build.0 = Release|Any CPU
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|x86.ActiveCfg = Release|Any CPU
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|x86.Build.0 = Release|Any CPU
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -403,6 +379,30 @@ Global
|
|||||||
{F2FE7C0B-58C1-D768-C37A-D428D2B85940}.Release|x64.Build.0 = Release|Any CPU
|
{F2FE7C0B-58C1-D768-C37A-D428D2B85940}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{F2FE7C0B-58C1-D768-C37A-D428D2B85940}.Release|x86.ActiveCfg = Release|Any CPU
|
{F2FE7C0B-58C1-D768-C37A-D428D2B85940}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{F2FE7C0B-58C1-D768-C37A-D428D2B85940}.Release|x86.Build.0 = Release|Any CPU
|
{F2FE7C0B-58C1-D768-C37A-D428D2B85940}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -443,8 +443,6 @@ Global
|
|||||||
{33D973D8-4D3E-47BA-8135-FCA0CFF7E210} = {94C8CFDB-C7C6-40DF-A596-647FEEA3C917}
|
{33D973D8-4D3E-47BA-8135-FCA0CFF7E210} = {94C8CFDB-C7C6-40DF-A596-647FEEA3C917}
|
||||||
{57E80693-7AFC-4446-87DE-25E97C036E2F} = {33D973D8-4D3E-47BA-8135-FCA0CFF7E210}
|
{57E80693-7AFC-4446-87DE-25E97C036E2F} = {33D973D8-4D3E-47BA-8135-FCA0CFF7E210}
|
||||||
{21D42B96-99F9-4E48-A499-5170A5A9597F} = {33D973D8-4D3E-47BA-8135-FCA0CFF7E210}
|
{21D42B96-99F9-4E48-A499-5170A5A9597F} = {33D973D8-4D3E-47BA-8135-FCA0CFF7E210}
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085} = {57E80693-7AFC-4446-87DE-25E97C036E2F}
|
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A} = {21D42B96-99F9-4E48-A499-5170A5A9597F}
|
|
||||||
{E713D25F-2602-44C9-AB9E-C9477FB2BA93} = {94C8CFDB-C7C6-40DF-A596-647FEEA3C917}
|
{E713D25F-2602-44C9-AB9E-C9477FB2BA93} = {94C8CFDB-C7C6-40DF-A596-647FEEA3C917}
|
||||||
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1} = {E713D25F-2602-44C9-AB9E-C9477FB2BA93}
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1} = {E713D25F-2602-44C9-AB9E-C9477FB2BA93}
|
||||||
{D1B99C5A-82F7-459D-B56D-F8FD096D3854} = {2769C4C3-2595-413B-B7FE-5903826770C1}
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854} = {2769C4C3-2595-413B-B7FE-5903826770C1}
|
||||||
@@ -453,6 +451,8 @@ Global
|
|||||||
{28A7F758-951B-6502-6EA4-C216BA12F77C} = {22A76A25-333D-4516-8EA6-4D03E3023183}
|
{28A7F758-951B-6502-6EA4-C216BA12F77C} = {22A76A25-333D-4516-8EA6-4D03E3023183}
|
||||||
{967F62B4-2815-473F-9F1E-E7F146EE8872} = {C5FB16A3-952C-4078-A15A-3C7CE42E73B5}
|
{967F62B4-2815-473F-9F1E-E7F146EE8872} = {C5FB16A3-952C-4078-A15A-3C7CE42E73B5}
|
||||||
{F2FE7C0B-58C1-D768-C37A-D428D2B85940} = {967F62B4-2815-473F-9F1E-E7F146EE8872}
|
{F2FE7C0B-58C1-D768-C37A-D428D2B85940} = {967F62B4-2815-473F-9F1E-E7F146EE8872}
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A} = {21D42B96-99F9-4E48-A499-5170A5A9597F}
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085} = {57E80693-7AFC-4446-87DE-25E97C036E2F}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {C584421D-5EC0-4821-B7D8-2633D8D405F2}
|
SolutionGuid = {C584421D-5EC0-4821-B7D8-2633D8D405F2}
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ public class EpResource : DynamicObject, IResource, INotifyPropertyChanged, IDyn
|
|||||||
throw new Exception("Trying to access a suspended object.");
|
throw new Exception("Trying to access a suspended object.");
|
||||||
|
|
||||||
|
|
||||||
if (_status != ResourceStatus.Attached)
|
if (_status != ResourceStatus.Published)
|
||||||
{
|
{
|
||||||
result = null;
|
result = null;
|
||||||
return false;
|
return false;
|
||||||
@@ -440,7 +440,7 @@ public class EpResource : DynamicObject, IResource, INotifyPropertyChanged, IDyn
|
|||||||
|
|
||||||
result = null;
|
result = null;
|
||||||
|
|
||||||
if (_status != ResourceStatus.Attached)
|
if (_status != ResourceStatus.Published)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var pt = Instance.Definition.GetPropertyDefByName(binder.Name);
|
var pt = Instance.Definition.GetPropertyDefByName(binder.Name);
|
||||||
@@ -498,7 +498,7 @@ public class EpResource : DynamicObject, IResource, INotifyPropertyChanged, IDyn
|
|||||||
if (_status == ResourceStatus.Suspended)
|
if (_status == ResourceStatus.Suspended)
|
||||||
throw new Exception("Trying to access a suspended object.");
|
throw new Exception("Trying to access a suspended object.");
|
||||||
|
|
||||||
if (_status != ResourceStatus.Attached)
|
if (_status != ResourceStatus.Published)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var pt = Instance.Definition.GetPropertyDefByName(binder.Name);
|
var pt = Instance.Definition.GetPropertyDefByName(binder.Name);
|
||||||
@@ -619,8 +619,8 @@ public class EpResource : DynamicObject, IResource, INotifyPropertyChanged, IDyn
|
|||||||
if (_status == ResourceStatus.Suspended)
|
if (_status == ResourceStatus.Suspended)
|
||||||
throw new Exception("Trying to access a suspended object.");
|
throw new Exception("Trying to access a suspended object.");
|
||||||
|
|
||||||
if (_status != ResourceStatus.Attached)
|
if (_status != ResourceStatus.Published)
|
||||||
throw new Exception("Resource is not attached.");
|
throw new Exception("Resource is not published.");
|
||||||
|
|
||||||
if (index >= _properties.Length)
|
if (index >= _properties.Length)
|
||||||
throw new Exception("Property index not found."); ;
|
throw new Exception("Property index not found."); ;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ var serverWh = new Warehouse();
|
|||||||
if (mode == "server" || mode == "both")
|
if (mode == "server" || mode == "both")
|
||||||
{
|
{
|
||||||
await serverWh.Put("sys", new MemoryStore());
|
await serverWh.Put("sys", new MemoryStore());
|
||||||
await serverWh.Put("sys/server", new EpServer() { Port = (ushort)port });
|
await serverWh.Put("sys/server", new EpServer() { Port = (ushort)port, AllowUnauthorizedAccess = true });
|
||||||
|
|
||||||
for (int i = 0; i < resources; i++)
|
for (int i = 0; i < resources; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ var clientWh = new Warehouse();
|
|||||||
if (mode == "server" || mode == "both")
|
if (mode == "server" || mode == "both")
|
||||||
{
|
{
|
||||||
await serverWh.Put("sys", new MemoryStore());
|
await serverWh.Put("sys", new MemoryStore());
|
||||||
await serverWh.Put("sys/server", new EpServer() { Port = (ushort)port });
|
await serverWh.Put("sys/server", new EpServer() { Port = (ushort)port, AllowUnauthorizedAccess = true });
|
||||||
|
|
||||||
for (int i = 0; i < resources; i++)
|
for (int i = 0; i < resources; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Console.WriteLine($"[Server] resources={resourceCount} interval={intervalMs}ms
|
|||||||
var wh = new Warehouse();
|
var wh = new Warehouse();
|
||||||
// --- Warehouse setup -------------------------------------------------
|
// --- Warehouse setup -------------------------------------------------
|
||||||
await wh.Put("sys", new MemoryStore());
|
await wh.Put("sys", new MemoryStore());
|
||||||
var server = await wh.Put("sys/server", new EpServer() { Port = (ushort)port });
|
var server = await wh.Put("sys/server", new EpServer() { Port = (ushort)port, AllowUnauthorizedAccess = true });
|
||||||
|
|
||||||
// Create and register all sensor resources
|
// Create and register all sensor resources
|
||||||
var sensors = new SensorResource[resourceCount];
|
var sensors = new SensorResource[resourceCount];
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\Libraries\Esiur\Esiur.csproj" OutputItemType="Analyzer" />
|
<ProjectReference Include="..\..\..\..\Libraries\Esiur\Esiur.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,77 +1,427 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// Scalability Extension: Fan-Out — SERVER NODE
|
// Scalability Extension: Fan-Out — ORCHESTRATOR CLIENT
|
||||||
// Hosts M sensor resources and emits Value updates at a fixed interval (the fan-out source). Also
|
// ------------------------------------------------------------
|
||||||
// hosts sys/control, updated once per second with the server process CPU (% across all cores) and
|
// Drives a full sweep of subscriber counts N against a single
|
||||||
// the live subscriber count, which the sweep orchestrator reads to characterise saturation.
|
// server instance. For each N value:
|
||||||
// Anonymous (None-mode) access so subscribers connect without credentials.
|
// 1. Spawns N in-process subscriber tasks, each opening its
|
||||||
|
// own EpConnection to the server.
|
||||||
|
// 2. Each subscriber attaches to all M resources and counts
|
||||||
|
// property-change notifications it receives over a fixed
|
||||||
|
// measurement window.
|
||||||
|
// 3. The orchestrator polls the server's sys/control resource
|
||||||
|
// to capture server-side CPU during the window.
|
||||||
|
// 4. Tears down all N subscribers and waits a settle interval
|
||||||
|
// before the next sweep point.
|
||||||
|
// 5. Repeats for `replications` rounds so the per-N mean and
|
||||||
|
// 95% confidence interval can be computed.
|
||||||
|
// 6. Auto-stops the sweep if either:
|
||||||
|
// - mean per-subscriber rate drops below 10% of theoretical,
|
||||||
|
// - or server CPU stays at >180% (>90% of 2 cores) for the
|
||||||
|
// entire measurement window.
|
||||||
//
|
//
|
||||||
// Usage: dotnet run -- --port 10900 --resources 100 --emit-interval-ms 50
|
// Note on in-process vs separate processes: subscribers are
|
||||||
// (Run the orchestrator from the sibling "Server" project against this host:port.)
|
// tasks within a single client process to keep the test self-
|
||||||
|
// contained and avoid spawning N OS processes. Each task uses
|
||||||
|
// its own EpConnection (TCP connection) to the server, so from
|
||||||
|
// the server's perspective the load looks identical to N
|
||||||
|
// distinct subscriber nodes for the property-propagation path.
|
||||||
|
// The single-client-process design does mean that the client
|
||||||
|
// host's CPU is shared across all subscribers; the orchestrator
|
||||||
|
// records this too so degradation can be attributed correctly.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Usage:
|
||||||
|
// dotnet run -- --host 127.0.0.1 --port 10900 --resources 100 \
|
||||||
|
// --emit-interval-ms 50 --window-sec 60 \
|
||||||
|
// --warmup-sec 5 --replications 3 \
|
||||||
|
// --n-values 2,5,10,20,50,100,200,500
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
using Esiur.Protocol;
|
using Esiur.Protocol;
|
||||||
using Esiur.Resource;
|
using Esiur.Resource;
|
||||||
using Esiur.Stores;
|
using System.Data.Common;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
var port = int.Parse(GetArg(args, "--port", "10900"));
|
var host = GetArg(args, "--host", "127.0.0.1");
|
||||||
var resources = int.Parse(GetArg(args, "--resources", "100"));
|
var port = int.Parse(GetArg(args, "--port", "10900"));
|
||||||
|
var resources = int.Parse(GetArg(args, "--resources", "100"));
|
||||||
var emitIntervalMs = int.Parse(GetArg(args, "--emit-interval-ms", "50"));
|
var emitIntervalMs = int.Parse(GetArg(args, "--emit-interval-ms", "50"));
|
||||||
|
var windowSec = int.Parse(GetArg(args, "--window-sec", "60"));
|
||||||
|
var warmupSec = int.Parse(GetArg(args, "--warmup-sec", "5"));
|
||||||
|
var settleSec = int.Parse(GetArg(args, "--settle-sec", "5"));
|
||||||
|
var replications = int.Parse(GetArg(args, "--replications", "3"));
|
||||||
|
var nValuesStr = GetArg(args, "--n-values", "2,5,10,20,50,100,200,500");
|
||||||
|
var outputCsv = GetArg(args, "--output", "fanout_sweep_results.csv");
|
||||||
|
|
||||||
Console.WriteLine($"[Server] resources={resources} emit-interval={emitIntervalMs}ms port={port} cores={Environment.ProcessorCount}");
|
var nValues = nValuesStr.Split(',').Select(int.Parse).ToArray();
|
||||||
|
double theoreticalMaxRate = 1000.0 / emitIntervalMs * resources;
|
||||||
|
double minAcceptableRate = theoreticalMaxRate * 0.10;
|
||||||
|
|
||||||
var wh = new Warehouse();
|
Console.WriteLine($"[Orchestrator] resources={resources} interval={emitIntervalMs}ms "
|
||||||
await wh.Put("sys", new MemoryStore());
|
+ $"window={windowSec}s replications={replications}");
|
||||||
var server = await wh.Put("sys/server", new EpServer { Port = (ushort)port, AllowUnauthorizedAccess = true });
|
Console.WriteLine($"[Orchestrator] theoretical_max_per_subscriber_rate={theoreticalMaxRate:F0} notif/s");
|
||||||
|
Console.WriteLine($"[Orchestrator] saturation_threshold={minAcceptableRate:F0} notif/s");
|
||||||
|
Console.WriteLine($"[Orchestrator] N values: {string.Join(",", nValues)}");
|
||||||
|
|
||||||
var sensors = new SensorResource[resources];
|
// ----------------------------------------------------------------
|
||||||
for (var i = 0; i < resources; i++) { sensors[i] = new SensorResource { SensorId = i }; await wh.Put($"sys/sensor_{i}", sensors[i]); }
|
// Attach to the server's control resource once.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
var control = new ControlResource();
|
var controlWh = new Warehouse();
|
||||||
await wh.Put("sys/control", control);
|
EpResource? control = null;
|
||||||
|
byte cpuIdx = 255, clientsIdx = 255;
|
||||||
await wh.Open();
|
try
|
||||||
Console.WriteLine($"[Server] Listening on port {port} with {resources} sensors + sys/control. Press Ctrl+C to stop.");
|
|
||||||
|
|
||||||
// Emit loop: drives property-change notifications to every attached subscriber.
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
while (true)
|
var controlConn = await controlWh.Get<EpConnection>($"ep://{host}:{port}");
|
||||||
{
|
control = (EpResource)await controlConn.Get("sys/control");
|
||||||
await Task.Delay(emitIntervalMs);
|
// Resolve property indices by name (EpResource exposes values by index, not dynamic member).
|
||||||
var value = sw.Elapsed.TotalSeconds;
|
var props = control.Instance.Definition.Properties;
|
||||||
foreach (var s in sensors) s.Value = value;
|
cpuIdx = (byte)Array.FindIndex(props, p => p.Name == "CpuPercent");
|
||||||
}
|
clientsIdx = (byte)Array.FindIndex(props, p => p.Name == "ConnectedClients");
|
||||||
});
|
Console.WriteLine($"[Orchestrator] sys/control attached (CpuPercent=idx {cpuIdx}, ConnectedClients=idx {clientsIdx}).");
|
||||||
|
}
|
||||||
// Telemetry loop: publish server CPU (% across all cores) and subscriber count once per second.
|
catch (Exception ex)
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
var proc = Process.GetCurrentProcess();
|
Console.WriteLine($"[Orchestrator] WARNING: could not attach to sys/control: {ex.Message}");
|
||||||
var prevCpu = proc.TotalProcessorTime;
|
Console.WriteLine("[Orchestrator] Server CPU will be reported as N/A.");
|
||||||
var prevWall = DateTime.UtcNow;
|
}
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
await Task.Delay(1000);
|
|
||||||
proc.Refresh();
|
|
||||||
var nowCpu = proc.TotalProcessorTime;
|
|
||||||
var nowWall = DateTime.UtcNow;
|
|
||||||
var wallMs = (nowWall - prevWall).TotalMilliseconds;
|
|
||||||
control.CpuPercent = wallMs > 0 ? (nowCpu - prevCpu).TotalMilliseconds / wallMs * 100.0 : 0;
|
|
||||||
control.ConnectedClients = server.Connections.Count;
|
|
||||||
prevCpu = nowCpu;
|
|
||||||
prevWall = nowWall;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var stop = new TaskCompletionSource();
|
// ----------------------------------------------------------------
|
||||||
Console.CancelKeyPress += (_, e) => { e.Cancel = true; stop.TrySetResult(); };
|
// All sweep points x replications, with per-N early-stop logic.
|
||||||
await stop.Task;
|
// ----------------------------------------------------------------
|
||||||
await wh.Close();
|
var allResults = new List<SweepResult>();
|
||||||
|
bool saturatedDetected = false;
|
||||||
|
|
||||||
|
foreach (int n in nValues)
|
||||||
|
{
|
||||||
|
if (saturatedDetected)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\n[Orchestrator] N={n}: SKIPPED (saturation reached at lower N)");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var perRepResults = new List<RepResult>();
|
||||||
|
|
||||||
|
for (int rep = 0; rep < replications; rep++)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"\n[Orchestrator] === N={n} rep={rep + 1}/{replications} ===");
|
||||||
|
|
||||||
|
var subscribers = new SubscriberTask[n];
|
||||||
|
var subscriberWhs = new Warehouse[n];
|
||||||
|
|
||||||
|
// ---------- spawn N subscribers ----------
|
||||||
|
Console.WriteLine($"[Orchestrator] Spawning {n} subscribers...");
|
||||||
|
var spawnSw = Stopwatch.StartNew();
|
||||||
|
var spawnTasks = new Task<SubscriberTask?>[n];
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
int captured = i;
|
||||||
|
subscriberWhs[i] = new Warehouse();
|
||||||
|
spawnTasks[i] = SpawnSubscriber(subscriberWhs[i], host, port, resources, captured);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(spawnTasks);
|
||||||
|
|
||||||
|
bool spawnFailed = false;
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
if (spawnTasks[i].Result == null) { spawnFailed = true; break; }
|
||||||
|
subscribers[i] = spawnTasks[i].Result!;
|
||||||
|
}
|
||||||
|
spawnSw.Stop();
|
||||||
|
|
||||||
|
if (spawnFailed)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Orchestrator] N={n}: spawn failed; treating as saturation.");
|
||||||
|
saturatedDetected = true;
|
||||||
|
await TeardownAll(subscriberWhs);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[Orchestrator] All {n} subscribers attached in {spawnSw.Elapsed.TotalSeconds:F2}s");
|
||||||
|
|
||||||
|
// ---------- warmup ----------
|
||||||
|
Console.WriteLine($"[Orchestrator] Warmup {warmupSec}s...");
|
||||||
|
await Task.Delay(warmupSec * 1000);
|
||||||
|
foreach (var s in subscribers) s.ResetCounters();
|
||||||
|
|
||||||
|
// ---------- measurement window with CPU sampling ----------
|
||||||
|
Console.WriteLine($"[Orchestrator] Measurement window {windowSec}s...");
|
||||||
|
var cpuSamples = new List<double>();
|
||||||
|
var connSamples = new List<int>();
|
||||||
|
var clientCpuSamples = new List<double>();
|
||||||
|
var clientProc = Process.GetCurrentProcess();
|
||||||
|
var prevClientCpu = clientProc.TotalProcessorTime;
|
||||||
|
var prevClientWall = DateTime.UtcNow;
|
||||||
|
var winSw = Stopwatch.StartNew();
|
||||||
|
while (winSw.Elapsed.TotalSeconds < windowSec)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
|
||||||
|
// Server CPU + subscriber count via the control resource (read by property index;
|
||||||
|
// values arrive as variable-width numerics, hence Convert.*).
|
||||||
|
if (control != null && cpuIdx != 255)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (control.TryGetPropertyValue(cpuIdx, out var cpuVal) && cpuVal != null)
|
||||||
|
cpuSamples.Add(Convert.ToDouble(cpuVal));
|
||||||
|
if (control.TryGetPropertyValue(clientsIdx, out var cliVal) && cliVal != null)
|
||||||
|
connSamples.Add(Convert.ToInt32(cliVal));
|
||||||
|
}
|
||||||
|
catch { /* control resource may not have a current value yet */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// This harness's own CPU (% across all cores). Recorded so saturation can be attributed
|
||||||
|
// to the server rather than to the single subscriber process driving N connections.
|
||||||
|
clientProc.Refresh();
|
||||||
|
var nowClientCpu = clientProc.TotalProcessorTime;
|
||||||
|
var nowClientWall = DateTime.UtcNow;
|
||||||
|
var wallMs = (nowClientWall - prevClientWall).TotalMilliseconds;
|
||||||
|
if (wallMs > 0) clientCpuSamples.Add((nowClientCpu - prevClientCpu).TotalMilliseconds / wallMs * 100.0);
|
||||||
|
prevClientCpu = nowClientCpu;
|
||||||
|
prevClientWall = nowClientWall;
|
||||||
|
}
|
||||||
|
|
||||||
|
double elapsedSec = winSw.Elapsed.TotalSeconds;
|
||||||
|
|
||||||
|
// ---------- collect per-subscriber counts ----------
|
||||||
|
var perSubRates = new double[n];
|
||||||
|
long totalReceived = 0;
|
||||||
|
long totalLate = 0;
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
perSubRates[i] = subscribers[i].Received / elapsedSec;
|
||||||
|
totalReceived += subscribers[i].Received;
|
||||||
|
totalLate += subscribers[i].LateDeliveries;
|
||||||
|
}
|
||||||
|
|
||||||
|
double meanPerSub = perSubRates.Average();
|
||||||
|
double stdPerSub = StdDev(perSubRates);
|
||||||
|
double minPerSub = perSubRates.Min();
|
||||||
|
double maxPerSub = perSubRates.Max();
|
||||||
|
double aggregate = perSubRates.Sum();
|
||||||
|
double avgServerCpu = cpuSamples.Count > 0 ? cpuSamples.Average() : double.NaN;
|
||||||
|
double peakServerCpu = cpuSamples.Count > 0 ? cpuSamples.Max() : double.NaN;
|
||||||
|
double avgClientCpu = clientCpuSamples.Count > 0 ? clientCpuSamples.Average() : double.NaN;
|
||||||
|
double peakClientCpu = clientCpuSamples.Count > 0 ? clientCpuSamples.Max() : double.NaN;
|
||||||
|
|
||||||
|
Console.WriteLine($"[Orchestrator] N={n} rep={rep + 1}: "
|
||||||
|
+ $"mean_per_sub={meanPerSub:F1}/s "
|
||||||
|
+ $"aggregate={aggregate:F0}/s "
|
||||||
|
+ $"late={totalLate} "
|
||||||
|
+ $"server_cpu_avg={avgServerCpu:F1}%/peak={peakServerCpu:F1}% "
|
||||||
|
+ $"client_cpu_avg={avgClientCpu:F1}%/peak={peakClientCpu:F1}%");
|
||||||
|
|
||||||
|
perRepResults.Add(new RepResult
|
||||||
|
{
|
||||||
|
N = n,
|
||||||
|
Rep = rep + 1,
|
||||||
|
MeanPerSub = meanPerSub,
|
||||||
|
StdPerSub = stdPerSub,
|
||||||
|
MinPerSub = minPerSub,
|
||||||
|
MaxPerSub = maxPerSub,
|
||||||
|
Aggregate = aggregate,
|
||||||
|
LateDeliveries = totalLate,
|
||||||
|
ServerCpuAvg = avgServerCpu,
|
||||||
|
ServerCpuPeak = peakServerCpu,
|
||||||
|
ClientCpuAvg = avgClientCpu,
|
||||||
|
ClientCpuPeak = peakClientCpu,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- teardown ----------
|
||||||
|
Console.WriteLine($"[Orchestrator] Tearing down {n} subscribers...");
|
||||||
|
await TeardownAll(subscriberWhs);
|
||||||
|
await Task.Delay(settleSec * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- per-N aggregation ----------
|
||||||
|
if (perRepResults.Count > 0)
|
||||||
|
{
|
||||||
|
double meanOfMeans = perRepResults.Average(r => r.MeanPerSub);
|
||||||
|
double ciHalfWidth = ConfidenceIntervalHalfWidth95(
|
||||||
|
perRepResults.Select(r => r.MeanPerSub).ToArray());
|
||||||
|
|
||||||
|
Console.WriteLine($"\n[Orchestrator] N={n} SUMMARY: "
|
||||||
|
+ $"mean_per_sub={meanOfMeans:F1} ± {ciHalfWidth:F1} notif/s (95% CI)");
|
||||||
|
|
||||||
|
// Saturation detection: stop sweep if per-sub rate falls below
|
||||||
|
// 10% of theoretical OR server CPU peaked above 180% (>90% of 2 cores)
|
||||||
|
if (meanOfMeans < minAcceptableRate)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Orchestrator] *** SATURATION DETECTED: rate {meanOfMeans:F0} < {minAcceptableRate:F0} ***");
|
||||||
|
saturatedDetected = true;
|
||||||
|
}
|
||||||
|
else if (perRepResults.Average(r => r.ServerCpuPeak) > 180.0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Orchestrator] *** SATURATION DETECTED: server CPU peaked > 180% ***");
|
||||||
|
saturatedDetected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate row for CSV
|
||||||
|
allResults.Add(new SweepResult
|
||||||
|
{
|
||||||
|
N = n,
|
||||||
|
Replications = perRepResults.Count,
|
||||||
|
MeanPerSubRate = meanOfMeans,
|
||||||
|
Ci95HalfWidth = ciHalfWidth,
|
||||||
|
MeanAggregate = perRepResults.Average(r => r.Aggregate),
|
||||||
|
TotalLate = perRepResults.Sum(r => r.LateDeliveries),
|
||||||
|
MeanServerCpuAvg = perRepResults.Average(r => r.ServerCpuAvg),
|
||||||
|
MeanServerCpuPeak = perRepResults.Average(r => r.ServerCpuPeak),
|
||||||
|
MeanClientCpuAvg = perRepResults.Average(r => r.ClientCpuAvg),
|
||||||
|
MeanClientCpuPeak = perRepResults.Average(r => r.ClientCpuPeak),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Output
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
sb.AppendLine("n,replications,mean_per_sub_rate,ci95_halfwidth,mean_aggregate," +
|
||||||
|
"total_late,mean_server_cpu_avg,mean_server_cpu_peak,mean_client_cpu_avg,mean_client_cpu_peak");
|
||||||
|
foreach (var r in allResults)
|
||||||
|
{
|
||||||
|
sb.AppendLine(string.Create(CultureInfo.InvariantCulture,
|
||||||
|
$"{r.N},{r.Replications},{r.MeanPerSubRate:F2},{r.Ci95HalfWidth:F2}," +
|
||||||
|
$"{r.MeanAggregate:F1},{r.TotalLate},{r.MeanServerCpuAvg:F2},{r.MeanServerCpuPeak:F2}," +
|
||||||
|
$"{r.MeanClientCpuAvg:F2},{r.MeanClientCpuPeak:F2}"));
|
||||||
|
}
|
||||||
|
await File.WriteAllTextAsync(outputCsv, sb.ToString());
|
||||||
|
Console.WriteLine($"\n[Orchestrator] Results written to {outputCsv}");
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Subscriber spawn / teardown
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
static async Task<SubscriberTask?> SpawnSubscriber(
|
||||||
|
Warehouse wh, string host, int port, int resources, int subId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var conn = await wh.Get<EpConnection>($"ep://{host}:{port}");
|
||||||
|
var sub = new SubscriberTask { SubscriberId = subId };
|
||||||
|
|
||||||
|
for (int i = 0; i < resources; i++)
|
||||||
|
{
|
||||||
|
var proxy = await conn.Get($"sys/sensor_{i}");
|
||||||
|
long lastTick = Stopwatch.GetTimestamp();
|
||||||
|
|
||||||
|
proxy.Instance.PropertyModified += (PropertyModificationInfo data) =>
|
||||||
|
{
|
||||||
|
if (data.Name != "Value") return;
|
||||||
|
long now = Stopwatch.GetTimestamp();
|
||||||
|
double elapsedMs = (now - lastTick) * 1000.0 / Stopwatch.Frequency;
|
||||||
|
lastTick = now;
|
||||||
|
Interlocked.Increment(ref sub._received);
|
||||||
|
if (elapsedMs > 400) Interlocked.Increment(ref sub._lateDeliveries);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" [Spawn-{subId}] FAILED: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task TeardownAll(Warehouse[] whs)
|
||||||
|
{
|
||||||
|
foreach (var wh in whs)
|
||||||
|
{
|
||||||
|
try { await wh.Close(); }
|
||||||
|
catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Stats helpers
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
static double StdDev(double[] xs)
|
||||||
|
{
|
||||||
|
if (xs.Length < 2) return 0;
|
||||||
|
double mean = xs.Average();
|
||||||
|
double sumSq = xs.Sum(x => (x - mean) * (x - mean));
|
||||||
|
return Math.Sqrt(sumSq / (xs.Length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 95% confidence interval half-width using Student's t-distribution.
|
||||||
|
/// For very small samples (n < 3) returns 0 (not enough data).
|
||||||
|
/// t values for 95% two-sided are hard-coded; see standard tables.
|
||||||
|
/// </summary>
|
||||||
|
static double ConfidenceIntervalHalfWidth95(double[] xs)
|
||||||
|
{
|
||||||
|
int n = xs.Length;
|
||||||
|
if (n < 2) return 0;
|
||||||
|
double std = StdDev(xs);
|
||||||
|
double sem = std / Math.Sqrt(n);
|
||||||
|
// t for df=n-1, two-sided 95%
|
||||||
|
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,
|
||||||
|
_ => 1.960 // normal approximation
|
||||||
|
};
|
||||||
|
return t * sem;
|
||||||
|
}
|
||||||
|
|
||||||
static string GetArg(string[] args, string key, string def)
|
static string GetArg(string[] args, string key, string def)
|
||||||
{
|
{
|
||||||
var i = Array.IndexOf(args, key);
|
int i = Array.IndexOf(args, key);
|
||||||
return (i >= 0 && i + 1 < args.Length) ? args[i + 1] : def;
|
return (i >= 0 && i + 1 < args.Length) ? args[i + 1] : def;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Records
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
class SubscriberTask
|
||||||
|
{
|
||||||
|
public int SubscriberId;
|
||||||
|
internal long _received;
|
||||||
|
internal long _lateDeliveries;
|
||||||
|
public long Received => Interlocked.Read(ref _received);
|
||||||
|
public long LateDeliveries => Interlocked.Read(ref _lateDeliveries);
|
||||||
|
public void ResetCounters()
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _received, 0);
|
||||||
|
Interlocked.Exchange(ref _lateDeliveries, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record RepResult
|
||||||
|
{
|
||||||
|
public int N;
|
||||||
|
public int Rep;
|
||||||
|
public double MeanPerSub;
|
||||||
|
public double StdPerSub;
|
||||||
|
public double MinPerSub;
|
||||||
|
public double MaxPerSub;
|
||||||
|
public double Aggregate;
|
||||||
|
public long LateDeliveries;
|
||||||
|
public double ServerCpuAvg;
|
||||||
|
public double ServerCpuPeak;
|
||||||
|
public double ClientCpuAvg;
|
||||||
|
public double ClientCpuPeak;
|
||||||
|
}
|
||||||
|
|
||||||
|
record SweepResult
|
||||||
|
{
|
||||||
|
public int N;
|
||||||
|
public int Replications;
|
||||||
|
public double MeanPerSubRate;
|
||||||
|
public double Ci95HalfWidth;
|
||||||
|
public double MeanAggregate;
|
||||||
|
public long TotalLate;
|
||||||
|
public double MeanServerCpuAvg;
|
||||||
|
public double MeanServerCpuPeak;
|
||||||
|
public double MeanClientCpuAvg;
|
||||||
|
public double MeanClientCpuPeak;
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\Libraries\Esiur\Esiur.csproj" />
|
<ProjectReference Include="..\..\..\..\Libraries\Esiur\Esiur.csproj" OutputItemType="Analyzer" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,427 +1,77 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// Scalability Extension: Fan-Out — ORCHESTRATOR CLIENT
|
// Scalability Extension: Fan-Out — SERVER NODE
|
||||||
// ------------------------------------------------------------
|
// Hosts M sensor resources and emits Value updates at a fixed interval (the fan-out source). Also
|
||||||
// Drives a full sweep of subscriber counts N against a single
|
// hosts sys/control, updated once per second with the server process CPU (% across all cores) and
|
||||||
// server instance. For each N value:
|
// the live subscriber count, which the sweep orchestrator reads to characterise saturation.
|
||||||
// 1. Spawns N in-process subscriber tasks, each opening its
|
// Anonymous (None-mode) access so subscribers connect without credentials.
|
||||||
// own EpConnection to the server.
|
|
||||||
// 2. Each subscriber attaches to all M resources and counts
|
|
||||||
// property-change notifications it receives over a fixed
|
|
||||||
// measurement window.
|
|
||||||
// 3. The orchestrator polls the server's sys/control resource
|
|
||||||
// to capture server-side CPU during the window.
|
|
||||||
// 4. Tears down all N subscribers and waits a settle interval
|
|
||||||
// before the next sweep point.
|
|
||||||
// 5. Repeats for `replications` rounds so the per-N mean and
|
|
||||||
// 95% confidence interval can be computed.
|
|
||||||
// 6. Auto-stops the sweep if either:
|
|
||||||
// - mean per-subscriber rate drops below 10% of theoretical,
|
|
||||||
// - or server CPU stays at >180% (>90% of 2 cores) for the
|
|
||||||
// entire measurement window.
|
|
||||||
//
|
//
|
||||||
// Note on in-process vs separate processes: subscribers are
|
// Usage: dotnet run -- --port 10900 --resources 100 --emit-interval-ms 50
|
||||||
// tasks within a single client process to keep the test self-
|
// (Run the orchestrator from the sibling "Server" project against this host:port.)
|
||||||
// contained and avoid spawning N OS processes. Each task uses
|
|
||||||
// its own EpConnection (TCP connection) to the server, so from
|
|
||||||
// the server's perspective the load looks identical to N
|
|
||||||
// distinct subscriber nodes for the property-propagation path.
|
|
||||||
// The single-client-process design does mean that the client
|
|
||||||
// host's CPU is shared across all subscribers; the orchestrator
|
|
||||||
// records this too so degradation can be attributed correctly.
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
// Usage:
|
|
||||||
// dotnet run -- --host 127.0.0.1 --port 10900 --resources 100 \
|
|
||||||
// --emit-interval-ms 50 --window-sec 60 \
|
|
||||||
// --warmup-sec 5 --replications 3 \
|
|
||||||
// --n-values 2,5,10,20,50,100,200,500
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
using Esiur.Protocol;
|
using Esiur.Protocol;
|
||||||
using Esiur.Resource;
|
using Esiur.Resource;
|
||||||
using System.Data.Common;
|
using Esiur.Stores;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
var host = GetArg(args, "--host", "127.0.0.1");
|
var port = int.Parse(GetArg(args, "--port", "10900"));
|
||||||
var port = int.Parse(GetArg(args, "--port", "10900"));
|
var resources = int.Parse(GetArg(args, "--resources", "100"));
|
||||||
var resources = int.Parse(GetArg(args, "--resources", "100"));
|
|
||||||
var emitIntervalMs = int.Parse(GetArg(args, "--emit-interval-ms", "50"));
|
var emitIntervalMs = int.Parse(GetArg(args, "--emit-interval-ms", "50"));
|
||||||
var windowSec = int.Parse(GetArg(args, "--window-sec", "60"));
|
|
||||||
var warmupSec = int.Parse(GetArg(args, "--warmup-sec", "5"));
|
|
||||||
var settleSec = int.Parse(GetArg(args, "--settle-sec", "5"));
|
|
||||||
var replications = int.Parse(GetArg(args, "--replications", "3"));
|
|
||||||
var nValuesStr = GetArg(args, "--n-values", "2,5,10,20,50,100,200,500");
|
|
||||||
var outputCsv = GetArg(args, "--output", "fanout_sweep_results.csv");
|
|
||||||
|
|
||||||
var nValues = nValuesStr.Split(',').Select(int.Parse).ToArray();
|
Console.WriteLine($"[Server] resources={resources} emit-interval={emitIntervalMs}ms port={port} cores={Environment.ProcessorCount}");
|
||||||
double theoreticalMaxRate = 1000.0 / emitIntervalMs * resources;
|
|
||||||
double minAcceptableRate = theoreticalMaxRate * 0.10;
|
|
||||||
|
|
||||||
Console.WriteLine($"[Orchestrator] resources={resources} interval={emitIntervalMs}ms "
|
var wh = new Warehouse();
|
||||||
+ $"window={windowSec}s replications={replications}");
|
await wh.Put("sys", new MemoryStore());
|
||||||
Console.WriteLine($"[Orchestrator] theoretical_max_per_subscriber_rate={theoreticalMaxRate:F0} notif/s");
|
var server = await wh.Put("sys/server", new EpServer { Port = (ushort)port, AllowUnauthorizedAccess = true });
|
||||||
Console.WriteLine($"[Orchestrator] saturation_threshold={minAcceptableRate:F0} notif/s");
|
|
||||||
Console.WriteLine($"[Orchestrator] N values: {string.Join(",", nValues)}");
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
var sensors = new SensorResource[resources];
|
||||||
// Attach to the server's control resource once.
|
for (var i = 0; i < resources; i++) { sensors[i] = new SensorResource { SensorId = i }; await wh.Put($"sys/sensor_{i}", sensors[i]); }
|
||||||
// ----------------------------------------------------------------
|
|
||||||
var controlWh = new Warehouse();
|
var control = new ControlResource();
|
||||||
EpResource? control = null;
|
await wh.Put("sys/control", control);
|
||||||
byte cpuIdx = 255, clientsIdx = 255;
|
|
||||||
try
|
await wh.Open();
|
||||||
|
Console.WriteLine($"[Server] Listening on port {port} with {resources} sensors + sys/control. Press Ctrl+C to stop.");
|
||||||
|
|
||||||
|
// Emit loop: drives property-change notifications to every attached subscriber.
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var controlConn = await controlWh.Get<EpConnection>($"ep://{host}:{port}");
|
while (true)
|
||||||
control = (EpResource)await controlConn.Get("sys/control");
|
|
||||||
// Resolve property indices by name (EpResource exposes values by index, not dynamic member).
|
|
||||||
var props = control.Instance.Definition.Properties;
|
|
||||||
cpuIdx = (byte)Array.FindIndex(props, p => p.Name == "CpuPercent");
|
|
||||||
clientsIdx = (byte)Array.FindIndex(props, p => p.Name == "ConnectedClients");
|
|
||||||
Console.WriteLine($"[Orchestrator] sys/control attached (CpuPercent=idx {cpuIdx}, ConnectedClients=idx {clientsIdx}).");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[Orchestrator] WARNING: could not attach to sys/control: {ex.Message}");
|
|
||||||
Console.WriteLine("[Orchestrator] Server CPU will be reported as N/A.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
// All sweep points x replications, with per-N early-stop logic.
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
var allResults = new List<SweepResult>();
|
|
||||||
bool saturatedDetected = false;
|
|
||||||
|
|
||||||
foreach (int n in nValues)
|
|
||||||
{
|
|
||||||
if (saturatedDetected)
|
|
||||||
{
|
{
|
||||||
Console.WriteLine($"\n[Orchestrator] N={n}: SKIPPED (saturation reached at lower N)");
|
await Task.Delay(emitIntervalMs);
|
||||||
continue;
|
var value = sw.Elapsed.TotalSeconds;
|
||||||
|
foreach (var s in sensors) s.Value = value;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var perRepResults = new List<RepResult>();
|
// Telemetry loop: publish server CPU (% across all cores) and subscriber count once per second.
|
||||||
|
_ = Task.Run(async () =>
|
||||||
for (int rep = 0; rep < replications; rep++)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"\n[Orchestrator] === N={n} rep={rep + 1}/{replications} ===");
|
|
||||||
|
|
||||||
var subscribers = new SubscriberTask[n];
|
|
||||||
var subscriberWhs = new Warehouse[n];
|
|
||||||
|
|
||||||
// ---------- spawn N subscribers ----------
|
|
||||||
Console.WriteLine($"[Orchestrator] Spawning {n} subscribers...");
|
|
||||||
var spawnSw = Stopwatch.StartNew();
|
|
||||||
var spawnTasks = new Task<SubscriberTask?>[n];
|
|
||||||
for (int i = 0; i < n; i++)
|
|
||||||
{
|
|
||||||
int captured = i;
|
|
||||||
subscriberWhs[i] = new Warehouse();
|
|
||||||
spawnTasks[i] = SpawnSubscriber(subscriberWhs[i], host, port, resources, captured);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(spawnTasks);
|
|
||||||
|
|
||||||
bool spawnFailed = false;
|
|
||||||
for (int i = 0; i < n; i++)
|
|
||||||
{
|
|
||||||
if (spawnTasks[i].Result == null) { spawnFailed = true; break; }
|
|
||||||
subscribers[i] = spawnTasks[i].Result!;
|
|
||||||
}
|
|
||||||
spawnSw.Stop();
|
|
||||||
|
|
||||||
if (spawnFailed)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[Orchestrator] N={n}: spawn failed; treating as saturation.");
|
|
||||||
saturatedDetected = true;
|
|
||||||
await TeardownAll(subscriberWhs);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Console.WriteLine($"[Orchestrator] All {n} subscribers attached in {spawnSw.Elapsed.TotalSeconds:F2}s");
|
|
||||||
|
|
||||||
// ---------- warmup ----------
|
|
||||||
Console.WriteLine($"[Orchestrator] Warmup {warmupSec}s...");
|
|
||||||
await Task.Delay(warmupSec * 1000);
|
|
||||||
foreach (var s in subscribers) s.ResetCounters();
|
|
||||||
|
|
||||||
// ---------- measurement window with CPU sampling ----------
|
|
||||||
Console.WriteLine($"[Orchestrator] Measurement window {windowSec}s...");
|
|
||||||
var cpuSamples = new List<double>();
|
|
||||||
var connSamples = new List<int>();
|
|
||||||
var clientCpuSamples = new List<double>();
|
|
||||||
var clientProc = Process.GetCurrentProcess();
|
|
||||||
var prevClientCpu = clientProc.TotalProcessorTime;
|
|
||||||
var prevClientWall = DateTime.UtcNow;
|
|
||||||
var winSw = Stopwatch.StartNew();
|
|
||||||
while (winSw.Elapsed.TotalSeconds < windowSec)
|
|
||||||
{
|
|
||||||
await Task.Delay(1000);
|
|
||||||
|
|
||||||
// Server CPU + subscriber count via the control resource (read by property index;
|
|
||||||
// values arrive as variable-width numerics, hence Convert.*).
|
|
||||||
if (control != null && cpuIdx != 255)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (control.TryGetPropertyValue(cpuIdx, out var cpuVal) && cpuVal != null)
|
|
||||||
cpuSamples.Add(Convert.ToDouble(cpuVal));
|
|
||||||
if (control.TryGetPropertyValue(clientsIdx, out var cliVal) && cliVal != null)
|
|
||||||
connSamples.Add(Convert.ToInt32(cliVal));
|
|
||||||
}
|
|
||||||
catch { /* control resource may not have a current value yet */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// This harness's own CPU (% across all cores). Recorded so saturation can be attributed
|
|
||||||
// to the server rather than to the single subscriber process driving N connections.
|
|
||||||
clientProc.Refresh();
|
|
||||||
var nowClientCpu = clientProc.TotalProcessorTime;
|
|
||||||
var nowClientWall = DateTime.UtcNow;
|
|
||||||
var wallMs = (nowClientWall - prevClientWall).TotalMilliseconds;
|
|
||||||
if (wallMs > 0) clientCpuSamples.Add((nowClientCpu - prevClientCpu).TotalMilliseconds / wallMs * 100.0);
|
|
||||||
prevClientCpu = nowClientCpu;
|
|
||||||
prevClientWall = nowClientWall;
|
|
||||||
}
|
|
||||||
|
|
||||||
double elapsedSec = winSw.Elapsed.TotalSeconds;
|
|
||||||
|
|
||||||
// ---------- collect per-subscriber counts ----------
|
|
||||||
var perSubRates = new double[n];
|
|
||||||
long totalReceived = 0;
|
|
||||||
long totalLate = 0;
|
|
||||||
for (int i = 0; i < n; i++)
|
|
||||||
{
|
|
||||||
perSubRates[i] = subscribers[i].Received / elapsedSec;
|
|
||||||
totalReceived += subscribers[i].Received;
|
|
||||||
totalLate += subscribers[i].LateDeliveries;
|
|
||||||
}
|
|
||||||
|
|
||||||
double meanPerSub = perSubRates.Average();
|
|
||||||
double stdPerSub = StdDev(perSubRates);
|
|
||||||
double minPerSub = perSubRates.Min();
|
|
||||||
double maxPerSub = perSubRates.Max();
|
|
||||||
double aggregate = perSubRates.Sum();
|
|
||||||
double avgServerCpu = cpuSamples.Count > 0 ? cpuSamples.Average() : double.NaN;
|
|
||||||
double peakServerCpu = cpuSamples.Count > 0 ? cpuSamples.Max() : double.NaN;
|
|
||||||
double avgClientCpu = clientCpuSamples.Count > 0 ? clientCpuSamples.Average() : double.NaN;
|
|
||||||
double peakClientCpu = clientCpuSamples.Count > 0 ? clientCpuSamples.Max() : double.NaN;
|
|
||||||
|
|
||||||
Console.WriteLine($"[Orchestrator] N={n} rep={rep + 1}: "
|
|
||||||
+ $"mean_per_sub={meanPerSub:F1}/s "
|
|
||||||
+ $"aggregate={aggregate:F0}/s "
|
|
||||||
+ $"late={totalLate} "
|
|
||||||
+ $"server_cpu_avg={avgServerCpu:F1}%/peak={peakServerCpu:F1}% "
|
|
||||||
+ $"client_cpu_avg={avgClientCpu:F1}%/peak={peakClientCpu:F1}%");
|
|
||||||
|
|
||||||
perRepResults.Add(new RepResult
|
|
||||||
{
|
|
||||||
N = n,
|
|
||||||
Rep = rep + 1,
|
|
||||||
MeanPerSub = meanPerSub,
|
|
||||||
StdPerSub = stdPerSub,
|
|
||||||
MinPerSub = minPerSub,
|
|
||||||
MaxPerSub = maxPerSub,
|
|
||||||
Aggregate = aggregate,
|
|
||||||
LateDeliveries = totalLate,
|
|
||||||
ServerCpuAvg = avgServerCpu,
|
|
||||||
ServerCpuPeak = peakServerCpu,
|
|
||||||
ClientCpuAvg = avgClientCpu,
|
|
||||||
ClientCpuPeak = peakClientCpu,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------- teardown ----------
|
|
||||||
Console.WriteLine($"[Orchestrator] Tearing down {n} subscribers...");
|
|
||||||
await TeardownAll(subscriberWhs);
|
|
||||||
await Task.Delay(settleSec * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- per-N aggregation ----------
|
|
||||||
if (perRepResults.Count > 0)
|
|
||||||
{
|
|
||||||
double meanOfMeans = perRepResults.Average(r => r.MeanPerSub);
|
|
||||||
double ciHalfWidth = ConfidenceIntervalHalfWidth95(
|
|
||||||
perRepResults.Select(r => r.MeanPerSub).ToArray());
|
|
||||||
|
|
||||||
Console.WriteLine($"\n[Orchestrator] N={n} SUMMARY: "
|
|
||||||
+ $"mean_per_sub={meanOfMeans:F1} ± {ciHalfWidth:F1} notif/s (95% CI)");
|
|
||||||
|
|
||||||
// Saturation detection: stop sweep if per-sub rate falls below
|
|
||||||
// 10% of theoretical OR server CPU peaked above 180% (>90% of 2 cores)
|
|
||||||
if (meanOfMeans < minAcceptableRate)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[Orchestrator] *** SATURATION DETECTED: rate {meanOfMeans:F0} < {minAcceptableRate:F0} ***");
|
|
||||||
saturatedDetected = true;
|
|
||||||
}
|
|
||||||
else if (perRepResults.Average(r => r.ServerCpuPeak) > 180.0)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[Orchestrator] *** SATURATION DETECTED: server CPU peaked > 180% ***");
|
|
||||||
saturatedDetected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aggregate row for CSV
|
|
||||||
allResults.Add(new SweepResult
|
|
||||||
{
|
|
||||||
N = n,
|
|
||||||
Replications = perRepResults.Count,
|
|
||||||
MeanPerSubRate = meanOfMeans,
|
|
||||||
Ci95HalfWidth = ciHalfWidth,
|
|
||||||
MeanAggregate = perRepResults.Average(r => r.Aggregate),
|
|
||||||
TotalLate = perRepResults.Sum(r => r.LateDeliveries),
|
|
||||||
MeanServerCpuAvg = perRepResults.Average(r => r.ServerCpuAvg),
|
|
||||||
MeanServerCpuPeak = perRepResults.Average(r => r.ServerCpuPeak),
|
|
||||||
MeanClientCpuAvg = perRepResults.Average(r => r.ClientCpuAvg),
|
|
||||||
MeanClientCpuPeak = perRepResults.Average(r => r.ClientCpuPeak),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
// Output
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
var sb = new System.Text.StringBuilder();
|
|
||||||
sb.AppendLine("n,replications,mean_per_sub_rate,ci95_halfwidth,mean_aggregate," +
|
|
||||||
"total_late,mean_server_cpu_avg,mean_server_cpu_peak,mean_client_cpu_avg,mean_client_cpu_peak");
|
|
||||||
foreach (var r in allResults)
|
|
||||||
{
|
{
|
||||||
sb.AppendLine(string.Create(CultureInfo.InvariantCulture,
|
var proc = Process.GetCurrentProcess();
|
||||||
$"{r.N},{r.Replications},{r.MeanPerSubRate:F2},{r.Ci95HalfWidth:F2}," +
|
var prevCpu = proc.TotalProcessorTime;
|
||||||
$"{r.MeanAggregate:F1},{r.TotalLate},{r.MeanServerCpuAvg:F2},{r.MeanServerCpuPeak:F2}," +
|
var prevWall = DateTime.UtcNow;
|
||||||
$"{r.MeanClientCpuAvg:F2},{r.MeanClientCpuPeak:F2}"));
|
while (true)
|
||||||
}
|
|
||||||
await File.WriteAllTextAsync(outputCsv, sb.ToString());
|
|
||||||
Console.WriteLine($"\n[Orchestrator] Results written to {outputCsv}");
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
// Subscriber spawn / teardown
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
static async Task<SubscriberTask?> SpawnSubscriber(
|
|
||||||
Warehouse wh, string host, int port, int resources, int subId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var conn = await wh.Get<EpConnection>($"ep://{host}:{port}");
|
await Task.Delay(1000);
|
||||||
var sub = new SubscriberTask { SubscriberId = subId };
|
proc.Refresh();
|
||||||
|
var nowCpu = proc.TotalProcessorTime;
|
||||||
for (int i = 0; i < resources; i++)
|
var nowWall = DateTime.UtcNow;
|
||||||
{
|
var wallMs = (nowWall - prevWall).TotalMilliseconds;
|
||||||
var proxy = await conn.Get($"sys/sensor_{i}");
|
control.CpuPercent = wallMs > 0 ? (nowCpu - prevCpu).TotalMilliseconds / wallMs * 100.0 : 0;
|
||||||
long lastTick = Stopwatch.GetTimestamp();
|
control.ConnectedClients = server.Connections.Count;
|
||||||
|
prevCpu = nowCpu;
|
||||||
proxy.Instance.PropertyModified += (PropertyModificationInfo data) =>
|
prevWall = nowWall;
|
||||||
{
|
|
||||||
if (data.Name != "Value") return;
|
|
||||||
long now = Stopwatch.GetTimestamp();
|
|
||||||
double elapsedMs = (now - lastTick) * 1000.0 / Stopwatch.Frequency;
|
|
||||||
lastTick = now;
|
|
||||||
Interlocked.Increment(ref sub._received);
|
|
||||||
if (elapsedMs > 400) Interlocked.Increment(ref sub._lateDeliveries);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return sub;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
});
|
||||||
{
|
|
||||||
Console.WriteLine($" [Spawn-{subId}] FAILED: {ex.Message}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async Task TeardownAll(Warehouse[] whs)
|
var stop = new TaskCompletionSource();
|
||||||
{
|
Console.CancelKeyPress += (_, e) => { e.Cancel = true; stop.TrySetResult(); };
|
||||||
foreach (var wh in whs)
|
await stop.Task;
|
||||||
{
|
await wh.Close();
|
||||||
try { await wh.Close(); }
|
|
||||||
catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
// Stats helpers
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
static double StdDev(double[] xs)
|
|
||||||
{
|
|
||||||
if (xs.Length < 2) return 0;
|
|
||||||
double mean = xs.Average();
|
|
||||||
double sumSq = xs.Sum(x => (x - mean) * (x - mean));
|
|
||||||
return Math.Sqrt(sumSq / (xs.Length - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 95% confidence interval half-width using Student's t-distribution.
|
|
||||||
/// For very small samples (n < 3) returns 0 (not enough data).
|
|
||||||
/// t values for 95% two-sided are hard-coded; see standard tables.
|
|
||||||
/// </summary>
|
|
||||||
static double ConfidenceIntervalHalfWidth95(double[] xs)
|
|
||||||
{
|
|
||||||
int n = xs.Length;
|
|
||||||
if (n < 2) return 0;
|
|
||||||
double std = StdDev(xs);
|
|
||||||
double sem = std / Math.Sqrt(n);
|
|
||||||
// t for df=n-1, two-sided 95%
|
|
||||||
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,
|
|
||||||
_ => 1.960 // normal approximation
|
|
||||||
};
|
|
||||||
return t * sem;
|
|
||||||
}
|
|
||||||
|
|
||||||
static string GetArg(string[] args, string key, string def)
|
static string GetArg(string[] args, string key, string def)
|
||||||
{
|
{
|
||||||
int i = Array.IndexOf(args, key);
|
var i = Array.IndexOf(args, key);
|
||||||
return (i >= 0 && i + 1 < args.Length) ? args[i + 1] : def;
|
return (i >= 0 && i + 1 < args.Length) ? args[i + 1] : def;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
// Records
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
class SubscriberTask
|
|
||||||
{
|
|
||||||
public int SubscriberId;
|
|
||||||
internal long _received;
|
|
||||||
internal long _lateDeliveries;
|
|
||||||
public long Received => Interlocked.Read(ref _received);
|
|
||||||
public long LateDeliveries => Interlocked.Read(ref _lateDeliveries);
|
|
||||||
public void ResetCounters()
|
|
||||||
{
|
|
||||||
Interlocked.Exchange(ref _received, 0);
|
|
||||||
Interlocked.Exchange(ref _lateDeliveries, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record RepResult
|
|
||||||
{
|
|
||||||
public int N;
|
|
||||||
public int Rep;
|
|
||||||
public double MeanPerSub;
|
|
||||||
public double StdPerSub;
|
|
||||||
public double MinPerSub;
|
|
||||||
public double MaxPerSub;
|
|
||||||
public double Aggregate;
|
|
||||||
public long LateDeliveries;
|
|
||||||
public double ServerCpuAvg;
|
|
||||||
public double ServerCpuPeak;
|
|
||||||
public double ClientCpuAvg;
|
|
||||||
public double ClientCpuPeak;
|
|
||||||
}
|
|
||||||
|
|
||||||
record SweepResult
|
|
||||||
{
|
|
||||||
public int N;
|
|
||||||
public int Replications;
|
|
||||||
public double MeanPerSubRate;
|
|
||||||
public double Ci95HalfWidth;
|
|
||||||
public double MeanAggregate;
|
|
||||||
public long TotalLate;
|
|
||||||
public double MeanServerCpuAvg;
|
|
||||||
public double MeanServerCpuPeak;
|
|
||||||
public double MeanClientCpuAvg;
|
|
||||||
public double MeanClientCpuPeak;
|
|
||||||
}
|
|
||||||
@@ -7,14 +7,6 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Remove="Program.cs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="Program.cs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\..\Libraries\Esiur\Esiur.csproj" />
|
<ProjectReference Include="..\..\..\..\Libraries\Esiur\Esiur.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// Test 4: Fork-Join Queueing Test — CLIENT NODE
|
// Test 4: Fork-Join Queueing Test — CLIENT NODE (REPLICATED)
|
||||||
//
|
//
|
||||||
// Usage: dotnet run -- --host 127.0.0.1 --port 10901 --trials 10000
|
// 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.Data;
|
||||||
@@ -9,85 +25,152 @@ using Esiur.Protocol;
|
|||||||
using Esiur.Resource;
|
using Esiur.Resource;
|
||||||
using Esiur.Tests.Queueing.Client;
|
using Esiur.Tests.Queueing.Client;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Globalization;
|
||||||
using System.Diagnostics.Metrics;
|
using System.IO;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
var results = new List<EsiurQueueEval.EvalResult>();
|
|
||||||
int counter = 0;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int currentAlpha = 0;
|
|
||||||
int currentDelay = 0;
|
|
||||||
|
|
||||||
|
// ---------- arguments ----------
|
||||||
var host = GetArg(args, "--host", "127.0.0.1");
|
var host = GetArg(args, "--host", "127.0.0.1");
|
||||||
var port = int.Parse(GetArg(args, "--port", "10901"));
|
var port = int.Parse(GetArg(args, "--port", "10901"));
|
||||||
var trials = int.Parse(GetArg(args, "--trials", "1000"));
|
var trials = int.Parse(GetArg(args, "--trials", "1000"));
|
||||||
var delays = GetArg(args, "--delays", "5:8:10:20:30:100")
|
var replications = int.Parse(GetArg(args, "--replications", "5"));
|
||||||
.Split(":").Select(x => Convert.ToInt32(x)).ToArray();
|
var settleMs = int.Parse(GetArg(args, "--settle-ms", "1000"));
|
||||||
var alphas = GetArg(args, "--alphas", "0.0:0.25:0.5:0.75:1")
|
var outputCsv = GetArg(args, "--output", "forkjoin_replicated.csv");
|
||||||
.Split(":").Select(y => Convert.ToDouble(y)).ToArray();
|
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");
|
||||||
|
|
||||||
Console.WriteLine($"[Client-T2] Connecting to {host}:{port}, trials={trials}");
|
// ---------- connect ----------
|
||||||
|
|
||||||
var wh = new Warehouse();
|
var wh = new Warehouse();
|
||||||
|
var serviceResource = await wh.Get<EpResource>($"ep://{host}:{port}/sys/queueing");
|
||||||
var serviceResource = await wh.Get<EpResource>(
|
|
||||||
$"ep://{host}:{port}/sys/queueing");
|
|
||||||
|
|
||||||
var service = (dynamic)serviceResource;
|
var service = (dynamic)serviceResource;
|
||||||
|
|
||||||
serviceResource.PropertyChanged += Service_PropertyChanged;
|
// ---------- 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) =>
|
||||||
Console.WriteLine("Starting test: Delay=" + delays[currentDelay] + " Alpha=" + alphas[currentAlpha]);
|
|
||||||
|
|
||||||
service.StartUpdatesLocal(delays[currentDelay], trials, alphas[currentAlpha]);
|
|
||||||
|
|
||||||
await Task.Delay(-1);
|
|
||||||
|
|
||||||
|
|
||||||
void Service_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
||||||
{
|
{
|
||||||
counter++;
|
int n = Interlocked.Increment(ref eventsThisRep);
|
||||||
|
if (n == trials)
|
||||||
if (counter == trials)
|
|
||||||
{
|
{
|
||||||
var queue = service.DistributedResourceConnection.GetFinishedQueue();
|
repDone.TrySetResult(true);
|
||||||
var result = EsiurQueueEval.Evaluate(queue);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Console.WriteLine(result);
|
// ---------- main sweep ----------
|
||||||
counter = 0;
|
var rows = new List<ReplicatedResult>();
|
||||||
|
|
||||||
if (currentAlpha == alphas.Length - 1)
|
using var writer = new StreamWriter(outputCsv);
|
||||||
{
|
writer.WriteLine(ReplicatedEvalAggregator.CsvHeader);
|
||||||
currentAlpha = 0;
|
writer.Flush();
|
||||||
currentDelay++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
currentAlpha++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentDelay == delays.Length)
|
|
||||||
{
|
|
||||||
System.Environment.Exit(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
foreach (var delay in delays)
|
||||||
|
{
|
||||||
|
foreach (var alpha in alphas)
|
||||||
|
{
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine("Starting next test: Delay=" + delays[currentDelay] + " Alpha=" + alphas[currentAlpha]);
|
Console.WriteLine($"[Client-T4-R] >>> delay={delay} ms α={alpha:F2} " +
|
||||||
|
$"(running {replications} replications) <<<");
|
||||||
|
|
||||||
service.StartUpdatesLocal(delays[currentDelay], trials, alphas[currentAlpha]);//, 0, resourceLink);
|
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.ResourceConnection.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.ResourceConnection.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)
|
static string GetArg(string[] args, string key, string def)
|
||||||
{
|
{
|
||||||
int i = Array.IndexOf(args, key);
|
int i = Array.IndexOf(args, key);
|
||||||
return (i >= 0 && i + 1 < args.Length) ? args[i + 1] : def;
|
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));
|
||||||
|
}
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
// ============================================================
|
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,9 @@ Console.WriteLine($"[Server] Listening on port {port}...");
|
|||||||
var wh = Warehouse.Default;
|
var wh = Warehouse.Default;
|
||||||
var mem = await wh.Put("sys", new MemoryStore());
|
var mem = await wh.Put("sys", new MemoryStore());
|
||||||
var service = await wh.Put("sys/queueing", new QueueingService());
|
var service = await wh.Put("sys/queueing", new QueueingService());
|
||||||
var server = await wh.Put("sys/server", new EpServer() { Port = (ushort)port,
|
var server = await wh.Put("sys/server", new EpServer() { Port = (ushort)port,
|
||||||
EntryPoint = service });
|
EntryPoint = service,
|
||||||
|
AllowUnauthorizedAccess = true });
|
||||||
|
|
||||||
|
|
||||||
long memBefore = GC.GetTotalMemory(forceFullCollection: true);
|
long memBefore = GC.GetTotalMemory(forceFullCollection: true);
|
||||||
|
|||||||
Reference in New Issue
Block a user