Skip to content

Commit a66bff0

Browse files
paulusakademiuscaaavik-msftbrianrob
authoredMar 14, 2025··
Add multi line viewer for Event Window (#2175)
* Add multi line viewer for Event Window This will add a multi line viewer to the Event Viewer, which can be turned on and off with F2 * Address review comments. -Change m_payloads from Array to List -Add other fields from conditional calls to AddField -Add "Press f2 to hide/unhide" text -Set default size of multi line viewer to 25% -Fix scroll behaviour * Update src/PerfView/EventViewer/EventWindow.xaml Co-authored-by: Cameron Aavik <[email protected]> * Update src/PerfView/EventViewer/EventWindow.xaml.cs Removing unnecessary INullOrEmpty() call. Co-authored-by: Brian Robbins <[email protected]> --------- Co-authored-by: Cameron Aavik <[email protected]> Co-authored-by: Brian Robbins <[email protected]>
1 parent b0c027a commit a66bff0

File tree

5 files changed

+124
-29
lines changed

5 files changed

+124
-29
lines changed
 

‎src/CSVReader/CsvReader.cs

+1
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ internal class CsvEventRecord : EventRecord
525525
public override double TimeStampRelatveMSec { get { return m_TimeStampRelativeMSec; } }
526526
public override string ProcessName { get { return m_ProcessName; } }
527527
public override string Rest { get { return m_Data; } set { } }
528+
public override List<Payload> Payloads => throw new NotImplementedException();
528529
#region private
529530
internal CsvEventRecord(ByteWindow window, CsvEventSource source, string[] colNames, Dictionary<string, int> columnOrder, double[] columnSums) : base(4)
530531
{

‎src/CSVReader/EventSource.cs

+21-2
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,24 @@ public static List<string> ParseColumns(string columnSpec, ICollection<string> c
144144
}
145145
}
146146

147+
/// <summary>
148+
/// A Payload object represents the Rest string from EventRecord as Object to make working
149+
/// with the values inside easier
150+
/// </summary>
151+
public class Payload
152+
{
153+
public Payload(string payloadName, string payload)
154+
{
155+
m_payloadName = payloadName;
156+
m_payload = payload;
157+
}
158+
159+
public string PayloadName { get { return m_payloadName; } set { } }
160+
public string PayloadValue { get { return m_payload; } set { } }
161+
162+
private string m_payloadName;
163+
private string m_payload;
164+
}
147165
/// <summary>
148166
/// An EventRecord is a abstraction that is returned by the EventSource.Events API. It represents
149167
/// a single event and is everything the GUI needs to display the event in the GUI.
@@ -153,7 +171,8 @@ public abstract class EventRecord
153171
public abstract string EventName { get; }
154172
public abstract string ProcessName { get; }
155173
public abstract double TimeStampRelatveMSec { get; }
156-
174+
public abstract List<Payload> Payloads { get; }
175+
157176
// TODO FIX NOW should be abstract, get CSV and ETW subclasses to implement
158177
/// <summary>
159178
/// The names of the fields in this record
@@ -186,7 +205,7 @@ public abstract class EventRecord
186205
public string DisplayField8 { get { return m_displayFields[7]; } set { } }
187206
public string DisplayField9 { get { return m_displayFields[8]; } set { } }
188207
public string DisplayField10 { get { return m_displayFields[9]; } set { } }
189-
208+
190209
// returns true of 'pattern' matches the display fields.
191210
public virtual bool Matches(Regex pattern)
192211
{

‎src/PerfView/EtwEventSource.cs

+28-20
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ public override void ForEach(Func<EventRecord, bool> callback)
319319
}
320320
}
321321
}
322-
322+
323323
ETWEventRecord eventRecord = null;
324324
if (textFilter != null)
325325
{
@@ -346,7 +346,8 @@ public override void ForEach(Func<EventRecord, bool> callback)
346346
eventRecord = emptyEventRecord;
347347
eventRecord.m_timeStampRelativeMSec = data.TimeStampRelativeMSec;
348348
}
349-
349+
350+
350351
if (eventRecord == null)
351352
{
352353
eventRecord = new ETWEventRecord(this, data, columnOrder, NonRestFields, durationMSec);
@@ -534,6 +535,7 @@ private Dictionary<TraceEventCounts, TraceEventCounts> GetEventCounts(List<strin
534535
private Dictionary<string, bool> m_selectedEvents; // set to true if the event is only present because it is a start for a stop.
535536
private bool m_selectedAllEvents; // This ensures that when a user selects all events he gets everything
536537

538+
537539
internal class ETWEventRecord : EventRecord
538540
{
539541
// Used as the null record (after MaxRet happens).
@@ -552,29 +554,30 @@ internal ETWEventRecord(ETWEventSource source, TraceEvent data, Dictionary<strin
552554

553555
m_timeStampRelativeMSec = data.TimeStampRelativeMSec;
554556
m_idx = data.EventIndex;
555-
557+
m_payloads = new List<Payload>();
558+
556559
// Compute the data column
557560
var restString = new StringBuilder();
558561

559562
// Deal with the special HasStack, ThreadID and ActivityID, DataLength fields;
560563
var hasStack = data.CallStackIndex() != CallStackIndex.Invalid;
561564
if (hasStack)
562565
{
563-
AddField("HasStack", hasStack.ToString(), columnOrder, restString);
566+
AddField("HasStack", hasStack.ToString(), columnOrder, restString, m_payloads);
564567
}
565568

566569
var asCSwitch = data as CSwitchTraceData;
567570
if (asCSwitch != null)
568571
{
569-
AddField("HasBlockingStack", (asCSwitch.BlockingStack() != CallStackIndex.Invalid).ToString(), columnOrder, restString);
572+
AddField("HasBlockingStack", (asCSwitch.BlockingStack() != CallStackIndex.Invalid).ToString(), columnOrder, restString, m_payloads);
570573
}
571574

572-
AddField("ThreadID", data.ThreadID.ToString("n0"), columnOrder, restString);
573-
AddField("ProcessorNumber", data.ProcessorNumber.ToString(), columnOrder, restString);
575+
AddField("ThreadID", data.ThreadID.ToString("n0"), columnOrder, restString, m_payloads);
576+
AddField("ProcessorNumber", data.ProcessorNumber.ToString(), columnOrder, restString, m_payloads);
574577

575578
if (0 < durationMSec)
576579
{
577-
AddField("DURATION_MSEC", durationMSec.ToString("n3"), columnOrder, restString);
580+
AddField("DURATION_MSEC", durationMSec.ToString("n3"), columnOrder, restString, m_payloads);
578581
}
579582

580583
var payloadNames = data.PayloadNames;
@@ -583,28 +586,28 @@ internal ETWEventRecord(ETWEventSource source, TraceEvent data, Dictionary<strin
583586
// WPP events look classic and use the EventID as their discriminator
584587
if (data.IsClassicProvider && data.ID != 0)
585588
{
586-
AddField("EventID", ((int)data.ID).ToString(), columnOrder, restString);
589+
AddField("EventID", ((int)data.ID).ToString(), columnOrder, restString, m_payloads);
587590
}
588591

589-
AddField("DataLength", data.EventDataLength.ToString(), columnOrder, restString);
592+
AddField("DataLength", data.EventDataLength.ToString(), columnOrder, restString, m_payloads);
590593
}
591594

592595
try
593596
{
594597
for (int i = 0; i < payloadNames.Length; i++)
595598
{
596-
AddField(payloadNames[i], data.PayloadString(i), columnOrder, restString);
599+
AddField(payloadNames[i], data.PayloadString(i), columnOrder, restString, m_payloads);
597600
}
598601
}
599602
catch (Exception e)
600603
{
601-
AddField("ErrorParsingFields", e.Message, columnOrder, restString);
604+
AddField("ErrorParsingFields", e.Message, columnOrder, restString, m_payloads);
602605
}
603606

604607
var message = data.FormattedMessage;
605608
if (message != null)
606609
{
607-
AddField("FormattedMessage", message, columnOrder, restString);
610+
AddField("FormattedMessage", message, columnOrder, restString, m_payloads);
608611
}
609612

610613
if (source.m_needsComputers)
@@ -621,7 +624,7 @@ internal ETWEventRecord(ETWEventSource source, TraceEvent data, Dictionary<strin
621624
id = "^" + id; // Indicates it is at the start of the task.
622625
}
623626

624-
AddField("ActivityInfo", id, columnOrder, restString);
627+
AddField("ActivityInfo", id, columnOrder, restString, m_payloads );
625628
}
626629

627630
var startStopActivity = source.m_startStopActivityComputer.GetCurrentStartStopActivity(thread, data);
@@ -634,26 +637,26 @@ internal ETWEventRecord(ETWEventSource source, TraceEvent data, Dictionary<strin
634637
parentName = startStopActivity.Creator.Name;
635638
}
636639

637-
AddField("StartStopActivity", name + "/P=" + parentName, columnOrder, restString);
640+
AddField("StartStopActivity", name + "/P=" + parentName, columnOrder, restString, m_payloads);
638641
}
639642
}
640643
}
641644

642645
// We pass 0 as the process ID for creating the activityID because we want uniform syntax.
643646
if (data.ActivityID != Guid.Empty)
644647
{
645-
AddField("ActivityID", StartStopActivityComputer.ActivityPathString(data.ActivityID), columnOrder, restString);
648+
AddField("ActivityID", StartStopActivityComputer.ActivityPathString(data.ActivityID), columnOrder, restString, m_payloads);
646649
}
647650

648651
Guid relatedActivityID = data.RelatedActivityID;
649652
if (relatedActivityID != Guid.Empty)
650653
{
651-
AddField("RelatedActivityID", StartStopActivityComputer.ActivityPathString(data.RelatedActivityID), columnOrder, restString);
654+
AddField("RelatedActivityID", StartStopActivityComputer.ActivityPathString(data.RelatedActivityID), columnOrder, restString, m_payloads);
652655
}
653656

654657
if(data.ContainerID != null)
655658
{
656-
AddField("ContainerID", data.ContainerID, columnOrder, restString);
659+
AddField("ContainerID", data.ContainerID, columnOrder, restString, m_payloads);
657660
}
658661

659662
m_asText = restString.ToString();
@@ -666,7 +669,8 @@ internal ETWEventRecord(ETWEventSource source, TraceEvent data, Dictionary<strin
666669
public DateTime OriginTimeStamp { get { return TimeZoneInfo.ConvertTime(LocalTimeStamp, this.m_source.OriginTimeZone); } }
667670
public override string Rest { get { return m_asText; } set { } }
668671
public EventIndex Index { get { return m_idx; } }
669-
672+
public override List<Payload> Payloads { get { return m_payloads; } }
673+
670674
#region private
671675

672676
private static readonly Regex specialCharRemover = new Regex(" *[\r\n\t]+ *", RegexOptions.Compiled);
@@ -675,12 +679,15 @@ internal ETWEventRecord(ETWEventSource source, TraceEvent data, Dictionary<strin
675679
/// Adds 'fieldName' with value 'fieldValue' to the output. It either goes into a column (based on columnOrder) or it goes into
676680
/// 'rest' as a fieldName="fieldValue" string. It also updates 'columnSums' for the fieldValue for any in a true column
677681
/// </summary>
678-
private void AddField(string fieldName, string fieldValue, Dictionary<string, int> columnOrder, StringBuilder restString)
682+
private void AddField(string fieldName, string fieldValue, Dictionary<string, int> columnOrder, StringBuilder restString, List<Payload> payloadsList)
679683
{
680684
if (fieldValue == null)
681685
{
682686
fieldValue = "";
683687
}
688+
689+
payloadsList.Add(new Payload(fieldName, fieldValue));
690+
684691
// If the field value has to many newlines in it, the GUI gets confused because the text block is larger than
685692
// the vertical size. WPF may fix this at some point, but in the mean time this is a work around.
686693
fieldValue = specialCharRemover.Replace(fieldValue, " ");
@@ -787,6 +794,7 @@ public override bool Matches(Regex textRegex)
787794
private string m_asText;
788795
private EventIndex m_idx;
789796
private ETWEventSource m_source; // Lets you get at source information
797+
private List<Payload> m_payloads;
790798
#endregion
791799
}
792800

‎src/PerfView/EventViewer/EventWindow.xaml

+21-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
<Style x:Key="CenterAlign">
1515
<Setter Property="TextBlock.TextAlignment" Value="Center" />
1616
</Style>
17+
<WPFToolKit:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
1718
</Window.Resources>
1819
<DockPanel Background="{DynamicResource BackgroundColour}">
1920
<DockPanel.CommandBindings>
21+
<CommandBinding Command="{x:Static src:EventWindow.ToggleMultiLineViewPaneCommand}" Executed="DoToggleMultiLineViewPane" />
2022
<CommandBinding Command="{x:Static src:EventWindow.UsersGuideCommand}" Executed="DoHyperlinkHelp"/>
2123
<CommandBinding Command="Help" Executed="DoHyperlinkHelp" />
2224
<CommandBinding Command="{x:Static src:EventWindow.UpdateCommand}" Executed="DoUpdate"/>
@@ -85,7 +87,7 @@
8587
<MenuItem Header="_Show Local Time" IsCheckable="True" IsChecked="False" Checked="DoUseLocalTime" Unchecked="DoUseOriginTime"/>
8688
</MenuItem>
8789
<MenuItem Header="_Help">
88-
<MenuItem Header="_Help on Event Viewer" Command="Help" CommandParameter="EventViewerQuickStart" />
90+
<MenuItem Header="_Help on Event Viewer" Command="Help" CommandParameter="EventViewerQuickStart" />
8991
<MenuItem Header="_Users Guide" Command="{x:Static src:EventWindow.UsersGuideCommand}" CommandParameter="UsersGuide"/>
9092
</MenuItem>
9193
</Menu>
@@ -219,6 +221,8 @@
219221
<Grid.RowDefinitions>
220222
<RowDefinition Height="auto"/>
221223
<RowDefinition Height="*"/>
224+
<RowDefinition Height="auto"/>
225+
<RowDefinition Name="MultiLineViewPaneRowDef" Height="0.25*"/>
222226
</Grid.RowDefinitions>
223227
<Grid Grid.Row="0">
224228
<Grid.ColumnDefinitions>
@@ -333,6 +337,22 @@
333337
Width="Auto" />
334338
</WPFToolKit:DataGrid.Columns>
335339
</WPFToolKit:DataGrid>
340+
<GridSplitter Grid.Row="2" Height="3" HorizontalAlignment="Stretch" VerticalAlignment="Center" />
341+
<WPFToolKit:DataGrid x:Name="MultiLineView" Grid.Row="3" AutoGenerateColumns="False" WPFToolKit:ScrollViewer.CanContentScroll="False" AlternatingRowBackground="{DynamicResource AlternateRowBackground}" AutomationProperties.Name="Multi-Line View" >
342+
<DataGrid.Columns>
343+
<DataGridTextColumn Header="Payload Name" Binding="{Binding PayloadName, Mode=OneWay}" Width="*"/>
344+
<DataGridTextColumn Header="Payload" Binding="{Binding PayloadValue, Mode=OneWay}" Width="3*" >
345+
<DataGridTextColumn.ElementStyle>
346+
<Style TargetType="TextBlock">
347+
<Setter Property="TextWrapping" Value="Wrap"/>
348+
<Setter Property="FontFamily" Value="Consolas" />
349+
</Style>
350+
</DataGridTextColumn.ElementStyle>
351+
</DataGridTextColumn>
352+
</DataGrid.Columns>
353+
</WPFToolKit:DataGrid>
354+
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="3" Text="Press F2 to hide/unhide"
355+
Visibility="{Binding Items.IsEmpty, Converter={StaticResource BooleanToVisibilityConverter}, ElementName=MultiLineView}"/>
336356
</Grid>
337357
</Grid>
338358
</DockPanel>

‎src/PerfView/EventViewer/EventWindow.xaml.cs

+53-6
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
using Microsoft.Diagnostics.Tracing.Etlx;
44
using Microsoft.Diagnostics.Tracing.TraceUtilities.FilterQueryExpression;
55
using Microsoft.Diagnostics.Utilities;
6+
using Microsoft.IdentityModel.Tokens;
67
using System;
78
using System.Collections.Generic;
89
using System.Collections.ObjectModel;
910
using System.ComponentModel;
1011
using System.Diagnostics;
1112
using System.Globalization;
1213
using System.IO;
14+
using System.Linq;
1315
using System.Text;
1416
using System.Text.RegularExpressions;
1517
using System.Threading;
@@ -55,7 +57,6 @@ public EventWindow(EventWindow template)
5557
{
5658
selection.Add(item);
5759
}
58-
5960
Update();
6061
}
6162
public EventWindow(Window parent, PerfViewEventSource data)
@@ -164,6 +165,8 @@ public EventWindow(Window parent, PerfViewEventSource data)
164165
var lcv = (ListCollectionView)CollectionViewSource.GetDefaultView(Grid.ItemsSource);
165166
lcv.CustomSort = new LogicalGridDataComparer<EventRecord>(e.Column.SortMemberPath, direction);
166167
};
168+
169+
MultiLineViewPaneHidden = (App.UserConfigData["MultiLineViewPaneHidden"] == "true");
167170
}
168171

169172
public PerfViewEventSource DataSource { get; private set; }
@@ -237,6 +240,33 @@ public void SaveDataToCsvFile(string csvFileName, int maxNonRestFields = int.Max
237240
m_source.NonRestFields = savedNonRestFields;
238241
}
239242
}
243+
public bool MultiLineViewPaneHidden
244+
{
245+
get { return m_MultiLineViewPaneHidden; }
246+
set
247+
{
248+
if (value == m_MultiLineViewPaneHidden)
249+
{
250+
return;
251+
}
252+
253+
if (value)
254+
{
255+
App.UserConfigData["MultiLineViewPaneHidden"] = "true";
256+
m_MultiLineViewPaneHidden = true;
257+
MultiLineViewPaneRowDef.MaxHeight = 0;
258+
}
259+
else
260+
{
261+
App.UserConfigData["MultiLineViewPaneHidden"] = "false";
262+
m_MultiLineViewPaneHidden = false;
263+
MultiLineViewPaneRowDef.MaxHeight = Double.PositiveInfinity;
264+
265+
}
266+
}
267+
}
268+
269+
private bool m_MultiLineViewPaneHidden;
240270
public void SaveDataToXmlFile(string xmlFileName)
241271
{
242272
// Sadly, streamWriter does not have a way of setting the IFormatProvider property
@@ -773,6 +803,11 @@ private void DoNewWindow(object sender, ExecutedRoutedEventArgs e)
773803
newEventViewer.Show();
774804
Update();
775805
}
806+
807+
private void DoToggleMultiLineViewPane(object sender, ExecutedRoutedEventArgs e)
808+
{
809+
MultiLineViewPaneHidden = !MultiLineViewPaneHidden;
810+
}
776811
private void DoColumnsToDisplayListClick(object sender, RoutedEventArgs e)
777812
{
778813
if (EventTypes.SelectedItems.Count == 0)
@@ -1093,7 +1128,7 @@ public bool Find(string pat)
10931128
{
10941129
string modifiedPat = FilterQueryUtilities.TryExtractFilterQueryExpression(pat, out tree);
10951130
}
1096-
catch(FilterQueryExpressionParsingException exp)
1131+
catch (FilterQueryExpressionParsingException exp)
10971132
{
10981133
StatusBar.LogError(exp.Message);
10991134
return false;
@@ -1151,7 +1186,7 @@ public bool Find(string pat)
11511186
// Before parsing the "Rest" column, grab everything displayed from the ColumnsToDisplay.
11521187
if (m_source.ColumnsToDisplay != null)
11531188
{
1154-
for(int displayFieldIdx = 0; displayFieldIdx < m_source.ColumnsToDisplay.Count; displayFieldIdx++)
1189+
for (int displayFieldIdx = 0; displayFieldIdx < m_source.ColumnsToDisplay.Count; displayFieldIdx++)
11551190
{
11561191
data[m_source.ColumnsToDisplay[displayFieldIdx]] = item.DisplayFields[displayFieldIdx];
11571192
}
@@ -1170,7 +1205,7 @@ public bool Find(string pat)
11701205
// Parse Rest if the above steps fail.
11711206
if (!string.IsNullOrEmpty(item.Rest))
11721207
{
1173-
foreach(var r in item.Rest.Split(FilterQueryUtilities.SpaceSeparator, StringSplitOptions.RemoveEmptyEntries))
1208+
foreach (var r in item.Rest.Split(FilterQueryUtilities.SpaceSeparator, StringSplitOptions.RemoveEmptyEntries))
11741209
{
11751210
// Format of Rest: Property0=Value0 Property1=Value1
11761211
var splitOnEquals = r.Trim().Split('=');
@@ -1313,7 +1348,7 @@ public void Update()
13131348
StatusBar.LogError(fqepEx.Message);
13141349
m_source.FilterQueryExpressionTree = null;
13151350
}
1316-
catch(Exception ex)
1351+
catch (Exception ex)
13171352
{
13181353
StatusBar.LogError(ex.Message);
13191354
m_source.FilterQueryExpressionTree = null;
@@ -1571,6 +1606,8 @@ public static string GetColumnHeaderText(DataGridColumn column)
15711606
}
15721607

15731608
#region commandDefintions
1609+
public static RoutedUICommand ToggleMultiLineViewPaneCommand = new RoutedUICommand("Toggle Multi-line view Pane", "ToggleMultiLineViewPane", typeof(StackWindow),
1610+
new InputGestureCollection() { new KeyGesture(Key.F2) });
15741611
public static RoutedUICommand UsersGuideCommand = new RoutedUICommand("UsersGuide", "UsersGuide", typeof(EventWindow));
15751612
public static RoutedUICommand UpdateCommand = new RoutedUICommand("Update", "Update", typeof(EventWindow),
15761613
new InputGestureCollection() { new KeyGesture(Key.F5) });
@@ -1641,7 +1678,6 @@ private void SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e
16411678
m_clipboardRangeEnd = "";
16421679

16431680
var dataGrid = sender as DataGrid;
1644-
16451681
var cells = dataGrid.SelectedCells;
16461682
bool seenHexValue = false;
16471683
StatusBar.Status = "";
@@ -1804,6 +1840,17 @@ private void SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e
18041840
}
18051841

18061842
m_maxColumnInSelection = null;
1843+
1844+
if (cells != null && cells.Count > 0)
1845+
{
1846+
var selectedRecord = cells[0].Item as EventRecord;
1847+
1848+
if (selectedRecord != null)
1849+
{
1850+
MultiLineView.ItemsSource = selectedRecord.Payloads;
1851+
}
1852+
}
1853+
18071854
}
18081855

18091856
/// <summary>

0 commit comments

Comments
 (0)
Please sign in to comment.