Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flame Graph #502

Merged
merged 3 commits into from
Jan 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</PropertyGroup>

<PropertyGroup>
<PerfViewVersion>2.0.0.0</PerfViewVersion>
<PerfViewVersion>2.0.1.0</PerfViewVersion>
</PropertyGroup>

<!-- versions of dependencies that more than one project use -->
Expand Down
2 changes: 1 addition & 1 deletion src/PerfView.Tests/StackViewer/StackWindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public Task TestIncludeItemOnByNameTabAsync()
return TestIncludeItemAsync(KnownDataGrid.ByName);
}

[WpfFact]
[WpfFact(Skip = "Failing with indexOutOfRange and Debug testing. See issue https://github.com/Microsoft/perfview/issues/354")]
[WorkItem(316, "https://github.com/Microsoft/perfview/issues/316")]
public Task TestIncludeItemOnCallerCalleeTabCallerAsync()
{
Expand Down
5 changes: 5 additions & 0 deletions src/PerfView/PerfView.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,11 @@
<WithCulture>false</WithCulture>
<LogicalName>.\Images\ThreadTimeWithStartStop.png</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="SupportFiles\Images\FlameGraphView.png">
<Type>Non-Resx</Type>
<WithCulture>false</WithCulture>
<LogicalName>.\Images\FlameGraphView.png</LogicalName>
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
Expand Down
160 changes: 160 additions & 0 deletions src/PerfView/StackViewer/FlameGraph.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Microsoft.Diagnostics.Tracing.Stacks;

namespace PerfView
{
public static class FlameGraph
{
private static readonly FontFamily FontFamily = new FontFamily("Consolas");
private static readonly Brush[] Brushes = GenerateBrushes(new Random(12345));

/// <summary>
/// (X=0, Y=0) is the left bottom corner of the canvas
/// </summary>
public struct FlameBox
{
public readonly double Width, Height, X, Y;
public readonly CallTreeNode Node;

public FlameBox(CallTreeNode node, double width, double height, double x, double y)
{
Node = node;
Width = width;
Height = height;
X = x;
Y = y;
}
}

private struct FlamePair
{
public readonly FlameBox ParentBox;
public readonly CallTreeNode Node;

public FlamePair(FlameBox parentBox, CallTreeNode node)
{
ParentBox = parentBox;
Node = node;
}
}

public static IEnumerable<FlameBox> Calculate(CallTree callTree, double maxWidth, double maxHeight)
{
double maxDepth = GetMaxDepth(callTree.Root);
double boxHeight = maxHeight / maxDepth;
double pixelsPerIncusiveSample = maxWidth / callTree.Root.InclusiveMetric;

var rootBox = new FlameBox(callTree.Root, maxWidth, boxHeight, 0, 0);
yield return rootBox;

var nodesToVisit = new Queue<FlamePair>();
nodesToVisit.Enqueue(new FlamePair(rootBox, callTree.Root));

while (nodesToVisit.Count > 0)
{
var current = nodesToVisit.Dequeue();
var parentBox = current.ParentBox;
var currentNode = current.Node;

double nextBoxX = (parentBox.Width - (currentNode.Callees.Sum(child => child.InclusiveMetric) * pixelsPerIncusiveSample)) / 2.0; // centering the starting point

foreach (var child in currentNode.Callees)
{
double childBoxWidth = child.InclusiveMetric * pixelsPerIncusiveSample;
var childBox = new FlameBox(child, childBoxWidth, boxHeight, parentBox.X + nextBoxX, parentBox.Y + boxHeight);
nextBoxX += childBoxWidth;

if (child.Callees != null)
nodesToVisit.Enqueue(new FlamePair(childBox, child));

yield return childBox;
}
}
}

public static void Draw(IEnumerable<FlameBox> boxes, Canvas canvas)
{
canvas.Children.Clear();

int index = 0;
foreach (var box in boxes)
{
FrameworkElement rectangle = CreateRectangle(box, ++index);

Canvas.SetLeft(rectangle, box.X);
Canvas.SetBottom(rectangle, box.Y);
canvas.Children.Add(rectangle);
}
}

public static void Export(Canvas flameGraphCanvas, string filePath)
{
var rectangle = new Rect(flameGraphCanvas.RenderSize);
var renderTargetBitmap = new RenderTargetBitmap((int)rectangle.Right, (int)rectangle.Bottom, 96d, 96d, PixelFormats.Default);
renderTargetBitmap.Render(flameGraphCanvas);

var pngEncoder = new PngBitmapEncoder();
pngEncoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap));

using (var file = System.IO.File.Create(filePath))
pngEncoder.Save(file);
}

private static Brush[] GenerateBrushes(Random random)
=> Enumerable.Range(0, 100)
.Select(_ => (Brush)new SolidColorBrush(
Color.FromRgb(
(byte)(205.0 + 50.0 * random.NextDouble()),
(byte)(230.0 * random.NextDouble()),
(byte)(55.0 * random.NextDouble()))))
.ToArray();

private static double GetMaxDepth(CallTreeNode callTree)
{
double deepest = 0;

if (callTree.Callees != null)
foreach (var callee in callTree.Callees)
deepest = Math.Max(deepest, GetMaxDepth(callee));

return deepest + 1;
}

private static FrameworkElement CreateRectangle(FlameBox box, int index)
{
var tooltip = $"Method: {box.Node.DisplayName} ({box.Node.InclusiveCount} inclusive samples, {box.Node.InclusiveMetricPercent:F}%)";
var background = Brushes[++index % Brushes.Length]; // in the future, the color could be chosen according to the belonging of the method (JIT, GC, user code, OS etc)

// for small boxes we create Rectangles, because they are much faster (too many TextBlocks === bad perf)
// also for small rectangles it's impossible to read the name of the method anyway (only few characters are printed)
if (box.Width < 50)
return new Rectangle
{
Height = box.Height,
Width = box.Width,
Fill = background,
ToolTip = new ToolTip { Content = tooltip },
DataContext = box.Node
};

return new TextBlock
{
Height = box.Height,
Width = box.Width,
Background = background,
ToolTip = new ToolTip { Content = tooltip },
Text = box.Node.DisplayName,
DataContext = box.Node,
FontFamily = FontFamily,
FontSize = Math.Min(20.0, box.Height)
};
}
}
}
16 changes: 16 additions & 0 deletions src/PerfView/StackViewer/StackWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
<CommandBinding Command="{x:Static src:StackWindow.CancelCommand}" Executed="DoCancel"/>
<CommandBinding Command="{x:Static src:StackWindow.SaveCommand}" Executed="DoSave"/>
<CommandBinding Command="{x:Static src:StackWindow.SaveAsCommand}" Executed="DoSaveAs"/>
<CommandBinding Command="{x:Static src:StackWindow.SaveFlameGraphCommand}" Executed="DoSaveFlameGraph"/>
<CommandBinding Command="{x:Static src:StackWindow.UsersGuideCommand}" Executed="DoHyperlinkHelp"/>
<CommandBinding Command="Help" Executed="DoHyperlinkHelp" />
</DockPanel.CommandBindings>
Expand Down Expand Up @@ -465,6 +466,21 @@
<src:PerfDataGrid Grid.Row="1" x:Name="CalleesDataGrid" Margin="0,0,20,0" MouseDoubleClick="DataGrid_MouseDoubleClick"/>
</Grid>
</TabItem>
<!-- FlameGraphTab -->
<TabItem Name="FlameGraphTab" GotFocus="FlameGraphTab_GotFocus">
<TabItem.Header>
<TextBlock>
Flame Graph <Hyperlink Command="Help" CommandParameter="FlameGraphView">?</Hyperlink>
</TextBlock>
</TabItem.Header>
<Canvas Name="FlameGraphCanvas" Background="Transparent" SizeChanged="FlameGraphCanvas_SizeChanged" MouseMove="FlameGraphCanvas_MouseMove">
<Canvas.ContextMenu>
<ContextMenu Name="noContextMenu" Visibility="Visible">
<MenuItem Header="Save Flame Graph" Command="{x:Static src:StackWindow.SaveFlameGraphCommand}" />
</ContextMenu>
</Canvas.ContextMenu>
</Canvas>
</TabItem>
<!-- NotesTab -->
<TabItem Name="NotesTab" GotFocus="NotesTab_GotFocus" LostFocus="NotesTab_LostFocus">
<TabItem.Header>
Expand Down
85 changes: 79 additions & 6 deletions src/PerfView/StackViewer/StackWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
using System.Threading;
using PerfView.GuiUtilities;
using Utilities;
using Path = System.IO.Path;

namespace PerfView
{
Expand Down Expand Up @@ -411,6 +412,8 @@ public void SetStackSource(StackSource newSource, Action onComplete = null)
cumMax / 1000000, controller.GetStartTimeForBucket((HistogramCharacterIndex)cumMaxIdx));
}

RedrawFlameGraphIfVisible();

TopStats.Text = stats;

// TODO this is a bit of a hack, as it might replace other instances of the string.
Expand Down Expand Up @@ -2529,6 +2532,69 @@ private void NotesTab_LostFocus(object sender, RoutedEventArgs e)
m_NotesTabActive = false;
}

private bool m_RedrawFlameGraphWhenItBecomesVisible = false;

private void FlameGraphTab_GotFocus(object sender, RoutedEventArgs e)
{
if (FlameGraphCanvas.Children.Count == 0 || m_RedrawFlameGraphWhenItBecomesVisible)
RedrawFlameGraph();
}

private void FlameGraphCanvas_SizeChanged(object sender, SizeChangedEventArgs e) => RedrawFlameGraphIfVisible();

private void RedrawFlameGraphIfVisible()
{
if (FlameGraphTab.IsSelected)
RedrawFlameGraph();
else
m_RedrawFlameGraphWhenItBecomesVisible = true;
}

private void RedrawFlameGraph()
{
FlameGraph.Draw(
CallTree.Root.HasChildren
? FlameGraph.Calculate(CallTree, FlameGraphCanvas.ActualWidth, FlameGraphCanvas.ActualHeight)
: Enumerable.Empty<FlameGraph.FlameBox>(),
FlameGraphCanvas);

m_RedrawFlameGraphWhenItBecomesVisible = false;
}

private void FlameGraphCanvas_MouseMove(object sender, MouseEventArgs e)
{
if (StatusBar.LoggedError || FlameGraphCanvas.Children.Count == 0)
return;

var pointed = FlameGraphCanvas.Children.OfType<FrameworkElement>().FirstOrDefault(box => box.IsMouseOver);
var toolTip = pointed?.ToolTip as ToolTip;
if (toolTip != null)
StatusBar.Status = toolTip.Content as string;
}

private void DoSaveFlameGraph(object sender, RoutedEventArgs e)
{
var saveDialog = new Microsoft.Win32.SaveFileDialog();
var baseName = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(DataSource.FilePath));

for (int i = 1; ; i++)
{
saveDialog.FileName = baseName + ".flameGraph" + i.ToString() + ".png";
if (!File.Exists(saveDialog.FileName))
break;
}
saveDialog.InitialDirectory = Path.GetDirectoryName(DataSource.FilePath);
saveDialog.Title = "File to save flame graph";
saveDialog.DefaultExt = ".png";
saveDialog.Filter = "Image files (*.png)|*.png|All files (*.*)|*.*";
saveDialog.AddExtension = true;
saveDialog.OverwritePrompt = true;

var result = saveDialog.ShowDialog();
if (result == true)
FlameGraph.Export(FlameGraphCanvas, saveDialog.FileName);
}

private TabItem SelectedTab
{
get
Expand All @@ -2543,8 +2609,11 @@ private TabItem SelectedTab
return CallersTab;
else if (CalleesTab.IsSelected)
return CalleesTab;
else if (FlameGraphTab.IsSelected)
return FlameGraphTab;
else if (NotesTab.IsSelected)
return NotesTab;

Debug.Assert(false, "No tab selected!");
return null;
}
Expand Down Expand Up @@ -2601,22 +2670,25 @@ public StackWindowGuiState GuiState
{
switch (value.TabSelected)
{
case "ByNameTab":
case nameof(ByNameTab):
ByNameTab.IsSelected = true;
break;
case "CallerCalleeTab":
case nameof(CallerCalleeTab):
CallerCalleeTab.IsSelected = true;
break;
case "CallTreeTab":
case nameof(CallTreeTab):
CallTreeTab.IsSelected = true;
break;
case "CalleesTab":
case nameof(CalleesTab):
CalleesTab.IsSelected = true;
break;
case "CallersTab":
case nameof(CallersTab):
CallersTab.IsSelected = true;
break;
case "NotesTab":
case nameof(FlameGraphTab):
FlameGraphTab.IsSelected = true;
break;
case nameof(NotesTab):
NotesTab.IsSelected = true;
break;
}
Expand Down Expand Up @@ -2656,6 +2728,7 @@ public StackWindowGuiState GuiState
public static RoutedUICommand SaveCommand = new RoutedUICommand("Save", "Save", typeof(StackWindow),
new InputGestureCollection() { new KeyGesture(Key.S, ModifierKeys.Control) });
public static RoutedUICommand SaveAsCommand = new RoutedUICommand("SaveAs", "SaveAs", typeof(StackWindow));
public static RoutedUICommand SaveFlameGraphCommand = new RoutedUICommand("SaveFlameGraph", "SaveFlameGraph", typeof(StackWindow));
public static RoutedUICommand CancelCommand = new RoutedUICommand("Cancel", "Cancel", typeof(StackWindow),
new InputGestureCollection() { new KeyGesture(Key.Escape) });
public static RoutedUICommand UpdateCommand = new RoutedUICommand("Update", "Update", typeof(StackWindow),
Expand Down
Loading