using Microsoft.Win32;
using System.IO;
using System.Globalization;
+using System.Reflection;
using System.Runtime.Versioning;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
internal sealed class EventPipeController
{
// Miscellaneous constants.
+ private const string DefaultAppName = "app";
private const string NetPerfFileExtension = ".netperf";
- private const string MarkerFileExtension = ".ctl";
+ private const string ConfigFileSuffix = ".eventpipeconfig";
private const int EnabledPollingIntervalMilliseconds = 1000; // 1 second
- private const int DisabledPollingIntervalMilliseconds = 10000; // 10 seconds
+ private const int DisabledPollingIntervalMilliseconds = 5000; // 5 seconds
private const uint DefaultCircularBufferMB = 1024; // 1 GB
private static readonly char[] ProviderConfigDelimiter = new char[] { ',' };
private static readonly char[] ConfigComponentDelimiter = new char[] { ':' };
+ private static readonly string[] ConfigFileLineDelimiters = new string[] { "\r\n", "\n" };
+ private const char ConfigEntryDelimiter = '=';
+
+ // Config file keys.
+ private const string ConfigKey_Providers = "Providers";
+ private const string ConfigKey_CircularMB = "CircularMB";
+ private const string ConfigKey_OutputPath = "OutputPath";
+ private const string ConfigKey_ProcessID = "ProcessID";
// The default set of providers/keywords/levels. Used if an alternative configuration is not specified.
private static readonly EventPipeProviderConfiguration[] DefaultProviderConfiguration = new EventPipeProviderConfiguration[]
new EventPipeProviderConfiguration("Microsoft-DotNETCore-SampleProfiler", 0x0, 5)
};
- // Cache for COMPlus configuration variables.
- private static int s_Config_EnableEventPipe = -1;
- private static string s_Config_EventPipeConfig = null;
- private static uint s_Config_EventPipeCircularMB = 0;
- private static string s_Config_EventPipeOutputFile = null;
-
// Singleton controller instance.
private static EventPipeController s_controllerInstance = null;
// Controller object state.
private Timer m_timer;
+ private string m_configFilePath;
+ private DateTime m_configFileUpdateTime;
private string m_traceFilePath = null;
- private string m_markerFilePath = null;
- private bool m_markerFileExists = false;
+ private bool m_configFileExists = false;
internal static void Initialize()
{
- // Don't allow failures to propagate upstream.
- // Instead, ensure program correctness without tracing.
+ // Don't allow failures to propagate upstream. Ensure program correctness without tracing.
try
{
if (s_controllerInstance == null)
{
- if(Config_EnableEventPipe == 4)
- {
- // Create a new controller to listen for commands.
- s_controllerInstance = new EventPipeController();
- }
- else if (Config_EnableEventPipe > 0)
+ if (Config_EnableEventPipe > 0)
{
// Enable tracing immediately.
// It will be disabled automatically on shutdown.
- EventPipe.Enable(GetConfiguration());
+ EventPipe.Enable(BuildConfigFromEnvironment());
+ }
+ else
+ {
+ // Create a new controller to listen for commands.
+ s_controllerInstance = new EventPipeController();
}
}
}
private EventPipeController()
{
- // Initialize the timer to run once. The timer will re-schedule itself after each poll operation.
- // This is done to ensure that there aren't multiple concurrent polling operations when an operation
- // takes longer than the polling interval (e.g. on disable/rundown).
+ // Set the config file path.
+ m_configFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, BuildConfigFileName());
+
+ // Initialize the timer, but don't set it to run.
+ // The timer will be set to run each time PollForTracingCommand is called.
m_timer = new Timer(
callback: new TimerCallback(PollForTracingCommand),
state: null,
- dueTime: DisabledPollingIntervalMilliseconds,
+ dueTime: Timeout.Infinite,
period: Timeout.Infinite,
flowExecutionContext: false);
+
+ // Trigger the first poll operation on the start-up path.
+ PollForTracingCommand(null);
}
private void PollForTracingCommand(object state)
// Make sure that any transient errors don't cause the listener thread to exit.
try
{
- // Perform initialization when the timer fires for the first time.
- if (m_traceFilePath == null)
- {
- // Set file paths.
- m_traceFilePath = GetDisambiguatedTraceFilePath(Config_EventPipeOutputFile);
- m_markerFilePath = MarkerFilePath;
-
- // Marker file is assumed to not exist.
- // This will be updated when the monitoring thread starts.
- m_markerFileExists = false;
- }
-
- // Check for existence of the file.
- // If the existence of the file has changed since the last time we checked
+ // Check for existence of the config file.
+ // If the existence of the file has changed since the last time we checked or the update time has changed
// this means that we need to act on that change.
- bool fileExists = File.Exists(m_markerFilePath);
- if (m_markerFileExists != fileExists)
+ bool fileExists = File.Exists(m_configFilePath);
+ if (m_configFileExists != fileExists)
{
// Save the result.
- m_markerFileExists = fileExists;
+ m_configFileExists = fileExists;
// Take the appropriate action.
if (fileExists)
{
// Enable tracing.
- EventPipe.Enable(GetConfiguration());
+ // Check for null here because it's possible that the configuration contains a process filter
+ // that doesn't match the current process. IF this occurs, we should't enable tracing.
+ EventPipeConfiguration config = BuildConfigFromFile(m_configFilePath);
+ if (config != null)
+ {
+ EventPipe.Enable(config);
+ }
}
else
{
catch { }
}
- private static EventPipeConfiguration GetConfiguration()
+ private static EventPipeConfiguration BuildConfigFromFile(string configFilePath)
+ {
+ // Read the config file in once call.
+ byte[] configContents = File.ReadAllBytes(configFilePath);
+
+ // Convert the contents to a string.
+ string strConfigContents = Encoding.UTF8.GetString(configContents);
+
+ // Read all of the config options.
+ string outputPath = null;
+ string strProviderConfig = null;
+ string strCircularMB = null;
+ string strProcessID = null;
+
+ // Split the configuration entries by line.
+ string[] configEntries = strConfigContents.Split(ConfigFileLineDelimiters, StringSplitOptions.RemoveEmptyEntries);
+ foreach (string configEntry in configEntries)
+ {
+ //`Split the key and value by '='.
+ string[] entryComponents = configEntry.Split(ConfigEntryDelimiter);
+ if(entryComponents.Length == 2)
+ {
+ string key = entryComponents[0];
+ if (key.Equals(ConfigKey_Providers))
+ {
+ strProviderConfig = entryComponents[1];
+ }
+ else if (key.Equals(ConfigKey_OutputPath))
+ {
+ outputPath = entryComponents[1];
+ }
+ else if (key.Equals(ConfigKey_CircularMB))
+ {
+ strCircularMB = entryComponents[1];
+ }
+ else if (key.Equals(ConfigKey_ProcessID))
+ {
+ strProcessID = entryComponents[1];
+ }
+ }
+ }
+
+ // Check the process ID filter if it is set.
+ if (!string.IsNullOrEmpty(strProcessID))
+ {
+ // If set, bail out early if the specified process does not match the current process.
+ int processID = Convert.ToInt32(strProcessID);
+ if (processID != Win32Native.GetCurrentProcessId())
+ {
+ return null;
+ }
+ }
+
+ // Ensure that the output path is set.
+ if (string.IsNullOrEmpty(outputPath))
+ {
+ throw new ArgumentNullException(nameof(outputPath));
+ }
+
+ // Build the full path to the trace file.
+ string traceFileName = BuildTraceFileName();
+ string outputFile = Path.Combine(outputPath, traceFileName);
+
+ // Get the circular buffer size.
+ uint circularMB = DefaultCircularBufferMB;
+ if(!string.IsNullOrEmpty(strCircularMB))
+ {
+ circularMB = Convert.ToUInt32(strCircularMB);
+ }
+
+ // Initialize a new configuration object.
+ EventPipeConfiguration config = new EventPipeConfiguration(outputFile, circularMB);
+
+ // Set the provider configuration if specified.
+ if (!string.IsNullOrEmpty(strProviderConfig))
+ {
+ SetProviderConfiguration(strProviderConfig, config);
+ }
+ else
+ {
+ // If the provider configuration isn't specified, use the default.
+ config.EnableProviderRange(DefaultProviderConfiguration);
+ }
+
+ return config;
+ }
+
+ private static EventPipeConfiguration BuildConfigFromEnvironment()
{
+ // Build the full path to the trace file.
+ string traceFileName = BuildTraceFileName();
+ string outputFilePath = Path.Combine(Config_EventPipeOutputPath, traceFileName);
+
// Create a new configuration object.
EventPipeConfiguration config = new EventPipeConfiguration(
- GetDisambiguatedTraceFilePath(Config_EventPipeOutputFile),
+ outputFilePath,
Config_EventPipeCircularMB);
// Get the configuration.
string strConfig = Config_EventPipeConfig;
if (!string.IsNullOrEmpty(strConfig))
{
- // String must be of the form "providerName:keywords:level,providerName:keywords:level..."
- string[] providers = strConfig.Split(ProviderConfigDelimiter);
- foreach (string provider in providers)
- {
- string[] components = provider.Split(ConfigComponentDelimiter);
- if (components.Length == 3)
- {
- string providerName = components[0];
-
- // We use a try/catch block here because ulong.TryParse won't accept 0x at the beginning
- // of a hex string. Thus, we either need to conditionally strip it or handle the exception.
- // Given that this is not a perf-critical path, catching the exception is the simpler code.
- ulong keywords = 0;
- try
- {
- keywords = Convert.ToUInt64(components[1], 16);
- }
- catch { }
-
- uint level;
- if (!uint.TryParse(components[2], out level))
- {
- level = 0;
- }
-
- config.EnableProvider(providerName, keywords, level);
- }
- }
+ // If the configuration is specified, parse it and save it to the config object.
+ SetProviderConfiguration(strConfig, config);
}
else
{
return config;
}
- /// <summary>
- /// Responsible for disambiguating the trace file path if the specified file already exists.
- /// This can happen if there are multiple applications with tracing enabled concurrently and COMPlus_EventPipeOutputFile
- /// is set to the same value for more than one concurrently running application.
- /// </summary>
- private static string GetDisambiguatedTraceFilePath(string inputPath)
+ private static string BuildConfigFileName()
+ {
+ return GetAppName() + ConfigFileSuffix;
+ }
+
+ private static string BuildTraceFileName()
{
- if (string.IsNullOrEmpty(inputPath))
+ return GetAppName() + "." + Win32Native.GetCurrentProcessId() + NetPerfFileExtension;
+ }
+
+ private static string GetAppName()
+ {
+ string appName = null;
+ Assembly entryAssembly = Assembly.GetEntryAssembly();
+ if (entryAssembly != null)
{
- throw new ArgumentNullException("inputPath");
+ AssemblyName assemblyName = entryAssembly.GetName();
+ if (assemblyName != null)
+ {
+ appName = assemblyName.Name;
+ }
}
- string filePath = inputPath;
- if (File.Exists(filePath))
+ if (string.IsNullOrEmpty(appName))
{
- string directoryName = Path.GetDirectoryName(filePath);
- string fileWithoutExtension = Path.GetFileName(filePath);
- string extension = Path.GetExtension(filePath);
+ appName = DefaultAppName;
+ }
+
+ return appName;
+ }
- string newFileWithExtension = fileWithoutExtension + "." + Win32Native.GetCurrentProcessId() + extension;
- filePath = Path.Combine(directoryName, newFileWithExtension);
+ private static void SetProviderConfiguration(string strConfig, EventPipeConfiguration config)
+ {
+ if (string.IsNullOrEmpty(strConfig))
+ {
+ throw new ArgumentNullException(nameof(strConfig));
}
- return filePath;
+ // String must be of the form "providerName:keywords:level,providerName:keywords:level..."
+ string[] providers = strConfig.Split(ProviderConfigDelimiter);
+ foreach (string provider in providers)
+ {
+ string[] components = provider.Split(ConfigComponentDelimiter);
+ if (components.Length == 3)
+ {
+ string providerName = components[0];
+
+ // We use a try/catch block here because ulong.TryParse won't accept 0x at the beginning
+ // of a hex string. Thus, we either need to conditionally strip it or handle the exception.
+ // Given that this is not a perf-critical path, catching the exception is the simpler code.
+ ulong keywords = 0;
+ try
+ {
+ keywords = Convert.ToUInt64(components[1], 16);
+ }
+ catch { }
+
+ uint level;
+ if (!uint.TryParse(components[2], out level))
+ {
+ level = 0;
+ }
+
+ config.EnableProvider(providerName, keywords, level);
+ }
+ }
}
#region Configuration
+ // Cache for COMPlus configuration variables.
+ private static int s_Config_EnableEventPipe = -1;
+ private static string s_Config_EventPipeConfig = null;
+ private static uint s_Config_EventPipeCircularMB = 0;
+ private static string s_Config_EventPipeOutputPath = null;
+
private static int Config_EnableEventPipe
{
get
}
}
- private static string Config_EventPipeOutputFile
+ private static string Config_EventPipeOutputPath
{
get
{
- if (s_Config_EventPipeOutputFile == null)
+ if (s_Config_EventPipeOutputPath == null)
{
- s_Config_EventPipeOutputFile = CompatibilitySwitch.GetValueInternal("EventPipeOutputFile");
- if (s_Config_EventPipeOutputFile == null)
+ s_Config_EventPipeOutputPath = CompatibilitySwitch.GetValueInternal("EventPipeOutputPath");
+ if (s_Config_EventPipeOutputPath == null)
{
- s_Config_EventPipeOutputFile = "Process-" + Win32Native.GetCurrentProcessId() + NetPerfFileExtension;
+ s_Config_EventPipeOutputPath = ".";
}
}
- return s_Config_EventPipeOutputFile;
- }
- }
-
- private static string MarkerFilePath
- {
- get
- {
- return Config_EventPipeOutputFile + MarkerFileExtension;
+ return s_Config_EventPipeOutputPath;
}
}
return &s_configCrst;
}
-void EventPipe::GetConfigurationFromEnvironment(SString &outputPath, EventPipeSession *pSession)
-{
- LIMITED_METHOD_CONTRACT;
-
- // Set the output path if specified.
- CLRConfigStringHolder wszOutputPath(CLRConfig::GetConfigValue(CLRConfig::INTERNAL_EventPipeOutputFile));
- if(wszOutputPath != NULL)
- {
- outputPath.Set(wszOutputPath);
- }
-
- // Read the the provider configuration from the environment if specified.
- CLRConfigStringHolder wszConfig(CLRConfig::GetConfigValue(CLRConfig::INTERNAL_EventPipeConfig));
- if(wszConfig == NULL)
- {
- pSession->EnableAllEvents();
- return;
- }
-
- size_t len = wcslen(wszConfig);
- if(len <= 0)
- {
- pSession->EnableAllEvents();
- return;
- }
-
- // Parses a string with the following format:
- //
- // ProviderName:Keywords:Level[,]*
- //
- // For example:
- //
- // Microsoft-Windows-DotNETRuntime:0xCAFEBABE:2,Microsoft-Windows-DotNETRuntimePrivate:0xDEADBEEF:1
- //
- // Each provider configuration is separated by a ',' and each component within the configuration is
- // separated by a ':'.
-
- const WCHAR ProviderSeparatorChar = ',';
- const WCHAR ComponentSeparatorChar = ':';
- size_t index = 0;
- WCHAR *pProviderName = NULL;
- UINT64 keywords = 0;
- EventPipeEventLevel level = EventPipeEventLevel::Critical;
-
- while(index < len)
- {
- WCHAR * pCurrentChunk = &wszConfig[index];
- size_t currentChunkStartIndex = index;
- size_t currentChunkEndIndex = 0;
-
- // Find the next chunk.
- while(index < len && wszConfig[index] != ProviderSeparatorChar)
- {
- index++;
- }
- currentChunkEndIndex = index++;
-
- // Split the chunk into components.
- size_t chunkIndex = currentChunkStartIndex;
-
- // Get the provider name.
- size_t provNameStartIndex = chunkIndex;
- size_t provNameEndIndex = currentChunkEndIndex;
-
- while(chunkIndex < currentChunkEndIndex && wszConfig[chunkIndex] != ComponentSeparatorChar)
- {
- chunkIndex++;
- }
- provNameEndIndex = chunkIndex++;
-
- size_t provNameLen = provNameEndIndex - provNameStartIndex;
- pProviderName = new WCHAR[provNameLen+1];
- memcpy(pProviderName, &wszConfig[provNameStartIndex], provNameLen*sizeof(WCHAR));
- pProviderName[provNameLen] = '\0';
-
- // Get the keywords.
- size_t keywordsStartIndex = chunkIndex;
- size_t keywordsEndIndex = currentChunkEndIndex;
-
- while(chunkIndex < currentChunkEndIndex && wszConfig[chunkIndex] != ComponentSeparatorChar)
- {
- chunkIndex++;
- }
- keywordsEndIndex = chunkIndex++;
-
- size_t keywordsLen = keywordsEndIndex - keywordsStartIndex;
- WCHAR *wszKeywords = new WCHAR[keywordsLen+1];
- memcpy(wszKeywords, &wszConfig[keywordsStartIndex], keywordsLen*sizeof(WCHAR));
- wszKeywords[keywordsLen] = '\0';
- keywords = _wcstoui64(wszKeywords, NULL, 16);
- delete[] wszKeywords;
- wszKeywords = NULL;
-
- // Get the level.
- size_t levelStartIndex = chunkIndex;
- size_t levelEndIndex = currentChunkEndIndex;
-
- while(chunkIndex < currentChunkEndIndex && wszConfig[chunkIndex] != ComponentSeparatorChar)
- {
- chunkIndex++;
- }
- levelEndIndex = chunkIndex++;
-
- size_t levelLen = levelEndIndex - levelStartIndex;
- WCHAR *wszLevel = new WCHAR[levelLen+1];
- memcpy(wszLevel, &wszConfig[levelStartIndex], levelLen*sizeof(WCHAR));
- wszLevel[levelLen] = '\0';
- level = (EventPipeEventLevel) wcstoul(wszLevel, NULL, 16);
- delete[] wszLevel;
- wszLevel = NULL;
-
- // Add a new EventPipeSessionProvider.
- EventPipeSessionProvider *pSessionProvider = new EventPipeSessionProvider(pProviderName, keywords, level);
- pSession->AddSessionProvider(pSessionProvider);
-
- // Free the provider name string.
- if(pProviderName != NULL)
- {
- delete[] pProviderName;
- pProviderName = NULL;
- }
- }
-}
-
void EventPipe::SaveCommandLine(LPCWSTR pwzAssemblyPath, int argc, LPCWSTR *argv)
{
CONTRACTL
--- /dev/null
+// 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.IO;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Tracing.Tests.Common;
+
+using Microsoft.Diagnostics.Tracing;
+using Microsoft.Diagnostics.Tracing.Parsers.Clr;
+
+namespace Tracing.Tests
+{
+ public static class TraceControlTest
+ {
+ private static string ConfigFileContents = @"
+OutputPath=.
+CircularMB=2048
+Providers=*:0xFFFFFFFFFFFFFFFF:5
+";
+
+ private const int BytesInOneMB = 1024 * 1024;
+
+ /// <summary>
+ /// This test collects a trace of itself and then performs some basic validation on the trace.
+ /// </summary>
+ public static int Main(string[] args)
+ {
+ // Calculate the path to the config file.
+ string configFileName = Assembly.GetEntryAssembly().GetName().Name + ".eventpipeconfig";
+ string configFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, configFileName);
+ Console.WriteLine("Calculated config file path: " + configFilePath);
+
+ // Write the config file to disk.
+ File.WriteAllText(configFilePath, ConfigFileContents);
+ Console.WriteLine("Wrote contents of config file.");
+
+ // Wait 5 seconds to ensure that tracing has started.
+ Console.WriteLine("Waiting 5 seconds for the config file to be picked up by the next poll operation.");
+ Thread.Sleep(TimeSpan.FromSeconds(5));
+
+ // Do some work that we can look for in the trace.
+ Console.WriteLine("Do some work that will be captured by the trace.");
+ GC.Collect(2, GCCollectionMode.Forced);
+ Console.WriteLine("Done with the work.");
+
+ // Delete the config file to start tracing.
+ File.Delete(configFilePath);
+ Console.WriteLine("Deleted the config file.");
+
+ // Build the full path to the trace file.
+ string[] traceFiles = Directory.GetFiles(".", "*.netperf", SearchOption.TopDirectoryOnly);
+ Assert.Equal("traceFiles.Length == 1", traceFiles.Length, 1);
+ string traceFilePath = traceFiles[0];
+
+ // Poll the file system and wait for the trace file to be written.
+ Console.WriteLine("Wait for the config file deletion to be picked up and for the trace file to be written.");
+
+ // Wait for 1 second, which is the poll time when tracing is enabled.
+ Thread.Sleep(TimeSpan.FromSeconds(1));
+
+ // Poll for file size changes to the trace file itself. When the size of the trace file hasn't changed for 5 seconds, consider it fully written out.
+ Console.WriteLine("Waiting for the trace file to be written. Poll every second to watch for 5 seconds of no file size changes.");
+ long lastSizeInBytes = 0;
+ DateTime timeOfLastChangeUTC = DateTime.UtcNow;
+ do
+ {
+ FileInfo traceFileInfo = new FileInfo(traceFilePath);
+ long currentSizeInBytes = traceFileInfo.Length;
+ Console.WriteLine("Trace file size: " + ((double)currentSizeInBytes / BytesInOneMB));
+
+ if (currentSizeInBytes > lastSizeInBytes)
+ {
+ lastSizeInBytes = currentSizeInBytes;
+ timeOfLastChangeUTC = DateTime.UtcNow;
+ }
+
+ Thread.Sleep(TimeSpan.FromSeconds(1));
+
+ } while (DateTime.UtcNow.Subtract(timeOfLastChangeUTC) < TimeSpan.FromSeconds(5));
+
+ int retVal = 0;
+
+ // Use TraceEvent to consume the trace file and look for the work that we did.
+ Console.WriteLine("Using TraceEvent to parse the file to find the work that was done during trace capture.");
+ using (var trace = TraceEventDispatcher.GetDispatcherFromFileName(traceFilePath))
+ {
+ string gcReasonInduced = GCReason.Induced.ToString();
+ string providerName = "Microsoft-Windows-DotNETRuntime";
+ string gcTriggeredEventName = "GC/Triggered";
+
+ trace.Clr.GCTriggered += delegate (GCTriggeredTraceData data)
+ {
+ if (gcReasonInduced.Equals(data.Reason.ToString()))
+ {
+ Console.WriteLine("Detected an induced GC");
+ retVal = 100;
+ }
+ };
+
+ trace.Process();
+ }
+
+ // Clean-up the resulting trace file.
+ File.Delete(traceFilePath);
+
+ return retVal;
+ }
+ }
+}