From ae5c5c574f95be6977c42094e87e10fd2ada1278 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Fri, 21 Jan 2022 07:41:04 +0700 Subject: [PATCH] Implement CPU benchmark project --- Lighthouse.sln | 6 + .../Lighthouse.Tests.Benchmark.csproj | 23 +++ src/Lighthouse.Tests.Benchmark/Program.cs | 164 ++++++++++++++++++ src/Lighthouse.Tests.Benchmark/akka.hocon | 38 ++++ src/Lighthouse/LighthouseHostFactory.cs | 17 +- 5 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 src/Lighthouse.Tests.Benchmark/Lighthouse.Tests.Benchmark.csproj create mode 100644 src/Lighthouse.Tests.Benchmark/Program.cs create mode 100644 src/Lighthouse.Tests.Benchmark/akka.hocon diff --git a/Lighthouse.sln b/Lighthouse.sln index f1f9f3cf..6d4ef30b 100644 --- a/Lighthouse.sln +++ b/Lighthouse.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{183986D1 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lighthouse", "src\Lighthouse\Lighthouse.csproj", "{5CA6B9E7-DAF2-47BD-9C91-7E979B4935A1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lighthouse.Tests.Benchmark", "src\Lighthouse.Tests.Benchmark\Lighthouse.Tests.Benchmark.csproj", "{652331F0-292D-4EDF-B1A0-D8F6DC486DCE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,6 +31,10 @@ Global {5CA6B9E7-DAF2-47BD-9C91-7E979B4935A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {5CA6B9E7-DAF2-47BD-9C91-7E979B4935A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {5CA6B9E7-DAF2-47BD-9C91-7E979B4935A1}.Release|Any CPU.Build.0 = Release|Any CPU + {652331F0-292D-4EDF-B1A0-D8F6DC486DCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {652331F0-292D-4EDF-B1A0-D8F6DC486DCE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {652331F0-292D-4EDF-B1A0-D8F6DC486DCE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {652331F0-292D-4EDF-B1A0-D8F6DC486DCE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Lighthouse.Tests.Benchmark/Lighthouse.Tests.Benchmark.csproj b/src/Lighthouse.Tests.Benchmark/Lighthouse.Tests.Benchmark.csproj new file mode 100644 index 00000000..8c1ece88 --- /dev/null +++ b/src/Lighthouse.Tests.Benchmark/Lighthouse.Tests.Benchmark.csproj @@ -0,0 +1,23 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/Lighthouse.Tests.Benchmark/Program.cs b/src/Lighthouse.Tests.Benchmark/Program.cs new file mode 100644 index 00000000..87e6e014 --- /dev/null +++ b/src/Lighthouse.Tests.Benchmark/Program.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Configuration; +using Tmds.Utils; +using Universe.CpuUsage; + +namespace Lighthouse.Tests.Benchmark +{ + public static class Program + { + private const int TestLength = 30; // in seconds + private const int TestDelay = 5; // in seconds + private const int TestRepeat = 20; + private const int TestClusterSize = 10; + + private const string ActorSystemName = "CpuTest"; + private const string LighthouseAddress = "127.0.0.1"; + private const int LighthousePort = 54053; + + private static readonly List Usages = new List(); + private static readonly List Processes = new List(); + + public static async Task Main(string[] args) + { + // ExecFunction hook + if (ExecFunction.IsExecFunctionCommand(args)) + return ExecFunction.Program.Main(args); + + // Start lighthouse + var service = new LighthouseService(LighthouseAddress, LighthousePort, ActorSystemName); + service.Start(); + + var executor = new FunctionExecutor(o => + { + o.StartInfo.RedirectStandardError = true; + o.OnExit = p => + { + if (p.ExitCode != 0) + { + var message = + "Function execution failed with exit code: " + + $"{p.ExitCode}{Environment.NewLine}{p.StandardError.ReadToEnd()}"; + throw new Exception(message); + } + }; + }); + + // Spin up cluster nodes + for (var port = LighthousePort + 1; port < LighthousePort + 1 + TestClusterSize; ++port) + { + Processes.Add(executor.Start(LaunchNode, new [] + { + LighthouseAddress, + LighthousePort.ToString(), + ActorSystemName, + port.ToString() + })); + } + + // Wait until things settles down + await Task.Delay(TimeSpan.FromSeconds(TestDelay)); + + // Warm up + foreach (var _ in Enumerable.Range(0, 5)) + { + CpuUsage.GetByProcess(); + } + + // Start benchmark + foreach (var i in Enumerable.Range(1, TestRepeat)) + { + var start = CpuUsage.GetByProcess(); + await Task.Delay(TimeSpan.FromSeconds(TestLength)); + var end = CpuUsage.GetByProcess(); + var final = end - start; + + Console.WriteLine($"{i}. Cpu Usage: {final}"); + Usages.Add(final.Value); + } + + // Kill cluster node processes + foreach (var process in Processes) + { + process.Kill(); + process.Dispose(); + } + + // Stop lighthouse + await service.StopAsync(); + + // Generate report + var sb = new StringBuilder(); + sb.AppendLine("CPU Benchmark complete."); + sb.AppendLine(); + + var num = 1; + foreach (var usage in Usages) + { + sb.AppendLine($"{num++}. CPU usage: user: {usage.UserUsage.TotalMicroSeconds / 1000.0} ms, " + + $"kernel: {usage.KernelUsage.TotalMicroSeconds / 1000.0} ms, " + + $"total: {usage.TotalMicroSeconds / 1000.0} ms"); + } + + sb.AppendLine() + .AppendLine(" CPU | Mean | StdErr | StdDev | Median |") + .AppendLine("---- |----- |------- |------- |------- |") + .AppendLine(CalculateResult(Usages + .Select(u => u.UserUsage.TotalMicroSeconds) + .OrderBy(i => i).ToArray(), "User")) + .AppendLine(CalculateResult(Usages + .Select(u => u.KernelUsage.TotalMicroSeconds) + .OrderBy(i => i).ToArray(), "Kernel")); + + Console.WriteLine(sb.ToString()); + + return 0; + } + + private static string CalculateResult(long[] times, string name) + { + var medianIndex = times.Length / 2; + + var mean = times.Average(); + var stdDev = Math.Sqrt(times.Average(v => Math.Pow(v - mean, 2))); + var stdErr = stdDev / Math.Sqrt(times.Length); + double median; + if (times.Length % 2 == 0) + median = (times[medianIndex - 1] + times[medianIndex]) / 2.0; + else + median = times[medianIndex]; + + return $" {name} | {(mean / 1000.0):N3} ms | {(stdErr / 1000.0):N3} ms | {(stdDev / 1000.0):N3} ms | {(median / 1000.0):N3} ms |"; + } + + // Function will be deployed in separate processes as Main() + private static async Task LaunchNode(string[] args) + { + var seedAddress = args[1]; + var seedPort = int.Parse(args[2]); + var systemName = args[3]; + var port = int.Parse(args[4]); + + var config = ConfigurationFactory.ParseString($@" + akka.actor.provider = cluster + akka.cluster.seed-nodes = [""{new Address("akka.tcp", systemName, seedAddress.Trim(), seedPort)}""] + akka.remote.dot-netty.tcp {{ + public-hostname=""{seedAddress}"" + port={port} + }}"); + + var system = ActorSystem.Create(systemName, config); + + // wait forever until we get killed + await Task.Delay(TimeSpan.FromDays(1)); + + return 0; + } + } +} diff --git a/src/Lighthouse.Tests.Benchmark/akka.hocon b/src/Lighthouse.Tests.Benchmark/akka.hocon new file mode 100644 index 00000000..c5f51514 --- /dev/null +++ b/src/Lighthouse.Tests.Benchmark/akka.hocon @@ -0,0 +1,38 @@ +lighthouse{ + actorsystem: "actor-system" #POPULATE NAME OF YOUR ACTOR SYSTEM HERE +} + +# See petabridge.cmd configuration options here: https://cmd.petabridge.com/articles/install/host-configuration.html +petabridge.cmd{ + # default IP address used to listen for incoming petabridge.cmd client connections + # should be a safe default as it listens on "all network interfaces". + host = "0.0.0.0" + + # default port number used to listen for incoming petabridge.cmd client connections + port = 9110 +} + +akka { + actor { + provider = cluster + } + + remote { + log-remote-lifecycle-events = DEBUG + dot-netty.tcp { + transport-class = "Akka.Remote.Transport.DotNetty.TcpTransport, Akka.Remote" + applied-adapters = [] + transport-protocol = tcp + #will be populated with a dynamic host-name at runtime if left uncommented + # public-hostname = "127.0.0.1" + # hostname = "0.0.0.0" + # port = 4053 + } + } + + cluster { + #will inject this node as a self-seed node at run-time + seed-nodes = [] + roles = [lighthouse] + } +} \ No newline at end of file diff --git a/src/Lighthouse/LighthouseHostFactory.cs b/src/Lighthouse/LighthouseHostFactory.cs index c3fb11ad..ddcbc11e 100755 --- a/src/Lighthouse/LighthouseHostFactory.cs +++ b/src/Lighthouse/LighthouseHostFactory.cs @@ -24,13 +24,11 @@ public static ActorSystem LaunchLighthouse(string ipAddress = null, int? specifi { systemName = systemName ?? Environment.GetEnvironmentVariable("ACTORSYSTEM")?.Trim(); - - // Set environment variables for use inside Akka.Bootstrap.Docker - // If overrides were provided to this method. - //if (!string.IsNullOrEmpty(ipAddress)) Environment.SetEnvironmentVariable("CLUSTER_IP", ipAddress); - - //if (specifiedPort != null) - // Environment.SetEnvironmentVariable("CLUSTER_PORT", specifiedPort.Value.ToString()); + var argConfig = ""; + if (ipAddress != null) + argConfig += $"akka.remote.dot-netty.tcp.public-hostname = {ipAddress}\n"; + if (specifiedPort != null) + argConfig += $"akka.remote.dot-netty.tcp.port = {specifiedPort}"; var useDocker = !(IsNullOrEmpty(Environment.GetEnvironmentVariable("CLUSTER_IP")?.Trim()) || IsNullOrEmpty(Environment.GetEnvironmentVariable("CLUSTER_SEEDS")?.Trim())); @@ -41,6 +39,11 @@ public static ActorSystem LaunchLighthouse(string ipAddress = null, int? specifi if (useDocker) clusterConfig = clusterConfig.BootstrapFromDocker(); + // Values from method arguments should always win + if (!IsNullOrEmpty(argConfig)) + clusterConfig = ConfigurationFactory.ParseString(argConfig) + .WithFallback(clusterConfig); + var lighthouseConfig = clusterConfig.GetConfig("lighthouse"); if (lighthouseConfig != null && IsNullOrEmpty(systemName)) systemName = lighthouseConfig.GetString("actorsystem", systemName);