From: Sung Yoon Whang Date: Fri, 29 Mar 2019 07:03:08 +0000 (-0700) Subject: Make dotnet-counters use IPC to enable/disable counters (#148) X-Git-Tag: submit/tizen/20190813.035844~41 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=53729a143545d843eca4ca5a03405a5a5a04bfbb;p=platform%2Fcore%2Fdotnet%2Fdiagnostics.git Make dotnet-counters use IPC to enable/disable counters (#148) * Modify dotnet-counters to use IPC to enable/disable counters * Add parsing for counterList command line arg * remove unused using * move scope of defaultProvider * Update RuntimeClient namespace --- diff --git a/src/Tools/dotnet-counters/CollectionConfiguration.cs b/src/Tools/dotnet-counters/CollectionConfiguration.cs deleted file mode 100644 index c1776ca42..000000000 --- a/src/Tools/dotnet-counters/CollectionConfiguration.cs +++ /dev/null @@ -1,55 +0,0 @@ -// 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 Providers { get; set; } = new List(); - - 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 providers) => string.Join(",", providers.Select(p => SerializeProvider(p))); - - private string SerializeProvider(CounterProvider provider) - { - return $"{provider.Name}:{provider.Keywords}:{provider.Level}:EventCounterIntervalSec={Interval}"; - } - - } -} diff --git a/src/Tools/dotnet-counters/ConfigPathDetector.cs b/src/Tools/dotnet-counters/ConfigPathDetector.cs deleted file mode 100644 index abe702632..000000000 --- a/src/Tools/dotnet-counters/ConfigPathDetector.cs +++ /dev/null @@ -1,225 +0,0 @@ -// 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 _managedExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".exe", ".dll" }; - - // Known .NET Platform Assemblies - private static readonly HashSet _platformAssemblies = new HashSet(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; - - /// - /// Gets the full path to the executable file identified by the specified PID - /// - /// The PID of the running process - /// A pointer to an allocated block of memory that will be filled with the process path - /// The size of the buffer, should be PROC_PIDPATHINFO_MAXSIZE - /// Returns the length of the path returned on success - [DllImport("libproc.dylib", SetLastError = true)] - private static extern unsafe int proc_pidpath( - int pid, - byte* buffer, - uint bufferSize); - - /// - /// Gets the full path to the executable file identified by the specified PID - /// - /// The PID of the running process - /// Returns the full path to the process executable - 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 _knownNativeLibraries = new HashSet(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()) - { - // 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; - } - } - } -} diff --git a/src/Tools/dotnet-counters/CounterMonitor.cs b/src/Tools/dotnet-counters/CounterMonitor.cs index c718a3899..66cd4c030 100644 --- a/src/Tools/dotnet-counters/CounterMonitor.cs +++ b/src/Tools/dotnet-counters/CounterMonitor.cs @@ -3,20 +3,23 @@ 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; @@ -43,13 +46,26 @@ namespace Microsoft.Diagnostics.Tools.Counters 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 ToProviders(string providers) + { + if (string.IsNullOrWhiteSpace(providers)) + throw new ArgumentNullException(nameof(providers)); + return providers.Split(',') + .Select(Provider.ToProvider); + } + private async Task StartMonitor() { if (_processId == 0) { @@ -62,47 +78,53 @@ namespace Microsoft.Diagnostics.Tools.Counters 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; diff --git a/src/Tools/dotnet-counters/CounterProvider.cs b/src/Tools/dotnet-counters/CounterProvider.cs index 84cccdca6..5c6e62835 100644 --- a/src/Tools/dotnet-counters/CounterProvider.cs +++ b/src/Tools/dotnet-counters/CounterProvider.cs @@ -24,6 +24,11 @@ namespace Microsoft.Diagnostics.Tools.Counters Level = level; Counters = counters.ToList(); } + + public string ToProviderString(int interval) + { + return $"{Name}:{Keywords}:{Level}:EventCounterIntervalSec={interval}"; + } } public class CounterProfile diff --git a/src/Tools/dotnet-counters/EventCollector.cs b/src/Tools/dotnet-counters/EventCollector.cs deleted file mode 100644 index f81327db2..000000000 --- a/src/Tools/dotnet-counters/EventCollector.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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(); - } -} diff --git a/src/Tools/dotnet-counters/EventPipeCollector.cs b/src/Tools/dotnet-counters/EventPipeCollector.cs deleted file mode 100644 index 255b62c73..000000000 --- a/src/Tools/dotnet-counters/EventPipeCollector.cs +++ /dev/null @@ -1,29 +0,0 @@ -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; - } - } -} diff --git a/src/Tools/dotnet-counters/StringParser.cs b/src/Tools/dotnet-counters/StringParser.cs deleted file mode 100644 index 5c3c643cc..000000000 --- a/src/Tools/dotnet-counters/StringParser.cs +++ /dev/null @@ -1,334 +0,0 @@ -// 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 -{ - /// - /// Provides a string parser that may be used instead of String.Split - /// to avoid unnecessary string and array allocations. - /// - internal struct StringParser - { - /// The string being parsed. - private readonly string _buffer; - - /// The separator character used to separate subcomponents of the larger string. - private readonly char _separator; - - /// true if empty subcomponents should be skipped; false to treat them as valid entries. - private readonly bool _skipEmpty; - - /// The starting index from which to parse the current entry. - private int _startIndex; - - /// The ending index that represents the next index after the last character that's part of the current entry. - private int _endIndex; - - /// Initialize the StringParser. - /// The string to parse. - /// The separator character used to separate subcomponents of . - /// true if empty subcomponents should be skipped; false to treat them as valid entries. Defaults to false. - 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; - } - - /// Moves to the next component of the string. - /// true if there is a next component to be parsed; otherwise, false. - 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; - } - } - } - - /// - /// Moves to the next component of the string. If there isn't one, it throws an exception. - /// - public void MoveNextOrFail() - { - if (!MoveNext()) - { - ThrowForInvalidData(); - } - } - - /// - /// Moves to the next component of the string and returns it as a string. - /// - /// - public string MoveAndExtractNext() - { - MoveNextOrFail(); - return _buffer.Substring(_startIndex, _endIndex - _startIndex); - } - - /// - /// 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. - /// - /// - 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; - } - - /// - /// Gets the current subcomponent of the string as a string. - /// - public string ExtractCurrent() - { - if (_buffer == null || _startIndex == -1) - { - throw new InvalidOperationException(); - } - return _buffer.Substring(_startIndex, _endIndex - _startIndex); - } - - /// Moves to the next component and parses it as an Int32. - 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; - } - - /// Moves to the next component and parses it as an Int64. - 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; - } - - /// Moves to the next component and parses it as a UInt32. - 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; - } - - /// Moves to the next component and parses it as a UInt64. - 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; - } - - /// Moves to the next component and parses it as a Char. - 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(string buffer, ref int startIndex, ref int endIndex); - - /// - /// 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. - /// - internal T ParseRaw(ParseRawFunc selector) - { - MoveNextOrFail(); - return selector(_buffer, ref _startIndex, ref _endIndex); - } - - /// - /// Gets the current subcomponent and all remaining components of the string as a string. - /// - public string ExtractCurrentToEnd() - { - if (_buffer == null || _startIndex == -1) - { - throw new InvalidOperationException(); - } - return _buffer.Substring(_startIndex); - } - - /// Throws unconditionally for invalid data. - private static void ThrowForInvalidData() - { - throw new InvalidDataException(); - } - } -} diff --git a/src/Tools/dotnet-counters/dotnet-counters.csproj b/src/Tools/dotnet-counters/dotnet-counters.csproj index 3cf98d847..d1b91a1c0 100644 --- a/src/Tools/dotnet-counters/dotnet-counters.csproj +++ b/src/Tools/dotnet-counters/dotnet-counters.csproj @@ -23,11 +23,12 @@ - + + \ No newline at end of file