From 8fa6bd4aaa949bbfd3dd263e7327996ab8fa7966 Mon Sep 17 00:00:00 2001 From: John Salem Date: Mon, 15 Jul 2019 17:28:57 -0700 Subject: [PATCH] Add EventPipe/DiagnosticsIpc tests (#25457) * Add Microsoft.Diagnostics.Tools.RuntimeClient to dependencies.props * add EventPipe tests for provider coherence, buffer size resilience, and rundown event presence --- dependencies.props | 1 + .../test_dependencies/test_dependencies.csproj | 3 + .../src/tracing/eventpipe/buffersize/buffersize.cs | 68 ++++++ .../tracing/eventpipe/buffersize/buffersize.csproj | 34 +++ tests/src/tracing/eventpipe/common/IpcTraceTest.cs | 252 +++++++++++++++++++++ tests/src/tracing/eventpipe/common/common.csproj | 33 +++ .../providervalidation/providervalidation.cs | 69 ++++++ .../providervalidation/providervalidation.csproj | 34 +++ .../rundownvalidation/rundownvalidation.cs | 55 +++++ .../rundownvalidation/rundownvalidation.csproj | 34 +++ 10 files changed, 583 insertions(+) create mode 100644 tests/src/tracing/eventpipe/buffersize/buffersize.cs create mode 100644 tests/src/tracing/eventpipe/buffersize/buffersize.csproj create mode 100644 tests/src/tracing/eventpipe/common/IpcTraceTest.cs create mode 100644 tests/src/tracing/eventpipe/common/common.csproj create mode 100644 tests/src/tracing/eventpipe/providervalidation/providervalidation.cs create mode 100644 tests/src/tracing/eventpipe/providervalidation/providervalidation.csproj create mode 100644 tests/src/tracing/eventpipe/rundownvalidation/rundownvalidation.cs create mode 100644 tests/src/tracing/eventpipe/rundownvalidation/rundownvalidation.csproj diff --git a/dependencies.props b/dependencies.props index b04a885..3209bfe 100644 --- a/dependencies.props +++ b/dependencies.props @@ -26,6 +26,7 @@ 2.4.1-pre.build.4059 1.0.0-beta-build0015 2.0.43 + 1.0.4-preview6.19326.1 2.2.0 + + + + + + + False + + + + + + + + diff --git a/tests/src/tracing/eventpipe/common/IpcTraceTest.cs b/tests/src/tracing/eventpipe/common/IpcTraceTest.cs new file mode 100644 index 0000000..cebf4b1 --- /dev/null +++ b/tests/src/tracing/eventpipe/common/IpcTraceTest.cs @@ -0,0 +1,252 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Diagnostics.Tools.RuntimeClient; + +namespace Tracing.Tests.Common +{ + public class ExpectedEventCount + { + // The acceptable percent error on the expected value + // represented as a floating point value in [0,1]. + public float Error { get; private set; } + + // The expected count of events. A value of -1 indicates + // that count does not matter, and we are simply testing + // that the provider exists in the trace. + public int Count { get; private set; } + + public ExpectedEventCount(int count, float error = 0.0f) + { + Count = count; + Error = error; + } + + public bool Validate(int actualValue) + { + return Count == -1 || CheckErrorBounds(actualValue); + } + + public bool CheckErrorBounds(int actualValue) + { + return Math.Abs(actualValue - Count) <= (Count * Error); + } + + public static implicit operator ExpectedEventCount(int i) + { + return new ExpectedEventCount(i); + } + + public override string ToString() + { + return $"{Count} +- {Count * Error}"; + } + } + + // This event source is used by the test infra to + // to insure that providers have finished being enabled + // for the session being observed. Since the client API + // returns the pipe for reading _before_ it finishes + // enabling the providers to write to that session, + // we need to guarantee that our providers are on before + // sending events. This is a _unique_ problem I imagine + // should _only_ affect scenarios like these tests + // where the reading and sending of events are required + // to synchronize. + public sealed class SentinelEventSource : EventSource + { + private SentinelEventSource() {} + public static SentinelEventSource Log = new SentinelEventSource(); + public void SentinelEvent() { WriteEvent(1, "SentinelEvent"); } + } + + public static class SessionConfigurationExtensions + { + public static SessionConfiguration InjectSentinel(this SessionConfiguration sessionConfiguration) + { + var newProviderList = new List(sessionConfiguration.Providers); + newProviderList.Add(new Provider("SentinelEventSource")); + return new SessionConfiguration(sessionConfiguration.CircularBufferSizeInMB, sessionConfiguration.Format, newProviderList.AsReadOnly()); + } + } + + public class IpcTraceTest + { + // This Action is executed while the trace is being collected. + private Action _eventGeneratingAction; + + // A dictionary of event providers to number of events. + // A count of -1 indicates that you are only testing for the presence of the provider + // and don't care about the number of events sent + private Dictionary _expectedEventCounts; + private Dictionary _actualEventCounts = new Dictionary(); + private SessionConfiguration _sessionConfiguration; + + // A function to be called with the EventPipeEventSource _before_ + // the call to `source.Process()`. The function should return another + // function that will be called to check whether the optional test was validated. + // Example in situ: providervalidation.cs + private Func> _optionalTraceValidator; + + IpcTraceTest( + Dictionary expectedEventCounts, + Action eventGeneratingAction, + SessionConfiguration? sessionConfiguration = null, + Func> optionalTraceValidator = null) + { + _eventGeneratingAction = eventGeneratingAction; + _expectedEventCounts = expectedEventCounts; + _sessionConfiguration = sessionConfiguration?.InjectSentinel() ?? new SessionConfiguration( + circularBufferSizeMB: 1000, + format: EventPipeSerializationFormat.NetTrace, + providers: new List { new Provider("Microsoft-Windows-DotNETRuntime") }); + _optionalTraceValidator = optionalTraceValidator; + } + + private int Fail(string message = "") + { + Console.WriteLine("Test FAILED!"); + Console.WriteLine(message); + Console.WriteLine("Configuration:"); + Console.WriteLine("{"); + Console.WriteLine($"\tbufferSize: {_sessionConfiguration.CircularBufferSizeInMB},"); + Console.WriteLine("\tproviders: ["); + foreach (var provider in _sessionConfiguration.Providers) + { + Console.WriteLine($"\t\t{provider.ToString()},"); + } + Console.WriteLine("\t]"); + Console.WriteLine("}\n"); + Console.WriteLine("Expected:"); + Console.WriteLine("{"); + foreach (var (k, v) in _expectedEventCounts) + { + Console.WriteLine($"\t\"{k}\" = {v}"); + } + Console.WriteLine("}\n"); + + Console.WriteLine("Actual:"); + Console.WriteLine("{"); + foreach (var (k, v) in _actualEventCounts) + { + Console.WriteLine($"\t\"{k}\" = {v}"); + } + Console.WriteLine("}"); + + return -1; + } + + private int Validate() + { + var processId = Process.GetCurrentProcess().Id; + var binaryReader = EventPipeClient.CollectTracing(processId, _sessionConfiguration, out var eventpipeSessionId); + if (eventpipeSessionId == 0) + return -1; + + // CollectTracing returns before EventPipe::Enable has returned, so the + // the sources we want to listen for may not have been enabled yet. + // We'll use this sentinel EventSource to check if Enable has finished + ManualResetEvent sentinelEventReceived = new ManualResetEvent(false); + var sentinelTask = new Task(() => + { + while (!sentinelEventReceived.WaitOne(50)) + { + SentinelEventSource.Log.SentinelEvent(); + } + }); + sentinelTask.Start(); + + EventPipeEventSource source = null; + Func optionalTraceValidationCallback = null; + var readerTask = new Task(() => + { + source = new EventPipeEventSource(binaryReader); + source.Dynamic.All += (eventData) => + { + if (eventData.ProviderName == "SentinelEventSource") + { + sentinelEventReceived.Set(); + } + else if (_actualEventCounts.TryGetValue(eventData.ProviderName, out _)) + { + _actualEventCounts[eventData.ProviderName]++; + } + else + { + _actualEventCounts[eventData.ProviderName] = 1; + } + }; + + if (_optionalTraceValidator != null) + { + optionalTraceValidationCallback = _optionalTraceValidator(source); + } + + source.Process(); + }); + + readerTask.Start(); + sentinelEventReceived.WaitOne(); + _eventGeneratingAction(); + EventPipeClient.StopTracing(processId, eventpipeSessionId); + + readerTask.Wait(); + + foreach (var (provider, expectedCount) in _expectedEventCounts) + { + if (_actualEventCounts.TryGetValue(provider, out var actualCount)) + { + if (!expectedCount.Validate(actualCount)) + { + return Fail($"Event count mismatch for provider \"{provider}\": expected {expectedCount}, but saw {actualCount}"); + } + } + else + { + return Fail($"No events for provider \"{provider}\""); + } + } + + if (optionalTraceValidationCallback != null) + { + return optionalTraceValidationCallback(); + } + else + { + return 100; + } + } + + public static int RunAndValidateEventCounts( + Dictionary expectedEventCounts, + Action eventGeneratingAction, + SessionConfiguration? sessionConfiguration = null, + Func> optionalTraceValidator = null) + { + Console.WriteLine("TEST STARTING"); + var test = new IpcTraceTest(expectedEventCounts, eventGeneratingAction, sessionConfiguration, optionalTraceValidator); + try + { + var ret = test.Validate(); + if (ret == 100) + Console.WriteLine("TEST PASSED!"); + return ret; + } + catch (Exception e) + { + Console.WriteLine("TEST FAILED!"); + Console.WriteLine(e); + return -1; + } + } + } +} \ No newline at end of file diff --git a/tests/src/tracing/eventpipe/common/common.csproj b/tests/src/tracing/eventpipe/common/common.csproj new file mode 100644 index 0000000..3624f90 --- /dev/null +++ b/tests/src/tracing/eventpipe/common/common.csproj @@ -0,0 +1,33 @@ + + + + + Debug + AnyCPU + 2.0 + netcoreapp3.0 + .NETCoreApp + {8E3244CB-407F-4142-BAAB-E7A55901A5FA} + Library + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + ..\..\ + SharedLibrary + + true + false + + + + + + + + + False + + + + + + + diff --git a/tests/src/tracing/eventpipe/providervalidation/providervalidation.cs b/tests/src/tracing/eventpipe/providervalidation/providervalidation.cs new file mode 100644 index 0000000..2fd4693 --- /dev/null +++ b/tests/src/tracing/eventpipe/providervalidation/providervalidation.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.Tracing; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.Diagnostics.Tools.RuntimeClient; +using Microsoft.Diagnostics.Tracing; +using Tracing.Tests.Common; + +namespace Tracing.Tests.ProviderValidation +{ + public sealed class MyEventSource : EventSource + { + private MyEventSource() {} + public static MyEventSource Log = new MyEventSource(); + public void MyEvent() { WriteEvent(1, "MyEvent"); } + } + + public class ProviderValidation + { + public static int Main(string[] args) + { + // This test validates that the rundown events are present + // and that providers turned on that generate events are being written to + // the stream. + + var providers = new List() + { + new Provider("MyEventSource"), + new Provider("Microsoft-DotNETCore-SampleProfiler") + }; + + var tests = new int[] { 4, 10 } + .Select(x => (uint)Math.Pow(2, x)) + .Select(bufferSize => new SessionConfiguration(circularBufferSizeMB: bufferSize, format: EventPipeSerializationFormat.NetTrace, providers: providers)) + .Select>(configuration => () => IpcTraceTest.RunAndValidateEventCounts(_expectedEventCounts, _eventGeneratingAction, configuration)); + + foreach (var test in tests) + { + var ret = test(); + if (ret < 0) + return ret; + } + + return 100; + } + + private static Dictionary _expectedEventCounts = new Dictionary() + { + { "MyEventSource", new ExpectedEventCount(1000, 0.30f) }, + { "Microsoft-Windows-DotNETRuntimeRundown", -1 }, + { "Microsoft-DotNETCore-SampleProfiler", -1 } + }; + + private static Action _eventGeneratingAction = () => + { + foreach (var _ in Enumerable.Range(0,1000)) + { + MyEventSource.Log.MyEvent(); + } + }; + } +} \ No newline at end of file diff --git a/tests/src/tracing/eventpipe/providervalidation/providervalidation.csproj b/tests/src/tracing/eventpipe/providervalidation/providervalidation.csproj new file mode 100644 index 0000000..adfb425 --- /dev/null +++ b/tests/src/tracing/eventpipe/providervalidation/providervalidation.csproj @@ -0,0 +1,34 @@ + + + + + Debug + AnyCPU + 2.0 + netcoreapp3.0 + .NETCoreApp + {8E3244CB-407F-4142-BAAB-E7A55901A5FA} + exe + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + ..\..\ + BuildAndRun + $(DefineConstants);STATIC + true + 0 + + + + + + + + + False + + + + + + + + diff --git a/tests/src/tracing/eventpipe/rundownvalidation/rundownvalidation.cs b/tests/src/tracing/eventpipe/rundownvalidation/rundownvalidation.cs new file mode 100644 index 0000000..2482081 --- /dev/null +++ b/tests/src/tracing/eventpipe/rundownvalidation/rundownvalidation.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.Tracing; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.Diagnostics.Tools.RuntimeClient; +using Microsoft.Diagnostics.Tracing; +using Tracing.Tests.Common; +using Microsoft.Diagnostics.Tracing.Parsers.Clr; + +namespace Tracing.Tests.RundownValidation +{ + + public class RundownValidation + { + public static int Main(string[] args) + { + // This test validates that the rundown events are present + // and that the rundown contains the necessary events to get + // symbols in a nettrace file. + + var providers = new List() + { + new Provider("Microsoft-DotNETCore-SampleProfiler") + }; + + var configuration = new SessionConfiguration(circularBufferSizeMB: 1024, format: EventPipeSerializationFormat.NetTrace, providers: providers); + return IpcTraceTest.RunAndValidateEventCounts(_expectedEventCounts, _eventGeneratingAction, configuration, _DoesRundownContainMethodEvents); + } + + private static Dictionary _expectedEventCounts = new Dictionary() + { + { "Microsoft-Windows-DotNETRuntimeRundown", -1 } + }; + + // We only care about rundown so skip generating any events. + private static Action _eventGeneratingAction = () => { }; + + private static Func> _DoesRundownContainMethodEvents = (source) => + { + bool hasMethodDCStopVerbose = false; + bool hasMethodILToNativeMap = false; + ClrRundownTraceEventParser rundownParser = new ClrRundownTraceEventParser(source); + rundownParser.MethodDCStopVerbose += (eventData) => hasMethodDCStopVerbose = true; + rundownParser.MethodILToNativeMapDCStop += (eventData) => hasMethodILToNativeMap = true; + return () => hasMethodDCStopVerbose && hasMethodILToNativeMap ? 100 : -1; + }; + } +} \ No newline at end of file diff --git a/tests/src/tracing/eventpipe/rundownvalidation/rundownvalidation.csproj b/tests/src/tracing/eventpipe/rundownvalidation/rundownvalidation.csproj new file mode 100644 index 0000000..df7e427 --- /dev/null +++ b/tests/src/tracing/eventpipe/rundownvalidation/rundownvalidation.csproj @@ -0,0 +1,34 @@ + + + + + Debug + AnyCPU + 2.0 + netcoreapp3.0 + .NETCoreApp + {8E3244CB-407F-4142-BAAB-E7A55901A5FA} + exe + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + ..\..\ + BuildAndRun + $(DefineConstants);STATIC + true + 0 + + + + + + + + + False + + + + + + + + -- 2.7.4