Skip to content

Commit 991618f

Browse files
authored
Add "Open in Browser" option for HTML reports in the TreeView (microsoft#1156)
* Add a new option in the right-click menu of the TreeView to open any TreeViewItems that are HTML reports in the machine's external browser. To do this I had to fix a second issue - when you right-click an item in the tree view it is not automatically selected before you choose an item in the right-click menu. This meant that you could not reliably use TreeView.SelectedItem in the callback of the command that was chosen in the right-click menu. I fixed this by hooking in to the right-button-down event and navigating the visual tree to find the TreeViewItem related to the visual that was clicked. * Clean up comments
1 parent bc86cdc commit 991618f

File tree

3 files changed

+169
-84
lines changed

3 files changed

+169
-84
lines changed

src/PerfView/MainWindow.xaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
</Window.Resources>
1818
<Window.CommandBindings>
1919
<CommandBinding Command="{x:Static src:MainWindow.ItemHelpCommand}" Executed="DoItemHelp" CanExecute="CanDoItemHelp"/>
20+
<CommandBinding Command="{x:Static src:MainWindow.OpenInBrowserCommand}" Executed="OpenInBrowser" CanExecute="CanOpenInBrowser"/>
2021
<CommandBinding Command="{x:Static src:MainWindow.RunCommand}" Executed="DoRun"/>
2122
<CommandBinding Command="{x:Static src:MainWindow.CollectCommand}" Executed="DoCollect"/>
2223
<CommandBinding Command="{x:Static src:MainWindow.AbortCommand}" Executed="DoAbort"/>
@@ -159,7 +160,7 @@
159160
</TextBlock>
160161
<TextBox Name="FileFilterTextBox" TextChanged="FilterTextChanged" KeyDown="FilterKeyDown" Grid.Column="1" Margin="5,2,2,2" />
161162
</Grid>
162-
<TreeView x:Name="TreeView" Grid.Column="0" MouseDoubleClick="DoMouseDoubleClickInTreeView" KeyDown="KeyDownInTreeView" SelectedItemChanged="SelectedItemChangedInTreeView"
163+
<TreeView x:Name="TreeView" Grid.Column="0" MouseDoubleClick="DoMouseDoubleClickInTreeView" KeyDown="KeyDownInTreeView" SelectedItemChanged="SelectedItemChangedInTreeView" PreviewMouseRightButtonDown="TreeView_PreviewMouseRightButtonDown"
163164
ToolTip="Double Click to open. Right click contains additional help and additional options."
164165
IsEnabled="{Binding ElementName=StatusBar, Path=IsNotWorking}" AllowDrop="True" Drop="DoDrop">
165166
<TreeView.ItemContainerStyle>
@@ -174,6 +175,7 @@
174175
<StackPanel.ContextMenu>
175176
<ContextMenu>
176177
<MenuItem Command="{x:Static src:MainWindow.ItemHelpCommand}"/>
178+
<MenuItem Command="{x:Static src:MainWindow.OpenInBrowserCommand}"/>
177179
<MenuItem Command="{x:Static src:MainWindow.RefreshDirCommand}"/>
178180
<MenuItem Command="{x:Static src:MainWindow.OpenCommand}"/>
179181
<MenuItem Command="{x:Static src:MainWindow.CloseCommand}"/>

src/PerfView/MainWindow.xaml.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using System.Windows;
1616
using System.Windows.Controls;
1717
using System.Windows.Input;
18+
using System.Windows.Media;
1819
using System.Windows.Navigation;
1920
using Utilities;
2021

@@ -1721,6 +1722,26 @@ private void DoItemHelp(object sender, ExecutedRoutedEventArgs e)
17211722
StatusBar.Log("Looking up topic " + anchor + " in Users Guide.");
17221723
DisplayUsersGuide(anchor);
17231724
}
1725+
1726+
private void OpenInBrowser(object sender, ExecutedRoutedEventArgs e)
1727+
{
1728+
var selectedReport = TreeView.SelectedItem as PerfViewHtmlReport;
1729+
if (selectedReport == null)
1730+
{
1731+
throw new ApplicationException("No report selected.");
1732+
}
1733+
1734+
selectedReport.OpenInExternalBrowser(StatusBar);
1735+
}
1736+
1737+
private void CanOpenInBrowser(object sender, CanExecuteRoutedEventArgs e)
1738+
{
1739+
if (TreeView.SelectedItem is PerfViewHtmlReport)
1740+
{
1741+
e.CanExecute = true;
1742+
}
1743+
}
1744+
17241745
private void DoRefreshDir(object sender, ExecutedRoutedEventArgs e)
17251746
{
17261747
RefreshCurrentDirectory();
@@ -1969,6 +1990,7 @@ private void Window_Closed(object sender, EventArgs e)
19691990
public static RoutedUICommand ZipCommand = new RoutedUICommand("Zip", "Zip", typeof(MainWindow));
19701991
public static RoutedUICommand UnZipCommand = new RoutedUICommand("UnZip", "UnZip", typeof(MainWindow));
19711992
public static RoutedUICommand ItemHelpCommand = new RoutedUICommand("Help on Item", "ItemHelp", typeof(MainWindow));
1993+
public static RoutedUICommand OpenInBrowserCommand = new RoutedUICommand("Open in Browser", "OpenInBrowser", typeof(MainWindow));
19721994
public static RoutedUICommand HideCommand = new RoutedUICommand("Hide", "Hide", typeof(MainWindow),
19731995
new InputGestureCollection() { new KeyGesture(Key.H, ModifierKeys.Alt) });
19741996
public static RoutedUICommand UserCommand = new RoutedUICommand("User Command", "UserCommand", typeof(MainWindow),
@@ -2219,5 +2241,33 @@ public void OpenNext(string fileName)
22192241
}
22202242

22212243
private string m_openNextFileName;
2244+
2245+
/// <summary>
2246+
/// When you right click an item in the TreeView it doesn't automatically change to the TreeViewItem you clicked on.
2247+
/// This helper method changes focus so that the right-click menu items commands are bound to the right TreeViewItem
2248+
/// </summary>
2249+
private void TreeView_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
2250+
{
2251+
TreeViewItem treeViewItem = FindTreeViewItemInVisualHeirarchy(e.OriginalSource as DependencyObject);
2252+
2253+
if (treeViewItem != null)
2254+
{
2255+
treeViewItem.Focus();
2256+
e.Handled = true;
2257+
}
2258+
}
2259+
2260+
/// <summary>
2261+
/// Given an item in visual a tree, navigate the parents upwards until we find the TreeViewItem it represents.
2262+
/// </summary>
2263+
private static TreeViewItem FindTreeViewItemInVisualHeirarchy(DependencyObject source)
2264+
{
2265+
while (source != null && !(source is TreeViewItem))
2266+
{
2267+
source = VisualTreeHelper.GetParent(source);
2268+
}
2269+
2270+
return source as TreeViewItem;
2271+
}
22222272
}
22232273
}

src/PerfView/PerfViewData.cs

Lines changed: 116 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,7 @@ private IProcessForStackSource GetProcessFromStack(StackSourceCallStackIndex cal
894894
if (caller == StackSourceCallStackIndex.Invalid)
895895
{
896896
string topCallStackStr = stackSource.GetFrameName(stackSource.GetFrameIndex(callStack), true);
897-
897+
898898
if (GetProcessForStackSourceFromTopCallStackFrame(topCallStackStr, out ret))
899899
{
900900
processes.Add(ret);
@@ -1063,80 +1063,11 @@ public override void Open(Window parentWindow, StatusBar worker, Action doAfter)
10631063
{
10641064
if (Viewer == null)
10651065
{
1066-
var etlDataFile = DataFile as ETLPerfViewData;
1067-
TraceLog trace = null;
1068-
if (etlDataFile != null)
1069-
{
1070-
trace = etlDataFile.GetTraceLog(worker.LogWriter);
1071-
}
1072-
else
1073-
{
1074-
var linuxDataFile = DataFile as LinuxPerfViewData;
1075-
if (linuxDataFile != null)
1076-
{
1077-
trace = linuxDataFile.GetTraceLog(worker.LogWriter);
1078-
}
1079-
else
1080-
{
1081-
var eventPipeDataFile = DataFile as EventPipePerfViewData;
1082-
if (eventPipeDataFile != null)
1083-
{
1084-
trace = eventPipeDataFile.GetTraceLog(worker.LogWriter);
1085-
}
1086-
}
1087-
}
1066+
TraceLog trace = GetTrace(worker);
10881067

10891068
worker.StartWork("Opening " + Name, delegate ()
10901069
{
1091-
var reportFileName = CacheFiles.FindFile(FilePath, "." + Name + ".html");
1092-
using (var writer = File.CreateText(reportFileName))
1093-
{
1094-
writer.WriteLine("<html>");
1095-
writer.WriteLine("<head>");
1096-
writer.WriteLine("<title>{0}</title>", Title);
1097-
writer.WriteLine("<meta charset=\"UTF-8\"/>");
1098-
writer.WriteLine("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>");
1099-
1100-
// Add basic styling to the generated HTML
1101-
writer.WriteLine(@"
1102-
<style>
1103-
body {
1104-
font-family: Segoe UI Light, Helvetica, sans-serif;
1105-
}
1106-
1107-
tr:hover {
1108-
background-color: #eeeeee;
1109-
}
1110-
1111-
th {
1112-
background-color: #eeeeee;
1113-
font-family: Helvetica;
1114-
padding: 4px;
1115-
font-size: small;
1116-
font-weight: normal;
1117-
}
1118-
1119-
td {
1120-
font-family: Consolas, monospace;
1121-
font-size: small;
1122-
padding: 3px;
1123-
padding-bottom: 5px;
1124-
}
1125-
1126-
table {
1127-
border-collapse: collapse;
1128-
}
1129-
</style>
1130-
");
1131-
1132-
writer.WriteLine("</head>");
1133-
writer.WriteLine("<body>");
1134-
WriteHtmlBody(trace, writer, reportFileName, worker.LogWriter);
1135-
writer.WriteLine("</body>");
1136-
writer.WriteLine("</html>");
1137-
1138-
1139-
}
1070+
string reportFileName = GenerateReportFile(worker, trace);
11401071

11411072
worker.EndWork(delegate ()
11421073
{
@@ -1185,8 +1116,110 @@ public override void Open(Window parentWindow, StatusBar worker, Action doAfter)
11851116
}
11861117
}
11871118

1119+
/// <summary>
1120+
/// Generates an HTML report and opens it using the machine's default handler .html file paths.
1121+
/// </summary>
1122+
/// <param name="worker">The StatusBar that should be updated with progress.</param>
1123+
public void OpenInExternalBrowser(StatusBar worker)
1124+
{
1125+
TraceLog trace = GetTrace(worker);
1126+
1127+
worker.StartWork("Opening in external browser " + Name, delegate ()
1128+
{
1129+
string reportFileName = GenerateReportFile(worker, trace);
1130+
1131+
worker.EndWork(delegate ()
1132+
{
1133+
Process.Start(reportFileName);
1134+
});
1135+
});
1136+
}
1137+
11881138
public override void Close() { }
11891139
public override ImageSource Icon { get { return GuiApp.MainWindow.Resources["HtmlReportBitmapImage"] as ImageSource; } }
1140+
1141+
private TraceLog GetTrace(StatusBar worker)
1142+
{
1143+
var etlDataFile = DataFile as ETLPerfViewData;
1144+
TraceLog trace = null;
1145+
if (etlDataFile != null)
1146+
{
1147+
trace = etlDataFile.GetTraceLog(worker.LogWriter);
1148+
}
1149+
else
1150+
{
1151+
var linuxDataFile = DataFile as LinuxPerfViewData;
1152+
if (linuxDataFile != null)
1153+
{
1154+
trace = linuxDataFile.GetTraceLog(worker.LogWriter);
1155+
}
1156+
else
1157+
{
1158+
var eventPipeDataFile = DataFile as EventPipePerfViewData;
1159+
if (eventPipeDataFile != null)
1160+
{
1161+
trace = eventPipeDataFile.GetTraceLog(worker.LogWriter);
1162+
}
1163+
}
1164+
}
1165+
1166+
return trace;
1167+
}
1168+
1169+
private string GenerateReportFile(StatusBar worker, TraceLog trace)
1170+
{
1171+
var reportFileName = CacheFiles.FindFile(FilePath, "." + Name + ".html");
1172+
using (var writer = File.CreateText(reportFileName))
1173+
{
1174+
writer.WriteLine("<html>");
1175+
writer.WriteLine("<head>");
1176+
writer.WriteLine("<title>{0}</title>", Title);
1177+
writer.WriteLine("<meta charset=\"UTF-8\"/>");
1178+
writer.WriteLine("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>");
1179+
1180+
// Add basic styling to the generated HTML
1181+
writer.WriteLine(@"
1182+
<style>
1183+
body {
1184+
font-family: Segoe UI Light, Helvetica, sans-serif;
1185+
}
1186+
1187+
tr:hover {
1188+
background-color: #eeeeee;
1189+
}
1190+
1191+
th {
1192+
background-color: #eeeeee;
1193+
font-family: Helvetica;
1194+
padding: 4px;
1195+
font-size: small;
1196+
font-weight: normal;
1197+
}
1198+
1199+
td {
1200+
font-family: Consolas, monospace;
1201+
font-size: small;
1202+
padding: 3px;
1203+
padding-bottom: 5px;
1204+
}
1205+
1206+
table {
1207+
border-collapse: collapse;
1208+
}
1209+
</style>
1210+
");
1211+
1212+
writer.WriteLine("</head>");
1213+
writer.WriteLine("<body>");
1214+
WriteHtmlBody(trace, writer, reportFileName, worker.LogWriter);
1215+
writer.WriteLine("</body>");
1216+
writer.WriteLine("</html>");
1217+
1218+
1219+
}
1220+
1221+
return reportFileName;
1222+
}
11901223
}
11911224

11921225
public class PerfViewTraceInfo : PerfViewHtmlReport
@@ -6705,7 +6738,7 @@ protected internal override void ConfigureStackWindow(string stackSourceName, St
67056738
{
67066739
if (App.ConfigData["WarnedAboutOsHeapAllocTypes"] == null)
67076740
{
6708-
MessageBox.Show(stackWindow,
6741+
MessageBox.Show(stackWindow,
67096742
"Warning: Allocation type resolution only happens on window launch.\r\n" +
67106743
"Thus if you manually lookup symbols in this view you will get method\r\n" +
67116744
"names of allocations sites, but to get the type name associated the \r\n" +
@@ -8766,18 +8799,18 @@ protected internal override StackSource OpenStackSourceImpl(string streamName, T
87668799
{
87678800
sample.Metric = objInfo.RepresentativeSize;
87688801
sample.Count = objInfo.RepresentativeSize / objInfo.Size; // We guess a count from the size.
8769-
sample.TimeRelativeMSec = objInfo.AllocationTimeRelativeMSec;
8802+
sample.TimeRelativeMSec = objInfo.AllocationTimeRelativeMSec;
87708803
sample.StackIndex = stackSource.Interner.CallStackIntern(objInfo.ClassFrame, objInfo.AllocStack); // Add the type as a pseudo frame.
8771-
stackSource.AddSample(sample);
8804+
stackSource.AddSample(sample);
87728805
return true;
87738806
};
87748807
newHeap.OnObjectDestroy += delegate (double time, int gen, Address objAddress, GCHeapSimulatorObject objInfo)
87758808
{
87768809
sample.Metric = -objInfo.RepresentativeSize;
87778810
sample.Count = -(objInfo.RepresentativeSize / objInfo.Size); // We guess a count from the size.
8778-
sample.TimeRelativeMSec = time;
8811+
sample.TimeRelativeMSec = time;
87798812
sample.StackIndex = stackSource.Interner.CallStackIntern(objInfo.ClassFrame, objInfo.AllocStack); // We remove the same stack we added at alloc.
8780-
stackSource.AddSample(sample);
8813+
stackSource.AddSample(sample);
87818814
};
87828815

87838816
newHeap.OnGC += delegate (double time, int gen)
@@ -8812,7 +8845,7 @@ protected internal override StackSource OpenStackSourceImpl(string streamName, T
88128845
{
88138846
sample.Metric = objInfo.RepresentativeSize;
88148847
sample.Count = (objInfo.RepresentativeSize / objInfo.Size); // We guess a count from the size.
8815-
sample.TimeRelativeMSec = objInfo.AllocationTimeRelativeMSec;
8848+
sample.TimeRelativeMSec = objInfo.AllocationTimeRelativeMSec;
88168849
sample.StackIndex = stackSource.Interner.CallStackIntern(objInfo.ClassFrame, objInfo.AllocStack);
88178850
stackSource.AddSample(sample);
88188851
}
@@ -8848,12 +8881,12 @@ protected internal override StackSource OpenStackSourceImpl(string streamName, T
88488881
var typeName = data.TypeName;
88498882
if (string.IsNullOrEmpty(typeName))
88508883
{
8851-
// Attempt to resolve the type name.
8852-
TraceLoadedModule module = data.Process().LoadedModules.GetModuleContainingAddress(data.TypeID, data.TimeStampRelativeMSec);
8884+
// Attempt to resolve the type name.
8885+
TraceLoadedModule module = data.Process().LoadedModules.GetModuleContainingAddress(data.TypeID, data.TimeStampRelativeMSec);
88538886
if (module != null)
88548887
{
8855-
// Resolve the type name.
8856-
typeName = typeNameSymbolResolver.ResolveTypeName((int)(data.TypeID - module.ModuleFile.ImageBase), module.ModuleFile, TypeNameSymbolResolver.TypeNameOptions.StripModuleName);
8888+
// Resolve the type name.
8889+
typeName = typeNameSymbolResolver.ResolveTypeName((int)(data.TypeID - module.ModuleFile.ImageBase), module.ModuleFile, TypeNameSymbolResolver.TypeNameOptions.StripModuleName);
88578890
}
88588891
}
88598892

@@ -8912,7 +8945,7 @@ protected internal override void ConfigureStackWindow(string stackSourceName, St
89128945
stackWindow.ComputeMaxInTopStats = true;
89138946
}
89148947

8915-
if(m_extraTopStats != null)
8948+
if (m_extraTopStats != null)
89168949
{
89178950
stackWindow.ExtraTopStats += " " + m_extraTopStats;
89188951
}

0 commit comments

Comments
 (0)