Repository navigation
Implement CPU benchmark project #237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFramework>netcoreapp3.1</TargetFramework> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Tmds.ExecFunction" Version="0.5.0" /> | ||
| <PackageReference Include="Universe.CpuUsage" Version="2.2.497.598" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\Lighthouse\Lighthouse.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <None Update="akka.hocon"> | ||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
| </None> | ||
| </ItemGroup> | ||
|
|
||
| </Project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<CpuUsage> Usages = new List<CpuUsage>(); | ||
| private static readonly List<Process> Processes = new List<Process>(); | ||
|
|
||
| public static async Task<int> Main(string[] args) | ||
| { | ||
| // ExecFunction hook | ||
| if (ExecFunction.IsExecFunctionCommand(args)) | ||
| return ExecFunction.Program.Main(args); | ||
|
|
||
| // Start lighthouse | ||
| var service = new LighthouseService(LighthouseAddress, LighthousePort, ActorSystemName); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we start 1 Lighthouse node in-proc and another remotely via
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now, its one lighthouse in the main benchmark program, and then it will spawn a cluster that uses said lighthose service. Right now it is spawning a cluster with 10 nodes, all using the lighthouse as the seed node. |
||
| 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)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense - wait until the cluster is formed. Log2(N) + 1 + [some fudge factor] is how many seconds it should take a cluster to form, where N is your target cluster size, FYI. |
||
|
|
||
| // Warm up | ||
| foreach (var _ in Enumerable.Range(0, 5)) | ||
| { | ||
| CpuUsage.GetByProcess(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of curiosity - how does this know which processes to profile? Does it just automatically profile child processes?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Internally, CpuUsage uses an extern call to this method in kernel32.dll: It only handle a single process, most probably not accounting the children processes, and CpuUsage automatically inserts the current process handler when calling the method |
||
| } | ||
|
|
||
| // 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<int> 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; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably need to add this back?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I already added it back, but instead of injecting it into the environment variable, I did a config fallback instead |
||
| 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); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, this was the package we mentioned in akkadotnet/akka.net#5400 (comment)