Skip to content

Commit

Permalink
Merge pull request #502 from adamsitnik/flameGraph
Browse files Browse the repository at this point in the history
Flame Graph
  • Loading branch information
vancem committed Jan 8, 2018
2 parents 36de508 + a4fdefc commit f1731b1
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 8 deletions.
2 changes: 1 addition & 1 deletion src/Directory.Build.props
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
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
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
@@ -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
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
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

0 comments on commit f1731b1

Please sign in to comment.