Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Lighthouse.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
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" />

Copy link
Copy Markdown
Member

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)

</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Lighthouse\Lighthouse.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="akka.hocon">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
164 changes: 164 additions & 0 deletions src/Lighthouse.Tests.Benchmark/Program.cs
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we start 1 Lighthouse node in-proc and another remotely via ExecFunction?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internally, CpuUsage uses an extern call to this method in kernel32.dll:
https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesstimes

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;
}
}
}
38 changes: 38 additions & 0 deletions src/Lighthouse.Tests.Benchmark/akka.hocon
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]
}
}
17 changes: 10 additions & 7 deletions src/Lighthouse/LighthouseHostFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need to add this back?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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()));
Expand All @@ -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);
Expand Down