[tools] Stress test improvements (#5381)

This commit is contained in:
Mikel Blanchard 2024-02-23 15:50:19 -08:00 committed by GitHub
parent 7e0213ddbd
commit 73b6e30c1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 515 additions and 307 deletions

View File

@ -16,6 +16,7 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Console" + AssemblyInfo.PublicKey)]
[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol" + AssemblyInfo.PublicKey)]
[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests" + AssemblyInfo.PublicKey)]
[assembly: InternalsVisibleTo("OpenTelemetry.Tests.Stress.Metrics" + AssemblyInfo.PublicKey)]
#endif
#if SIGNED

View File

@ -3,21 +3,13 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>$(TargetFrameworksForTests)</TargetFrameworks>
<!-- this is temporary. will remove in future PR. -->
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\OpenTelemetry.Exporter.Prometheus.HttpListener.csproj" />
<ProjectReference Include="$(RepoRoot)\test\OpenTelemetry.Tests.Stress\OpenTelemetry.Tests.Stress.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Condition="'$(TargetFramework)' == '$(NetFrameworkMinimumSupportedVersion)'" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests.Stress\Skeleton.cs" Link="Includes\Skeleton.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\Utils.cs" Link="Includes\Utils.cs" />
</ItemGroup>

View File

@ -1,39 +1,55 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;
namespace OpenTelemetry.Tests.Stress;
public partial class Program
public static class Program
{
private static ILogger logger;
private static Payload payload = new Payload();
public static void Main()
public static int Main(string[] args)
{
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddOpenTelemetry(options =>
{
options.AddProcessor(new DummyProcessor());
});
});
logger = loggerFactory.CreateLogger<Program>();
Stress(prometheusPort: 9464);
return StressTestFactory.RunSynchronously<LogsStressTest>(args);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static void Run()
private sealed class LogsStressTest : StressTest<StressTestOptions>
{
logger.Log(
logLevel: LogLevel.Information,
eventId: 2,
state: payload,
exception: null,
formatter: (state, ex) => string.Empty);
private static readonly Payload Payload = new();
private readonly ILoggerFactory loggerFactory;
private readonly ILogger logger;
public LogsStressTest(StressTestOptions options)
: base(options)
{
this.loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddOpenTelemetry(options =>
{
options.AddProcessor(new DummyProcessor());
});
});
this.logger = this.loggerFactory.CreateLogger<LogsStressTest>();
}
protected override void RunWorkItemInParallel()
{
this.logger.Log(
logLevel: LogLevel.Information,
eventId: 2,
state: Payload,
exception: null,
formatter: (state, ex) => string.Empty);
}
protected override void Dispose(bool isDisposing)
{
if (isDisposing)
{
this.loggerFactory.Dispose();
}
base.Dispose(isDisposing);
}
}
}

View File

@ -3,23 +3,15 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>$(TargetFrameworksForTests)</TargetFrameworks>
<!-- this is temporary. will remove in future PR. -->
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Condition="'$(TargetFramework)' == '$(NetFrameworkMinimumSupportedVersion)'" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry\OpenTelemetry.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\OpenTelemetry.Exporter.Prometheus.HttpListener.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.OpenTelemetryProtocol\OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj" />
<ProjectReference Include="$(RepoRoot)\test\OpenTelemetry.Tests.Stress\OpenTelemetry.Tests.Stress.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\Utils.cs" Link="Includes\Utils.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests.Stress\Skeleton.cs" Link="Includes\Skeleton.cs" />
</ItemGroup>
</Project>

View File

@ -2,65 +2,140 @@
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics.Metrics;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using CommandLine;
using OpenTelemetry.Metrics;
namespace OpenTelemetry.Tests.Stress;
public partial class Program
public static class Program
{
private const int ArraySize = 10;
// Note: Uncomment the below line if you want to run Histogram stress test
private const int MaxHistogramMeasurement = 1000;
private static readonly Meter TestMeter = new(Utils.GetCurrentMethodName());
private static readonly Counter<long> TestCounter = TestMeter.CreateCounter<long>("TestCounter");
private static readonly string[] DimensionValues = new string[ArraySize];
private static readonly ThreadLocal<Random> ThreadLocalRandom = new(() => new Random());
// Note: Uncomment the below line if you want to run Histogram stress test
private static readonly Histogram<long> TestHistogram = TestMeter.CreateHistogram<long>("TestHistogram");
public static void Main()
private enum MetricsStressTestType
{
for (int i = 0; i < ArraySize; i++)
/// <summary>Histogram.</summary>
Histogram,
/// <summary>Counter.</summary>
Counter,
}
public static int Main(string[] args)
{
return StressTestFactory.RunSynchronously<MetricsStressTest, MetricsStressTestOptions>(args);
}
private sealed class MetricsStressTest : StressTest<MetricsStressTestOptions>
{
private const int ArraySize = 10;
private const int MaxHistogramMeasurement = 1000;
private static readonly Meter TestMeter = new(Utils.GetCurrentMethodName());
private static readonly Histogram<long> TestHistogram = TestMeter.CreateHistogram<long>("TestHistogram");
private static readonly Counter<long> TestCounter = TestMeter.CreateCounter<long>("TestCounter");
private static readonly string[] DimensionValues = new string[ArraySize];
private static readonly ThreadLocal<Random> ThreadLocalRandom = new(() => new Random());
private readonly MeterProvider meterProvider;
static MetricsStressTest()
{
DimensionValues[i] = $"DimValue{i}";
for (int i = 0; i < ArraySize; i++)
{
DimensionValues[i] = $"DimValue{i}";
}
}
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter(TestMeter.Name)
public MetricsStressTest(MetricsStressTestOptions options)
: base(options)
{
var builder = Sdk.CreateMeterProviderBuilder().AddMeter(TestMeter.Name);
// .SetExemplarFilter(new AlwaysOnExemplarFilter())
.AddPrometheusHttpListener(
options => options.UriPrefixes = new string[] { $"http://localhost:9185/" })
.Build();
if (options.PrometheusTestMetricsPort != 0)
{
builder.AddPrometheusHttpListener(o => o.UriPrefixes = new string[] { $"http://localhost:{options.PrometheusTestMetricsPort}/" });
}
Stress(prometheusPort: 9464);
if (options.EnableExemplars)
{
builder.SetExemplarFilter(new AlwaysOnExemplarFilter());
}
if (options.AddViewToFilterTags)
{
builder
.AddView("TestCounter", new MetricStreamConfiguration { TagKeys = new string[] { "DimName1" } })
.AddView("TestHistogram", new MetricStreamConfiguration { TagKeys = new string[] { "DimName1" } });
}
if (options.AddOtlpExporter)
{
builder.AddOtlpExporter((exporterOptions, readerOptions) =>
{
readerOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = options.OtlpExporterExportIntervalMilliseconds;
});
}
this.meterProvider = builder.Build();
}
protected override void WriteRunInformationToConsole()
{
if (this.Options.PrometheusTestMetricsPort != 0)
{
Console.Write($", testPrometheusEndpoint = http://localhost:{this.Options.PrometheusTestMetricsPort}/metrics/");
}
}
protected override void RunWorkItemInParallel()
{
var random = ThreadLocalRandom.Value!;
if (this.Options.TestType == MetricsStressTestType.Histogram)
{
TestHistogram.Record(
random.Next(MaxHistogramMeasurement),
new("DimName1", DimensionValues[random.Next(0, ArraySize)]),
new("DimName2", DimensionValues[random.Next(0, ArraySize)]),
new("DimName3", DimensionValues[random.Next(0, ArraySize)]));
}
else if (this.Options.TestType == MetricsStressTestType.Counter)
{
TestCounter.Add(
100,
new("DimName1", DimensionValues[random.Next(0, ArraySize)]),
new("DimName2", DimensionValues[random.Next(0, ArraySize)]),
new("DimName3", DimensionValues[random.Next(0, ArraySize)]));
}
}
protected override void Dispose(bool isDisposing)
{
if (isDisposing)
{
this.meterProvider.Dispose();
}
base.Dispose(isDisposing);
}
}
// Note: Uncomment the below lines if you want to run Counter stress test
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// protected static void Run()
// {
// var random = ThreadLocalRandom.Value;
// TestCounter.Add(
// 100,
// new("DimName1", DimensionValues[random.Next(0, ArraySize)]),
// new("DimName2", DimensionValues[random.Next(0, ArraySize)]),
// new("DimName3", DimensionValues[random.Next(0, ArraySize)]));
// }
// Note: Uncomment the below lines if you want to run Histogram stress test
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static void Run()
private sealed class MetricsStressTestOptions : StressTestOptions
{
var random = ThreadLocalRandom.Value;
TestHistogram.Record(
random.Next(MaxHistogramMeasurement),
new("DimName1", DimensionValues[random.Next(0, ArraySize)]),
new("DimName2", DimensionValues[random.Next(0, ArraySize)]),
new("DimName3", DimensionValues[random.Next(0, ArraySize)]));
[JsonConverter(typeof(JsonStringEnumConverter))]
[Option('t', "type", HelpText = "The metrics stress test type to run. Valid values: [Histogram, Counter]. Default value: Histogram.", Required = false)]
public MetricsStressTestType TestType { get; set; } = MetricsStressTestType.Histogram;
[Option('m', "metrics_port", HelpText = "The Prometheus http listener port where Prometheus will be exposed for retrieving test metrics while the stress test is running. Set to '0' to disable. Default value: 9185.", Required = false)]
public int PrometheusTestMetricsPort { get; set; } = 9185;
[Option('v', "view", HelpText = "Whether or not a view should be configured to filter tags for the stress test. Default value: False.", Required = false)]
public bool AddViewToFilterTags { get; set; }
[Option('o', "otlp", HelpText = "Whether or not an OTLP exporter should be added for the stress test. Default value: False.", Required = false)]
public bool AddOtlpExporter { get; set; }
[Option('i', "interval", HelpText = "The OTLP exporter export interval in milliseconds. Default value: 5000.", Required = false)]
public int OtlpExporterExportIntervalMilliseconds { get; set; } = 5000;
[Option('e', "exemplars", HelpText = "Whether or not to enable exemplars for the stress test. Default value: False.", Required = false)]
public bool EnableExemplars { get; set; }
}
}

View File

@ -6,17 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Condition="'$(TargetFramework)' == '$(NetFrameworkMinimumSupportedVersion)'" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry\OpenTelemetry.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\OpenTelemetry.Exporter.Prometheus.HttpListener.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests.Stress\Skeleton.cs" Link="Includes\Skeleton.cs" />
<ProjectReference Include="$(RepoRoot)\test\OpenTelemetry.Tests.Stress\OpenTelemetry.Tests.Stress.csproj" />
</ItemGroup>
</Project>

View File

@ -2,31 +2,45 @@
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
using System.Runtime.CompilerServices;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace OpenTelemetry.Tests.Stress;
public partial class Program
public static class Program
{
private static readonly ActivitySource ActivitySource = new ActivitySource("OpenTelemetry.Tests.Stress");
public static void Main()
public static int Main(string[] args)
{
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(ActivitySource.Name)
.Build();
Stress(prometheusPort: 9464);
return StressTestFactory.RunSynchronously<TracesStressTest>(args);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static void Run()
private sealed class TracesStressTest : StressTest<StressTestOptions>
{
using (var activity = ActivitySource.StartActivity("test"))
private static readonly ActivitySource ActivitySource = new("OpenTelemetry.Tests.Stress");
private readonly TracerProvider tracerProvider;
public TracesStressTest(StressTestOptions options)
: base(options)
{
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(ActivitySource.Name)
.Build();
}
protected override void RunWorkItemInParallel()
{
using var activity = ActivitySource.StartActivity("test");
activity?.SetTag("foo", "value");
}
protected override void Dispose(bool isDisposing)
{
if (isDisposing)
{
this.tracerProvider.Dispose();
}
base.Dispose(isDisposing);
}
}
}

View File

@ -1,19 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Runtime.CompilerServices;
namespace OpenTelemetry.Tests.Stress;
public partial class Program
{
public static void Main()
{
Stress(concurrency: 1, prometheusPort: 9464);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static void Run()
{
}
}

View File

@ -5,8 +5,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Condition="'$(TargetFramework)' == '$(NetFrameworkMinimumSupportedVersion)'" />
<PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == '$(NetFrameworkMinimumSupportedVersion)'" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,24 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
namespace OpenTelemetry.Tests.Stress;
public static class Program
{
public static int Main(string[] args)
{
return StressTestFactory.RunSynchronously<DemoStressTest>(args);
}
private sealed class DemoStressTest : StressTest<StressTestOptions>
{
public DemoStressTest(StressTestOptions options)
: base(options)
{
}
protected override void RunWorkItemInParallel()
{
}
}
}

View File

@ -73,6 +73,10 @@ process_runtime_dotnet_gc_allocations_size_bytes 5485192 1658950184752
## Writing your own stress test
> [!WARNING]
> These instructions are out of date and should NOT be followed. They will be
updated soon.
Create a simple console application with the following code:
```csharp
@ -93,7 +97,7 @@ public partial class Program
}
```
Add the [`Skeleton.cs`](./Skeleton.cs) file to your `*.csproj` file:
Add the Skeleton.cs file to your `*.csproj` file:
```xml
<ItemGroup>

View File

@ -1,172 +0,0 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Runtime.InteropServices;
using OpenTelemetry.Metrics;
namespace OpenTelemetry.Tests.Stress;
public partial class Program
{
private static volatile bool bContinue = true;
private static volatile string output = "Test results not available yet.";
static Program()
{
}
public static void Stress(int concurrency = 0, int prometheusPort = 0)
{
#if DEBUG
Console.WriteLine("***WARNING*** The current build is DEBUG which may affect timing!");
Console.WriteLine();
#endif
if (concurrency < 0)
{
throw new ArgumentOutOfRangeException(nameof(concurrency), "concurrency level should be a non-negative number.");
}
if (concurrency == 0)
{
concurrency = Environment.ProcessorCount;
}
using var meter = new Meter("OpenTelemetry.Tests.Stress." + Guid.NewGuid().ToString("D"));
var cntLoopsTotal = 0UL;
meter.CreateObservableCounter(
"OpenTelemetry.Tests.Stress.Loops",
() => unchecked((long)cntLoopsTotal),
description: "The total number of `Run()` invocations that are completed.");
var dLoopsPerSecond = 0D;
meter.CreateObservableGauge(
"OpenTelemetry.Tests.Stress.LoopsPerSecond",
() => dLoopsPerSecond,
description: "The rate of `Run()` invocations based on a small sliding window of few hundreds of milliseconds.");
var dCpuCyclesPerLoop = 0D;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
meter.CreateObservableGauge(
"OpenTelemetry.Tests.Stress.CpuCyclesPerLoop",
() => dCpuCyclesPerLoop,
description: "The average CPU cycles for each `Run()` invocation, based on a small sliding window of few hundreds of milliseconds.");
}
using var meterProvider = prometheusPort != 0 ? Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.AddRuntimeInstrumentation()
.AddPrometheusHttpListener(
options => options.UriPrefixes = new string[] { $"http://localhost:{prometheusPort}/" })
.Build() : null;
var statistics = new long[concurrency];
var watchForTotal = Stopwatch.StartNew();
Parallel.Invoke(
() =>
{
Console.Write($"Running (concurrency = {concurrency}");
if (prometheusPort != 0)
{
Console.Write($", prometheusEndpoint = http://localhost:{prometheusPort}/metrics/");
}
Console.WriteLine("), press <Esc> to stop...");
var bOutput = false;
var watch = new Stopwatch();
while (true)
{
if (Console.KeyAvailable)
{
var key = Console.ReadKey(true).Key;
switch (key)
{
case ConsoleKey.Enter:
Console.WriteLine(string.Format("{0} {1}", DateTime.UtcNow.ToString("O"), output));
break;
case ConsoleKey.Escape:
bContinue = false;
return;
case ConsoleKey.Spacebar:
bOutput = !bOutput;
break;
}
continue;
}
if (bOutput)
{
Console.WriteLine(string.Format("{0} {1}", DateTime.UtcNow.ToString("O"), output));
}
var cntLoopsOld = (ulong)statistics.Sum();
var cntCpuCyclesOld = GetCpuCycles();
watch.Restart();
Thread.Sleep(200);
watch.Stop();
cntLoopsTotal = (ulong)statistics.Sum();
var cntCpuCyclesNew = GetCpuCycles();
var nLoops = cntLoopsTotal - cntLoopsOld;
var nCpuCycles = cntCpuCyclesNew - cntCpuCyclesOld;
dLoopsPerSecond = (double)nLoops / ((double)watch.ElapsedMilliseconds / 1000.0);
dCpuCyclesPerLoop = nLoops == 0 ? 0 : nCpuCycles / nLoops;
output = $"Loops: {cntLoopsTotal:n0}, Loops/Second: {dLoopsPerSecond:n0}, CPU Cycles/Loop: {dCpuCyclesPerLoop:n0}, RunwayTime (Seconds): {watchForTotal.Elapsed.TotalSeconds:n0} ";
Console.Title = output;
}
},
() =>
{
Parallel.For(0, concurrency, (i) =>
{
statistics[i] = 0;
while (bContinue)
{
Run();
statistics[i]++;
}
});
});
watchForTotal.Stop();
cntLoopsTotal = (ulong)statistics.Sum();
var totalLoopsPerSecond = (double)cntLoopsTotal / ((double)watchForTotal.ElapsedMilliseconds / 1000.0);
var cntCpuCyclesTotal = GetCpuCycles();
var cpuCyclesPerLoopTotal = cntLoopsTotal == 0 ? 0 : cntCpuCyclesTotal / cntLoopsTotal;
Console.WriteLine("Stopping the stress test...");
Console.WriteLine($"* Total Runaway Time (seconds) {watchForTotal.Elapsed.TotalSeconds:n0}");
Console.WriteLine($"* Total Loops: {cntLoopsTotal:n0}");
Console.WriteLine($"* Average Loops/Second: {totalLoopsPerSecond:n0}");
Console.WriteLine($"* Average CPU Cycles/Loop: {cpuCyclesPerLoopTotal:n0}");
}
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool QueryProcessCycleTime(IntPtr hProcess, out ulong cycles);
private static ulong GetCpuCycles()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return 0;
}
if (!QueryProcessCycleTime((IntPtr)(-1), out var cycles))
{
return 0;
}
return cycles;
}
}

View File

@ -0,0 +1,209 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Runtime.InteropServices;
using System.Text.Json;
using OpenTelemetry.Metrics;
namespace OpenTelemetry.Tests.Stress;
public abstract class StressTest<T> : IDisposable
where T : StressTestOptions
{
private volatile bool bContinue = true;
private volatile string output = "Test results not available yet.";
protected StressTest(T options)
{
this.Options = options ?? throw new ArgumentNullException(nameof(options));
}
public T Options { get; }
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
public void RunSynchronously()
{
#if DEBUG
Console.WriteLine("***WARNING*** The current build is DEBUG which may affect timing!");
Console.WriteLine();
#endif
var options = this.Options;
if (options.Concurrency < 0)
{
throw new ArgumentOutOfRangeException(nameof(options.Concurrency), "Concurrency level should be a non-negative number.");
}
if (options.Concurrency == 0)
{
options.Concurrency = Environment.ProcessorCount;
}
using var meter = new Meter("OpenTelemetry.Tests.Stress." + Guid.NewGuid().ToString("D"));
var cntLoopsTotal = 0UL;
meter.CreateObservableCounter(
"OpenTelemetry.Tests.Stress.Loops",
() => unchecked((long)cntLoopsTotal),
description: "The total number of `Run()` invocations that are completed.");
var dLoopsPerSecond = 0D;
meter.CreateObservableGauge(
"OpenTelemetry.Tests.Stress.LoopsPerSecond",
() => dLoopsPerSecond,
description: "The rate of `Run()` invocations based on a small sliding window of few hundreds of milliseconds.");
var dCpuCyclesPerLoop = 0D;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
meter.CreateObservableGauge(
"OpenTelemetry.Tests.Stress.CpuCyclesPerLoop",
() => dCpuCyclesPerLoop,
description: "The average CPU cycles for each `Run()` invocation, based on a small sliding window of few hundreds of milliseconds.");
}
using var meterProvider = options.PrometheusInternalMetricsPort != 0 ? Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.AddRuntimeInstrumentation()
.AddPrometheusHttpListener(o => o.UriPrefixes = new string[] { $"http://localhost:{options.PrometheusInternalMetricsPort}/" })
.Build() : null;
var statistics = new long[options.Concurrency];
var watchForTotal = Stopwatch.StartNew();
TimeSpan? duration = options.DurationSeconds > 0
? TimeSpan.FromSeconds(options.DurationSeconds)
: null;
Parallel.Invoke(
() =>
{
Console.WriteLine($"Options: {JsonSerializer.Serialize(options)}");
Console.WriteLine($"Run {Process.GetCurrentProcess().ProcessName}.exe --help to see available options.");
Console.Write($"Running (concurrency = {options.Concurrency}");
if (options.PrometheusInternalMetricsPort != 0)
{
Console.Write($", internalPrometheusEndpoint = http://localhost:{options.PrometheusInternalMetricsPort}/metrics/");
}
this.WriteRunInformationToConsole();
Console.WriteLine("), press <Esc> to stop, press <Spacebar> to toggle statistics in the console...");
Console.WriteLine(this.output);
var outputCursorTop = Console.CursorTop - 1;
var bOutput = true;
var watch = new Stopwatch();
while (true)
{
if (Console.KeyAvailable)
{
var key = Console.ReadKey(true).Key;
switch (key)
{
case ConsoleKey.Enter:
Console.WriteLine(string.Format("{0} {1}", DateTime.UtcNow.ToString("O"), this.output));
break;
case ConsoleKey.Escape:
this.bContinue = false;
return;
case ConsoleKey.Spacebar:
bOutput = !bOutput;
break;
}
continue;
}
if (bOutput)
{
var tempCursorLeft = Console.CursorLeft;
var tempCursorTop = Console.CursorTop;
Console.SetCursorPosition(0, outputCursorTop);
Console.WriteLine(this.output.PadRight(Console.BufferWidth));
Console.SetCursorPosition(tempCursorLeft, tempCursorTop);
}
var cntLoopsOld = (ulong)statistics.Sum();
var cntCpuCyclesOld = StressTestNativeMethods.GetCpuCycles();
watch.Restart();
Thread.Sleep(200);
watch.Stop();
cntLoopsTotal = (ulong)statistics.Sum();
var cntCpuCyclesNew = StressTestNativeMethods.GetCpuCycles();
var nLoops = cntLoopsTotal - cntLoopsOld;
var nCpuCycles = cntCpuCyclesNew - cntCpuCyclesOld;
dLoopsPerSecond = (double)nLoops / ((double)watch.ElapsedMilliseconds / 1000.0);
dCpuCyclesPerLoop = nLoops == 0 ? 0 : nCpuCycles / nLoops;
var totalElapsedTime = watchForTotal.Elapsed;
if (duration.HasValue)
{
this.output = $"Loops: {cntLoopsTotal:n0}, Loops/Second: {dLoopsPerSecond:n0}, CPU Cycles/Loop: {dCpuCyclesPerLoop:n0}, RemainingTime (Seconds): {(duration.Value - totalElapsedTime).TotalSeconds:n0}";
if (totalElapsedTime > duration)
{
this.bContinue = false;
return;
}
}
else
{
this.output = $"Loops: {cntLoopsTotal:n0}, Loops/Second: {dLoopsPerSecond:n0}, CPU Cycles/Loop: {dCpuCyclesPerLoop:n0}, RunningTime (Seconds): {totalElapsedTime.TotalSeconds:n0}";
}
Console.Title = this.output;
}
},
() =>
{
Parallel.For(0, options.Concurrency, (i) =>
{
ref var count = ref statistics[i];
while (this.bContinue)
{
this.RunWorkItemInParallel();
count++;
}
});
});
watchForTotal.Stop();
cntLoopsTotal = (ulong)statistics.Sum();
var totalLoopsPerSecond = (double)cntLoopsTotal / ((double)watchForTotal.ElapsedMilliseconds / 1000.0);
var cntCpuCyclesTotal = StressTestNativeMethods.GetCpuCycles();
var cpuCyclesPerLoopTotal = cntLoopsTotal == 0 ? 0 : cntCpuCyclesTotal / cntLoopsTotal;
Console.WriteLine("Stopping the stress test...");
Console.WriteLine($"* Total Running Time (Seconds) {watchForTotal.Elapsed.TotalSeconds:n0}");
Console.WriteLine($"* Total Loops: {cntLoopsTotal:n0}");
Console.WriteLine($"* Average Loops/Second: {totalLoopsPerSecond:n0}");
Console.WriteLine($"* Average CPU Cycles/Loop: {cpuCyclesPerLoopTotal:n0}");
#if !NETFRAMEWORK
Console.WriteLine($"* GC Total Allocated Bytes: {GC.GetTotalAllocatedBytes()}");
#endif
}
protected virtual void WriteRunInformationToConsole()
{
}
protected abstract void RunWorkItemInParallel();
protected virtual void Dispose(bool isDisposing)
{
}
}

View File

@ -0,0 +1,34 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using CommandLine;
namespace OpenTelemetry.Tests.Stress;
public static class StressTestFactory
{
public static int RunSynchronously<TStressTest>(string[] commandLineArguments)
where TStressTest : StressTest<StressTestOptions>
{
return RunSynchronously<TStressTest, StressTestOptions>(commandLineArguments);
}
public static int RunSynchronously<TStressTest, TStressTestOptions>(string[] commandLineArguments)
where TStressTest : StressTest<TStressTestOptions>
where TStressTestOptions : StressTestOptions
{
return Parser.Default.ParseArguments<TStressTestOptions>(commandLineArguments)
.MapResult(
CreateStressTestAndRunSynchronously,
_ => 1);
static int CreateStressTestAndRunSynchronously(TStressTestOptions options)
{
using var stressTest = (TStressTest)Activator.CreateInstance(typeof(TStressTest), options)!;
stressTest.RunSynchronously();
return 0;
}
}
}

View File

@ -0,0 +1,28 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Runtime.InteropServices;
namespace OpenTelemetry.Tests.Stress;
internal static class StressTestNativeMethods
{
public static ulong GetCpuCycles()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return 0;
}
if (!QueryProcessCycleTime((IntPtr)(-1), out var cycles))
{
return 0;
}
return cycles;
}
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool QueryProcessCycleTime(IntPtr hProcess, out ulong cycles);
}

View File

@ -0,0 +1,18 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using CommandLine;
namespace OpenTelemetry.Tests.Stress;
public class StressTestOptions
{
[Option('c', "concurrency", HelpText = "The concurrency (maximum degree of parallelism) for the stress test. Default value: Environment.ProcessorCount.", Required = false)]
public int Concurrency { get; set; }
[Option('p', "internal_port", HelpText = "The Prometheus http listener port where Prometheus will be exposed for retrieving internal metrics while the stress test is running. Set to '0' to disable. Default value: 9464.", Required = false)]
public int PrometheusInternalMetricsPort { get; set; } = 9464;
[Option('d', "duration", HelpText = "The duration for the stress test to run in seconds. If set to '0' or a negative value the stress test will run until canceled. Default value: 0.", Required = false)]
public int DurationSeconds { get; set; }
}