+++ /dev/null
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.Tracing;
-using System.Linq;
-using System.Text;
-
-namespace Microsoft.Diagnostics.Tools.Counters
-{
- public class CollectionConfiguration
- {
- public int? ProcessId { get; set; }
- public string OutputPath { get; set; }
- public int? CircularMB { get; set; }
- public int? Interval { get; set; }
- public IList<CounterProvider> Providers { get; set; } = new List<CounterProvider>();
-
- internal string ToConfigString()
- {
- var builder = new StringBuilder();
- if (ProcessId != null)
- {
- builder.AppendLine($"ProcessId={ProcessId.Value}");
- }
- if (!string.IsNullOrEmpty(OutputPath))
- {
- builder.AppendLine($"OutputPath={OutputPath}");
- }
- if (CircularMB != null)
- {
- builder.AppendLine($"CircularMB={CircularMB}");
- }
- if (Providers != null && Providers.Count > 0)
- {
- builder.AppendLine($"Providers={SerializeProviders(Providers)}");
- }
- return builder.ToString();
- }
-
- public void AddProvider(CounterProvider provider)
- {
- Providers.Add(provider);
- }
-
- private string SerializeProviders(IEnumerable<CounterProvider> providers) => string.Join(",", providers.Select(p => SerializeProvider(p)));
-
- private string SerializeProvider(CounterProvider provider)
- {
- return $"{provider.Name}:{provider.Keywords}:{provider.Level}:EventCounterIntervalSec={Interval}";
- }
-
- }
-}
+++ /dev/null
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-
-namespace Microsoft.Diagnostics.Tools.Counters
-{
- // NOTE: This is copied over from dotnet-collect.
- // This will eventually go away once we get the runtime ready to stream events via IPC.
- internal static class ConfigPathDetector
- {
- private static readonly HashSet<string> _managedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".exe", ".dll" };
-
- // Known .NET Platform Assemblies
- private static readonly HashSet<string> _platformAssemblies = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "System.Private.CoreLib.dll",
- "clrjit.dll",
- };
-
- internal static string TryDetectConfigPath(int processId)
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- return Windows.TryDetectConfigPath(processId);
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- {
- return Linux.TryDetectConfigPath(processId);
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- {
- return OSX.TryDetectConfigPath(processId);
- }
- return null;
- }
-
- private static class OSX
- {
- // This is defined in proc_info.h (https://opensource.apple.com/source/xnu/xnu-1228/bsd/sys/proc_info.h)
- private const int PROC_PIDPATHINFO_MAXSIZE = 1024 * 4;
-
- /// <summary>
- /// Gets the full path to the executable file identified by the specified PID
- /// </summary>
- /// <param name="pid">The PID of the running process</param>
- /// <param name="buffer">A pointer to an allocated block of memory that will be filled with the process path</param>
- /// <param name="bufferSize">The size of the buffer, should be PROC_PIDPATHINFO_MAXSIZE</param>
- /// <returns>Returns the length of the path returned on success</returns>
- [DllImport("libproc.dylib", SetLastError = true)]
- private static extern unsafe int proc_pidpath(
- int pid,
- byte* buffer,
- uint bufferSize);
-
- /// <summary>
- /// Gets the full path to the executable file identified by the specified PID
- /// </summary>
- /// <param name="pid">The PID of the running process</param>
- /// <returns>Returns the full path to the process executable</returns>
- internal static unsafe string proc_pidpath(int pid)
- {
- // The path is a fixed buffer size, so use that and trim it after
- int result = 0;
- byte* pBuffer = stackalloc byte[PROC_PIDPATHINFO_MAXSIZE];
-
- // WARNING - Despite its name, don't try to pass in a smaller size than specified by PROC_PIDPATHINFO_MAXSIZE.
- // For some reason libproc returns -1 if you specify something that's NOT EQUAL to PROC_PIDPATHINFO_MAXSIZE
- // even if you declare your buffer to be smaller/larger than this size.
- result = proc_pidpath(pid, pBuffer, (uint)(PROC_PIDPATHINFO_MAXSIZE * sizeof(byte)));
- if (result <= 0)
- {
- throw new InvalidOperationException("Could not find procpath using libproc.");
- }
-
- // OS X uses UTF-8. The conversion may not strip off all trailing \0s so remove them here
- return System.Text.Encoding.UTF8.GetString(pBuffer, result);
- }
-
- public static string TryDetectConfigPath(int processId)
- {
- try
- {
- var path = proc_pidpath(processId);
- var candidateDir = Path.GetDirectoryName(path);
- var candidateName = Path.GetFileNameWithoutExtension(path);
- return Path.Combine(candidateDir, $"{candidateName}.eventpipeconfig");
- }
- catch (InvalidOperationException)
- {
- return null; // The pinvoke above may fail - return null in that case to handle error gracefully.
- }
- }
- }
-
- private static class Linux
- {
- public static string TryDetectConfigPath(int processId)
- {
- // Read procfs maps list
- var lines = File.ReadAllLines($"/proc/{processId}/maps");
-
- foreach (var line in lines)
- {
- try
- {
- var parser = new StringParser(line, separator: ' ', skipEmpty: true);
-
- // Skip the address range
- parser.MoveNext();
-
- var permissions = parser.MoveAndExtractNext();
-
- // The managed entry point is Read-Only, Non-Execute and Shared.
- if (!string.Equals(permissions, "r--s", StringComparison.Ordinal))
- {
- continue;
- }
-
- // Skip offset, dev, and inode
- parser.MoveNext();
- parser.MoveNext();
- parser.MoveNext();
-
- // Parse the path
- if (!parser.MoveNext())
- {
- continue;
- }
-
- var path = parser.ExtractCurrentToEnd();
- var candidateDir = Path.GetDirectoryName(path);
- var candidateName = Path.GetFileNameWithoutExtension(path);
- if (File.Exists(Path.Combine(candidateDir, $"{candidateName}.deps.json")))
- {
- return Path.Combine(candidateDir, $"{candidateName}.eventpipeconfig");
- }
- }
- catch (ArgumentNullException) { return null; }
- catch (InvalidDataException) { return null; }
- catch (InvalidOperationException) { return null; }
- }
- return null;
- }
- }
-
- private static class Windows
- {
- private static readonly HashSet<string> _knownNativeLibraries = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- // .NET Core Host
- "dotnet.exe",
- "hostfxr.dll",
- "hostpolicy.dll",
- "coreclr.dll",
-
- // Windows Native Libraries
- "ntdll.dll",
- "kernel32.dll",
- "kernelbase.dll",
- "apphelp.dll",
- "ucrtbase.dll",
- "advapi32.dll",
- "msvcrt.dll",
- "sechost.dll",
- "rpcrt4.dll",
- "ole32.dll",
- "combase.dll",
- "bcryptPrimitives.dll",
- "gdi32.dll",
- "gdi32full.dll",
- "msvcp_win.dll",
- "user32.dll",
- "win32u.dll",
- "oleaut32.dll",
- "shlwapi.dll",
- "version.dll",
- "bcrypt.dll",
- "imm32.dll",
- "kernel.appcore.dll",
- };
-
- public static string TryDetectConfigPath(int processId)
- {
- try
- {
-
- var process = Process.GetProcessById(processId);
-
- // Iterate over modules
- foreach (var module in process.Modules.Cast<ProcessModule>())
- {
- // Filter out things that aren't exes and dlls (useful on Unix/macOS to skip native libraries)
- var extension = Path.GetExtension(module.FileName);
- var name = Path.GetFileName(module.FileName);
- if (_managedExtensions.Contains(extension) && !_knownNativeLibraries.Contains(name) && !_platformAssemblies.Contains(name))
- {
- var candidateDir = Path.GetDirectoryName(module.FileName);
- var appName = Path.GetFileNameWithoutExtension(module.FileName);
-
- // Check for the deps.json file
- // TODO: Self-contained apps?
- if (File.Exists(Path.Combine(candidateDir, $"{appName}.deps.json")))
- {
- // This is an app!
- return Path.Combine(candidateDir, $"{appName}.eventpipeconfig");
- }
- }
- }
- }
- catch (ArgumentException)
- {
- return null;
- }
-
- return null;
- }
- }
- }
-}
using System;
using System.CommandLine;
-using System.Diagnostics;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
+using Microsoft.Diagnostics.Tools.RuntimeClient;
+
namespace Microsoft.Diagnostics.Tools.Counters
{
public class CounterMonitor
{
- private string configPath;
- private EventPipeCollector collector;
- private CollectionConfiguration config;
+ private string outputPath;
+ private ulong sessionId;
private int _processId;
private int _interval;
catch (OperationCanceledException)
{
- await collector.StopCollectingAsync();
- console.Out.WriteLine($"Tracing stopped. Trace files written to {config.OutputPath}");
+ try
+ {
+ EventPipeClient.DisableTracingToFile(_processId, sessionId);
+ }
+ catch (Exception) {} // Swallow all exceptions for now.
+
+ console.Out.WriteLine($"Tracing stopped. Trace files written to {outputPath}");
console.Out.WriteLine($"Complete");
return 1;
}
}
+ private static IEnumerable<Provider> ToProviders(string providers)
+ {
+ if (string.IsNullOrWhiteSpace(providers))
+ throw new ArgumentNullException(nameof(providers));
+ return providers.Split(',')
+ .Select(Provider.ToProvider);
+ }
+
private async Task<int> StartMonitor()
{
if (_processId == 0) {
return 1;
}
- configPath = ConfigPathDetector.TryDetectConfigPath(_processId);
+ outputPath = Path.Combine(Directory.GetCurrentDirectory(), $"dotnet-counters-{_processId}.netperf"); // TODO: This can be removed once events can be streamed in real time.
- if(string.IsNullOrEmpty(configPath))
- {
- _console.Error.WriteLine("Couldn't determine the path for the eventpipeconfig file from the process ID.");
- return 1;
- }
-
- _console.Out.WriteLine($"Detected config file path: {configPath}");
-
- config = new CollectionConfiguration()
- {
- ProcessId = _processId,
- CircularMB = 1000, // TODO: Make this configurable?
- OutputPath = Directory.GetCurrentDirectory(),
- Interval = _interval
- };
+ String providerString;
if (string.IsNullOrEmpty(_counterList))
{
+ CounterProvider defaultProvider = null;
_console.Out.WriteLine($"counter_list is unspecified. Monitoring all counters by default.");
// Enable the default profile if nothing is specified
- if (!KnownData.TryGetProvider("System.Runtime", out var defaultProvider))
+ if (!KnownData.TryGetProvider("System.Runtime", out defaultProvider))
{
_console.Error.WriteLine("No providers or profiles were specified and there is no default profile available.");
return 1;
}
- config.AddProvider(defaultProvider);
+ providerString = defaultProvider.ToProviderString(_interval);
}
-
- if (File.Exists(configPath))
+ else
{
- _console.Error.WriteLine("Config file already exists, tracing is already underway by a different consumer.");
- return 1;
+ string[] counters = _counterList.Split(" ");
+ CounterProvider provider = null;
+ StringBuilder sb = new StringBuilder("");
+ for (var i = 0; i < counters.Length; i++)
+ {
+ if (!KnownData.TryGetProvider(counters[i], out provider))
+ {
+ _console.Error.WriteLine($"No known provider called {counters[i]}.");
+ return 1;
+ }
+ sb.Append(provider.ToProviderString(_interval));
+ if (i != counters.Length - 1)
+ {
+ sb.Append(",");
+ }
+ }
+ providerString = sb.ToString();
}
- collector = new EventPipeCollector(config, configPath);
+ var configuration = new SessionConfiguration(
+ 1000,
+ 0,
+ outputPath,
+ ToProviders(providerString));
+
+ sessionId = EventPipeClient.EnableTracingToFile(_processId, configuration);
// Write the config file contents
- await collector.StartCollectingAsync();
_console.Out.WriteLine("Tracing has started. Press Ctrl-C to stop.");
await Task.Delay(int.MaxValue, _ct);
return 0;
Level = level;
Counters = counters.ToList();
}
+
+ public string ToProviderString(int interval)
+ {
+ return $"{Name}:{Keywords}:{Level}:EventCounterIntervalSec={interval}";
+ }
}
public class CounterProfile
+++ /dev/null
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Counters
-{
- public abstract class EventCollector
- {
- public abstract Task StartCollectingAsync();
- public abstract Task StopCollectingAsync();
- }
-}
+++ /dev/null
-using System.IO;
-using System.Threading.Tasks;
-
-namespace Microsoft.Diagnostics.Tools.Counters
-{
- public class EventPipeCollector : EventCollector
- {
- private readonly CollectionConfiguration _config;
- private readonly string _configPath;
-
- public EventPipeCollector(CollectionConfiguration config, string configPath)
- {
- _config = config;
- _configPath = configPath;
- }
-
- public override Task StartCollectingAsync()
- {
- var configContent = _config.ToConfigString();
- return File.WriteAllTextAsync(_configPath, configContent);
- }
-
- public override Task StopCollectingAsync()
- {
- File.Delete(_configPath);
- return Task.CompletedTask;
- }
- }
-}
+++ /dev/null
-// Borrowed from https://github.com/dotnet/corefx/blob/b2f960abe1d8690be9d68dd9b56ea7636fb4a38b/src/Common/src/System/IO/StringParser.cs
-
-// 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.Diagnostics;
-
-namespace System.IO
-{
- /// <summary>
- /// Provides a string parser that may be used instead of String.Split
- /// to avoid unnecessary string and array allocations.
- /// </summary>
- internal struct StringParser
- {
- /// <summary>The string being parsed.</summary>
- private readonly string _buffer;
-
- /// <summary>The separator character used to separate subcomponents of the larger string.</summary>
- private readonly char _separator;
-
- /// <summary>true if empty subcomponents should be skipped; false to treat them as valid entries.</summary>
- private readonly bool _skipEmpty;
-
- /// <summary>The starting index from which to parse the current entry.</summary>
- private int _startIndex;
-
- /// <summary>The ending index that represents the next index after the last character that's part of the current entry.</summary>
- private int _endIndex;
-
- /// <summary>Initialize the StringParser.</summary>
- /// <param name="buffer">The string to parse.</param>
- /// <param name="separator">The separator character used to separate subcomponents of <paramref name="buffer"/>.</param>
- /// <param name="skipEmpty">true if empty subcomponents should be skipped; false to treat them as valid entries. Defaults to false.</param>
- public StringParser(string buffer, char separator, bool skipEmpty = false)
- {
- if (buffer == null)
- {
- throw new ArgumentNullException(nameof(buffer));
- }
- _buffer = buffer;
- _separator = separator;
- _skipEmpty = skipEmpty;
- _startIndex = -1;
- _endIndex = -1;
- }
-
- /// <summary>Moves to the next component of the string.</summary>
- /// <returns>true if there is a next component to be parsed; otherwise, false.</returns>
- public bool MoveNext()
- {
- if (_buffer == null)
- {
- throw new InvalidOperationException();
- }
-
- while (true)
- {
- if (_endIndex >= _buffer.Length)
- {
- _startIndex = _endIndex;
- return false;
- }
-
- int nextSeparator = _buffer.IndexOf(_separator, _endIndex + 1);
- _startIndex = _endIndex + 1;
- _endIndex = nextSeparator >= 0 ? nextSeparator : _buffer.Length;
-
- if (!_skipEmpty || _endIndex >= _startIndex + 1)
- {
- return true;
- }
- }
- }
-
- /// <summary>
- /// Moves to the next component of the string. If there isn't one, it throws an exception.
- /// </summary>
- public void MoveNextOrFail()
- {
- if (!MoveNext())
- {
- ThrowForInvalidData();
- }
- }
-
- /// <summary>
- /// Moves to the next component of the string and returns it as a string.
- /// </summary>
- /// <returns></returns>
- public string MoveAndExtractNext()
- {
- MoveNextOrFail();
- return _buffer.Substring(_startIndex, _endIndex - _startIndex);
- }
-
- /// <summary>
- /// Moves to the next component of the string, which must be enclosed in the only set of top-level parentheses
- /// in the string. The extracted value will be everything between (not including) those parentheses.
- /// </summary>
- /// <returns></returns>
- public string MoveAndExtractNextInOuterParens()
- {
- // Move to the next position
- MoveNextOrFail();
-
- // After doing so, we should be sitting at a the opening paren.
- if (_buffer[_startIndex] != '(')
- {
- ThrowForInvalidData();
- }
-
- // Since we only allow for one top-level set of parentheses, find the last
- // parenthesis in the string; it's paired with the opening one we just found.
- int lastParen = _buffer.LastIndexOf(')');
- if (lastParen == -1 || lastParen < _startIndex)
- {
- ThrowForInvalidData();
- }
-
- // Extract the contents of the parens, then move our ending position to be after the paren
- string result = _buffer.Substring(_startIndex + 1, lastParen - _startIndex - 1);
- _endIndex = lastParen + 1;
-
- return result;
- }
-
- /// <summary>
- /// Gets the current subcomponent of the string as a string.
- /// </summary>
- public string ExtractCurrent()
- {
- if (_buffer == null || _startIndex == -1)
- {
- throw new InvalidOperationException();
- }
- return _buffer.Substring(_startIndex, _endIndex - _startIndex);
- }
-
- /// <summary>Moves to the next component and parses it as an Int32.</summary>
- public unsafe int ParseNextInt32()
- {
- MoveNextOrFail();
-
- bool negative = false;
- int result = 0;
-
- fixed (char* bufferPtr = _buffer)
- {
- char* p = bufferPtr + _startIndex;
- char* end = bufferPtr + _endIndex;
-
- if (p == end)
- {
- ThrowForInvalidData();
- }
-
- if (*p == '-')
- {
- negative = true;
- p++;
- if (p == end)
- {
- ThrowForInvalidData();
- }
- }
-
- while (p != end)
- {
- int d = *p - '0';
- if (d < 0 || d > 9)
- {
- ThrowForInvalidData();
- }
- result = negative ? checked((result * 10) - d) : checked((result * 10) + d);
-
- p++;
- }
- }
-
- Debug.Assert(result == int.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
- return result;
- }
-
- /// <summary>Moves to the next component and parses it as an Int64.</summary>
- public unsafe long ParseNextInt64()
- {
- MoveNextOrFail();
-
- bool negative = false;
- long result = 0;
-
- fixed (char* bufferPtr = _buffer)
- {
- char* p = bufferPtr + _startIndex;
- char* end = bufferPtr + _endIndex;
-
- if (p == end)
- {
- ThrowForInvalidData();
- }
-
- if (*p == '-')
- {
- negative = true;
- p++;
- if (p == end)
- {
- ThrowForInvalidData();
- }
- }
-
- while (p != end)
- {
- int d = *p - '0';
- if (d < 0 || d > 9)
- {
- ThrowForInvalidData();
- }
- result = negative ? checked((result * 10) - d) : checked((result * 10) + d);
-
- p++;
- }
- }
-
- Debug.Assert(result == long.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
- return result;
- }
-
- /// <summary>Moves to the next component and parses it as a UInt32.</summary>
- public unsafe uint ParseNextUInt32()
- {
- MoveNextOrFail();
- if (_startIndex == _endIndex)
- {
- ThrowForInvalidData();
- }
-
- uint result = 0;
- fixed (char* bufferPtr = _buffer)
- {
- char* p = bufferPtr + _startIndex;
- char* end = bufferPtr + _endIndex;
- while (p != end)
- {
- int d = *p - '0';
- if (d < 0 || d > 9)
- {
- ThrowForInvalidData();
- }
- result = (uint)checked((result * 10) + d);
-
- p++;
- }
- }
-
- Debug.Assert(result == uint.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
- return result;
- }
-
- /// <summary>Moves to the next component and parses it as a UInt64.</summary>
- public unsafe ulong ParseNextUInt64()
- {
- MoveNextOrFail();
-
- ulong result = 0;
- fixed (char* bufferPtr = _buffer)
- {
- char* p = bufferPtr + _startIndex;
- char* end = bufferPtr + _endIndex;
- while (p != end)
- {
- int d = *p - '0';
- if (d < 0 || d > 9)
- {
- ThrowForInvalidData();
- }
- result = checked((result * 10ul) + (ulong)d);
-
- p++;
- }
- }
-
- Debug.Assert(result == ulong.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
- return result;
- }
-
- /// <summary>Moves to the next component and parses it as a Char.</summary>
- public char ParseNextChar()
- {
- MoveNextOrFail();
-
- if (_endIndex - _startIndex != 1)
- {
- ThrowForInvalidData();
- }
- char result = _buffer[_startIndex];
-
- Debug.Assert(result == char.Parse(ExtractCurrent()), "Expected manually parsed result to match Parse result");
- return result;
- }
-
- internal delegate T ParseRawFunc<T>(string buffer, ref int startIndex, ref int endIndex);
-
- /// <summary>
- /// Moves to the next component and hands the raw buffer and indexing data to a selector function
- /// that can validate and return the appropriate data from the component.
- /// </summary>
- internal T ParseRaw<T>(ParseRawFunc<T> selector)
- {
- MoveNextOrFail();
- return selector(_buffer, ref _startIndex, ref _endIndex);
- }
-
- /// <summary>
- /// Gets the current subcomponent and all remaining components of the string as a string.
- /// </summary>
- public string ExtractCurrentToEnd()
- {
- if (_buffer == null || _startIndex == -1)
- {
- throw new InvalidOperationException();
- }
- return _buffer.Substring(_startIndex);
- }
-
- /// <summary>Throws unconditionally for invalid data.</summary>
- private static void ThrowForInvalidData()
- {
- throw new InvalidDataException();
- }
- }
-}
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.30" />
+ <PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.38" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Microsoft.Diagnostic.Repl\Microsoft.Diagnostic.Repl.csproj" />
+ <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.Tools.RuntimeClient\Microsoft.Diagnostics.Tools.RuntimeClient.csproj" />
</ItemGroup>
</Project>
\ No newline at end of file