Add performance tracing

This commit is contained in:
Andrew Stanton-Nurse 2016-04-27 16:04:26 -07:00 committed by Pavel Krymets
parent 563f6497fd
commit 36e56e9d00
12 changed files with 348 additions and 50 deletions

View file

@ -92,16 +92,16 @@ namespace Microsoft.DotNet.Cli.Utils
{
return new Command(commandSpec);
}
public static Command CreateForScript(
string commandName,
IEnumerable<string> args,
Project project,
string commandName,
IEnumerable<string> args,
Project project,
string[] inferredExtensionList)
{
var commandSpec = CommandResolver.TryResolveScriptCommandSpec(commandName,
args,
project,
var commandSpec = CommandResolver.TryResolveScriptCommandSpec(commandName,
args,
project,
inferredExtensionList);
if (commandSpec == null)
@ -128,15 +128,18 @@ namespace Microsoft.DotNet.Cli.Utils
var sw = Stopwatch.StartNew();
Reporter.Verbose.WriteLine($"> {FormatProcessInfo(_process.StartInfo)}".White());
#endif
_process.Start();
using (PerfTrace.Current.CaptureTiming($"{Path.GetFileNameWithoutExtension(_process.StartInfo.FileName)} {_process.StartInfo.Arguments}"))
{
_process.Start();
Reporter.Verbose.WriteLine($"Process ID: {_process.Id}");
Reporter.Verbose.WriteLine($"Process ID: {_process.Id}");
var taskOut = _stdOut.BeginRead(_process.StandardOutput);
var taskErr = _stdErr.BeginRead(_process.StandardError);
_process.WaitForExit();
var taskOut = _stdOut.BeginRead(_process.StandardOutput);
var taskErr = _stdErr.BeginRead(_process.StandardError);
_process.WaitForExit();
Task.WaitAll(taskOut, taskErr);
Task.WaitAll(taskOut, taskErr);
}
var exitCode = _process.ExitCode;

View file

@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace Microsoft.DotNet.Cli.Utils
{
public static class PerfTrace
{
private static ConcurrentBag<PerfTraceThreadContext> _threads = new ConcurrentBag<PerfTraceThreadContext>();
[ThreadStatic]
private static PerfTraceThreadContext _current;
public static bool Enabled { get; set; }
public static PerfTraceThreadContext Current => _current ?? (_current = InitializeCurrent());
private static PerfTraceThreadContext InitializeCurrent()
{
var context = new PerfTraceThreadContext(Thread.CurrentThread.ManagedThreadId);
_threads.Add(context);
return context;
}
public static IEnumerable<PerfTraceThreadContext> GetEvents()
{
return _threads;
}
}
}

View file

@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.DotNet.Cli.Utils
{
public class PerfTraceEvent
{
public string Type { get; }
public string Instance { get; }
public DateTime StartUtc { get; }
public TimeSpan Duration { get; }
public IList<PerfTraceEvent> Children { get; }
public PerfTraceEvent(string type, string instance, IEnumerable<PerfTraceEvent> children, DateTime startUtc, TimeSpan duration)
{
Type = type;
Instance = instance;
StartUtc = startUtc;
Duration = duration;
Children = children.OrderBy(e => e.StartUtc).ToList();
}
}
}

View file

@ -0,0 +1,81 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.DotNet.Cli.Utils
{
public class PerfTraceOutput
{
private static TimeSpan _minDuration = TimeSpan.FromSeconds(0.001);
public static void Print(Reporter reporter, IEnumerable<PerfTraceThreadContext> contexts)
{
foreach (var threadContext in contexts)
{
Print(reporter, new[] { threadContext.Root }, threadContext.Root, null);
}
}
private static void Print(Reporter reporter, IEnumerable<PerfTraceEvent> events, PerfTraceEvent root, PerfTraceEvent parent, int padding = 0)
{
foreach (var e in events)
{
if (e.Duration < _minDuration)
{
continue;
}
reporter.Write(new string(' ', padding));
reporter.WriteLine(FormatEvent(e, root, parent));
Print(reporter, e.Children, root, e, padding + 2);
}
}
private static string FormatEvent(PerfTraceEvent e, PerfTraceEvent root, PerfTraceEvent parent)
{
var builder = new StringBuilder();
FormatEventTimeStat(builder, e, root, parent);
builder.Append($" {e.Type.Bold()} {e.Instance}");
return builder.ToString();
}
private static void FormatEventTimeStat(StringBuilder builder, PerfTraceEvent e, PerfTraceEvent root, PerfTraceEvent parent)
{
builder.Append("[");
if (root != e)
{
AppendTime(builder, e.Duration.TotalSeconds / root.Duration.TotalSeconds, 0.2);
}
AppendTime(builder, e.Duration.TotalSeconds / parent?.Duration.TotalSeconds, 0.5);
builder.Append($"{e.Duration.ToString("ss\\.fff\\s").Blue()}]");
}
private static void AppendTime(StringBuilder builder, double? percent, double treshold)
{
if (percent != null)
{
var formattedPercent = $"{percent*100:00\\.00%}";
if (percent > treshold)
{
builder.Append(formattedPercent.Red());
}
else if (percent > treshold / 2)
{
builder.Append(formattedPercent.Yellow());
}
else if (percent > treshold / 5)
{
builder.Append(formattedPercent.Green());
}
else
{
builder.Append(formattedPercent);
}
builder.Append(" ");
}
}
}
}

View file

@ -0,0 +1,76 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Microsoft.DotNet.Cli.Utils
{
public class PerfTraceThreadContext
{
private readonly int _threadId;
private TimerDisposable _activeEvent;
public PerfTraceEvent Root => _activeEvent.CreateEvent();
public PerfTraceThreadContext(int threadId)
{
_activeEvent = new TimerDisposable(this, "Thread", $"{threadId.ToString()}");
_threadId = threadId;
}
public IDisposable CaptureTiming(string instance = "", [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "")
{
if(!PerfTrace.Enabled)
{
return null;
}
var newTimer = new TimerDisposable(this, $"{Path.GetFileNameWithoutExtension(filePath)}:{memberName}", instance);
var previousTimer = Interlocked.Exchange(ref _activeEvent, newTimer);
newTimer.Parent = previousTimer;
return newTimer;
}
private void RecordTiming(PerfTraceEvent newEvent, TimerDisposable parent)
{
Interlocked.Exchange(ref _activeEvent, parent);
_activeEvent.Children.Add(newEvent);
}
private class TimerDisposable : IDisposable
{
private readonly PerfTraceThreadContext _context;
private string _eventType;
private string _instance;
private DateTime _startUtc;
private Stopwatch _stopwatch = Stopwatch.StartNew();
public TimerDisposable Parent { get; set; }
public ConcurrentBag<PerfTraceEvent> Children { get; set; } = new ConcurrentBag<PerfTraceEvent>();
public TimerDisposable(PerfTraceThreadContext context, string eventType, string instance)
{
_context = context;
_eventType = eventType;
_instance = instance;
_startUtc = DateTime.UtcNow;
}
public void Dispose()
{
_stopwatch.Stop();
_context.RecordTiming(CreateEvent(), Parent);
}
public PerfTraceEvent CreateEvent() => new PerfTraceEvent(_eventType, _instance, Children, _startUtc, _stopwatch.Elapsed);
}
}
}

View file

@ -8,6 +8,8 @@ namespace Microsoft.DotNet.Cli
{
public interface ITelemetry
{
bool Enabled { get; }
void TrackEvent(string eventName, IDictionary<string, string> properties, IDictionary<string, double> measurements);
}
}

View file

@ -42,17 +42,33 @@ namespace Microsoft.DotNet.Cli
{
DebugHelper.HandleDebugSwitch(ref args);
if (Env.GetEnvironmentVariableAsBool("DOTNET_CLI_CAPTURE_TIMING", false))
{
PerfTrace.Enabled = true;
}
InitializeProcess();
try
{
return Program.ProcessArgs(args, new Telemetry());
using (PerfTrace.Current.CaptureTiming())
{
return ProcessArgs(args, new Telemetry());
}
}
catch (GracefulException e)
{
Console.WriteLine(e.Message.Red().Bold());
return 1;
}
finally
{
if (PerfTrace.Enabled)
{
Reporter.Output.WriteLine("Performance Summary:");
PerfTraceOutput.Print(Reporter.Output, PerfTrace.GetEvents());
}
}
}
internal static int ProcessArgs(string[] args, ITelemetry telemetryClient)
@ -107,6 +123,7 @@ namespace Microsoft.DotNet.Cli
if (verbose.HasValue)
{
Environment.SetEnvironmentVariable(CommandContext.Variables.Verbose, verbose.ToString());
Console.WriteLine($"Telemetry is: {(telemetryClient.Enabled ? "Enabled" : "Disabled")}");
}
if (string.IsNullOrEmpty(command))
@ -141,6 +158,7 @@ namespace Microsoft.DotNet.Cli
}
private static void InitializeProcess()
{
// by default, .NET Core doesn't have all code pages needed for Console apps.

View file

@ -22,36 +22,41 @@ namespace Microsoft.DotNet.Cli
private const string RuntimeId = "Runtime Id";
private const string ProductVersion = "Product Version";
public bool Enabled { get; }
public Telemetry()
{
bool optout = Env.GetEnvironmentVariableAsBool(TelemetryOptout);
Enabled = !Env.GetEnvironmentVariableAsBool(TelemetryOptout);
if (optout)
if (!Enabled)
{
return;
}
try
{
_client = new TelemetryClient();
_client.InstrumentationKey = InstrumentationKey;
_client.Context.Session.Id = Guid.NewGuid().ToString();
using (PerfTrace.Current.CaptureTiming())
{
_client = new TelemetryClient();
_client.InstrumentationKey = InstrumentationKey;
_client.Context.Session.Id = Guid.NewGuid().ToString();
var runtimeEnvironment = PlatformServices.Default.Runtime;
_client.Context.Device.OperatingSystem = runtimeEnvironment.OperatingSystem;
var runtimeEnvironment = PlatformServices.Default.Runtime;
_client.Context.Device.OperatingSystem = runtimeEnvironment.OperatingSystem;
_commonProperties = new Dictionary<string, string>();
_commonProperties.Add(OSVersion, runtimeEnvironment.OperatingSystemVersion);
_commonProperties.Add(OSPlatform, runtimeEnvironment.OperatingSystemPlatform.ToString());
_commonProperties.Add(RuntimeId, runtimeEnvironment.GetRuntimeIdentifier());
_commonProperties.Add(ProductVersion, Product.Version);
_commonMeasurements = new Dictionary<string, double>();
_commonProperties = new Dictionary<string, string>();
_commonProperties.Add(OSVersion, runtimeEnvironment.OperatingSystemVersion);
_commonProperties.Add(OSPlatform, runtimeEnvironment.OperatingSystemPlatform.ToString());
_commonProperties.Add(RuntimeId, runtimeEnvironment.GetRuntimeIdentifier());
_commonProperties.Add(ProductVersion, Product.Version);
_commonMeasurements = new Dictionary<string, double>();
}
_isInitialized = true;
}
catch (Exception)
{
// we dont want to fail the tool if telemetry fais. We should be able to detect abnormalities from data
// we dont want to fail the tool if telemetry fais. We should be able to detect abnormalities from data
// at the server end
Debug.Fail("Exception during telemetry initialization");
}
@ -64,17 +69,20 @@ namespace Microsoft.DotNet.Cli
return;
}
Dictionary<string, double> eventMeasurements = GetEventMeasures(measurements);
Dictionary<string, string> eventProperties = GetEventProperties(properties);
using (PerfTrace.Current.CaptureTiming())
{
Dictionary<string, double> eventMeasurements = GetEventMeasures(measurements);
Dictionary<string, string> eventProperties = GetEventProperties(properties);
try
{
_client.TrackEvent(eventName, eventProperties, eventMeasurements);
_client.Flush();
}
catch (Exception)
{
Debug.Fail("Exception during TrackEvent");
try
{
_client.TrackEvent(eventName, eventProperties, eventMeasurements);
_client.Flush();
}
catch (Exception)
{
Debug.Fail("Exception during TrackEvent");
}
}
}

View file

@ -48,8 +48,17 @@ namespace Microsoft.DotNet.Tools.Build
!builderCommandApp.ShouldSkipDependencies,
(project, target) => args.Workspace.GetProjectContext(project, target));
var contexts = ResolveRootContexts(files, frameworks, args);
var graph = graphCollector.Collect(contexts).ToArray();
IEnumerable<ProjectContext> contexts;
using (PerfTrace.Current.CaptureTiming(string.Empty, nameof(ResolveRootContexts)))
{
contexts = ResolveRootContexts(files, frameworks, args);
}
ProjectGraphNode[] graph;
using (PerfTrace.Current.CaptureTiming(string.Empty, "Collect graph"))
{
graph = graphCollector.Collect(contexts).ToArray();
}
var builder = new DotNetProjectBuilder(builderCommandApp);
return builder.Build(graph).ToArray().All(r => r != CompilationResult.Failure);
}
@ -63,7 +72,11 @@ namespace Microsoft.DotNet.Tools.Build
foreach (var file in files)
{
var project = args.Workspace.GetProject(file);
Project project;
using (PerfTrace.Current.CaptureTiming(file, "Loading project.json"))
{
project = args.Workspace.GetProject(file);
}
var projectFrameworks = project.GetTargetFrameworks().Select(f => f.FrameworkName);
if (!projectFrameworks.Any())
{
@ -92,7 +105,10 @@ namespace Microsoft.DotNet.Tools.Build
tasks.Add(Task.Run(() => args.Workspace.GetProjectContext(file, framework)));
}
}
return Task.WhenAll(tasks).GetAwaiter().GetResult();
using (PerfTrace.Current.CaptureTiming(string.Empty, "Waiting for project contexts to finish loading"))
{
return Task.WhenAll(tasks).GetAwaiter().GetResult();
}
}
}
}

View file

@ -7,6 +7,8 @@ using System.Linq;
using Microsoft.DotNet.ProjectModel;
using Microsoft.DotNet.ProjectModel.Files;
using NuGet.Frameworks;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Compiler.Common;
namespace Microsoft.DotNet.Tools.Build
{
@ -18,7 +20,10 @@ namespace Microsoft.DotNet.Tools.Build
{
foreach (var projectNode in roots)
{
yield return Build(projectNode);
using (PerfTrace.Current.CaptureTiming($"{projectNode.ProjectContext.ProjectName()}"))
{
yield return Build(projectNode);
}
}
}
@ -68,18 +73,33 @@ namespace Microsoft.DotNet.Tools.Build
}
var context = projectNode.ProjectContext;
if (!HasSourceFiles(context))
using (PerfTrace.Current.CaptureTiming($"{projectNode.ProjectContext.ProjectName()}", nameof(HasSourceFiles)))
{
return CompilationResult.IncrementalSkip;
if (!HasSourceFiles(context))
{
return CompilationResult.IncrementalSkip;
}
}
if (NeedsRebuilding(projectNode))
bool needsRebuilding;
using (PerfTrace.Current.CaptureTiming($"{projectNode.ProjectContext.ProjectName()}", nameof(NeedsRebuilding)))
{
return RunCompile(projectNode);
needsRebuilding = NeedsRebuilding(projectNode);
}
if (needsRebuilding)
{
using (PerfTrace.Current.CaptureTiming($"{projectNode.ProjectContext.ProjectName()}",nameof(RunCompile)))
{
return RunCompile(projectNode);
}
}
else
{
ProjectSkiped(projectNode);
using (PerfTrace.Current.CaptureTiming($"{projectNode.ProjectContext.ProjectName()}", nameof(ProjectSkiped)))
{
ProjectSkiped(projectNode);
}
return CompilationResult.IncrementalSkip;
}
}

View file

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ProjectModel;
using System;
using System.Collections.Generic;
@ -20,7 +21,16 @@ namespace Microsoft.DotNet.Tools.Build
IsRoot = isRoot;
}
public ProjectContext ProjectContext { get { return _projectContextCreator.GetAwaiter().GetResult(); } }
public ProjectContext ProjectContext
{
get
{
using (_projectContextCreator.IsCompleted ? null : PerfTrace.Current.CaptureTiming("", "Blocking ProjectContext wait"))
{
return _projectContextCreator.GetAwaiter().GetResult();
}
}
}
public IReadOnlyList<ProjectGraphNode> Dependencies { get; }

View file

@ -14,7 +14,10 @@ namespace Microsoft.DotNet.Tests
{
public class MockTelemetry : ITelemetry
{
public string EventName{get;set;}
public bool Enabled { get; set; }
public string EventName { get; set; }
public void TrackEvent(string eventName, IDictionary<string, string> properties, IDictionary<string, double> measurements)
{
EventName = eventName;