From ef46c316265f69043557c5a20fc9072f20893d92 Mon Sep 17 00:00:00 2001
From: Ivo Petrov <48355182+ivaylo-matov@users.noreply.github.com>
Date: Tue, 17 Sep 2024 01:48:37 +0100
Subject: [PATCH] [DYN-7334] Revised TuneUp controls layout (#53)
* initial button re-arrangement
* DataGrid work
* progress record
* progress record
* collections wotking
fix sorting
add datagrids
* totals aligned
* total run times working
* evaluation completed works
* progress record
* progress
TODO:
- reset PreviousRun TotalGroup nodes' execution times
- remove execution time 0s from NotExecuted nodes when hoover over them
* progress record
TODO:
- Remove executionOrder from TotalGroupNodes
- ExportJSON - write the code
- Total time to be calculated from rounded times
- Remove ExecutionTimes from NotExecuted nodes
- UI to update when a node is renamed
- UI to update when group is renamed and/or background color has changed
* before UI cleanup
* progress record
* progress
* draft PR
* showGroups off by default
* Update ProfiledNodeViewModel.cs
---
TuneUp/ProfiledNodeViewModel.cs | 123 +++-
TuneUp/TuneUpViewExtension.cs | 7 +-
TuneUp/TuneUpWindow.xaml | 730 +++++++++++++++++-------
TuneUp/TuneUpWindow.xaml.cs | 200 +++++--
TuneUp/TuneUpWindowViewModel.cs | 964 +++++++++++++++++++++++++-------
TuneUpTests/TuneUpTests.cs | 6 +-
help-16px.png | Bin 0 -> 471 bytes
7 files changed, 1558 insertions(+), 472 deletions(-)
create mode 100644 help-16px.png
diff --git a/TuneUp/ProfiledNodeViewModel.cs b/TuneUp/ProfiledNodeViewModel.cs
index 87aca8b..ef378d4 100644
--- a/TuneUp/ProfiledNodeViewModel.cs
+++ b/TuneUp/ProfiledNodeViewModel.cs
@@ -7,18 +7,115 @@
using Dynamo.Core;
using Dynamo.Graph.Annotations;
using Dynamo.Graph.Nodes;
+using Dynamo.Graph.Nodes.CustomNodes;
+using Dynamo.Graph.Nodes.ZeroTouch;
namespace TuneUp
{
public class ProfiledNodeViewModel : NotificationObject
{
#region Properties
+
+ ///
+ /// Checks if the Node has been Renamed after its creation
+ ///
+ public bool IsRenamed
+ {
+ get
+ {
+ if (NodeModel == null)
+ {
+ return false;
+ }
+ isRenamed = GetOriginalName(NodeModel) != NodeModel.Name;
+ return isRenamed;
+ }
+ internal set
+ {
+ if (isRenamed == value) return;
+ isRenamed = value;
+ RaisePropertyChanged(nameof(IsRenamed));
+ }
+ }
+ private bool isRenamed = false;
+
+ ///
+ /// The original name of the node
+ ///
+ public string OriginalName
+ {
+ get
+ {
+ //string originalName = NodeModel.GetOriginalName();
+ string originalName = GetOriginalName(NodeModel);
+ return originalName;
+ }
+ internal set
+ {
+ if (originalName == value) return;
+ originalName = value;
+ RaisePropertyChanged(nameof(OriginalName));
+ }
+ }
+ private string originalName = string.Empty;
+
+ ///
+ /// Indicates whether this node represents the total execution time for its group
+ ///
+ public bool IsGroupExecutionTime
+ {
+ get => isGroupExecutionTime;
+ set
+ {
+ isGroupExecutionTime = value;
+ RaisePropertyChanged(nameof(IsGroupExecutionTime));
+ }
+ }
+ private bool isGroupExecutionTime = false;
+
+ ///
+ /// Getting the original name before graph author renamed the node
+ ///
+ /// target NodeModel
+ /// Original node name as string
+ private static string GetOriginalName(NodeModel node)
+ {
+ if (node == null) return string.Empty;
+ // For dummy node, return the current name so that does not appear to be renamed
+ if (node is DummyNode)
+ {
+ return node.Name;
+ }
+ if (node.IsCustomFunction)
+ {
+ // If the custom node is not loaded, return the current name so that does not appear to be renamed
+ if ((node as Function).State == ElementState.Error && (node as Function).Definition.IsProxy)
+ {
+ return node.Name;
+ }
+ // If the custom node is loaded, return original name as usual
+ var customNodeFunction = node as Function;
+ return customNodeFunction?.Definition.DisplayName;
+ }
+
+ var function = node as DSFunctionBase;
+ if (function != null)
+ return function.Controller.Definition.DisplayName;
+
+ var nodeType = node.GetType();
+ var elNameAttrib = nodeType.GetCustomAttributes(false).FirstOrDefault();
+ if (elNameAttrib != null)
+ return elNameAttrib.Name;
+
+ return nodeType.FullName;
+ }
+
///
/// Prefix string of execution time.
///
public static readonly string ExecutionTimelString = "Execution Time";
-
public static readonly string GroupNodePrefix = "Group: ";
+ public static readonly string GroupExecutionTimeString = "Group total";
private string name = String.Empty;
///
@@ -31,7 +128,9 @@ public string Name
get
{
// For virtual row, do not attempt to grab node name
- if (!name.Contains(ExecutionTimelString) && !name.StartsWith(GroupNodePrefix))
+ if (!name.Contains(ExecutionTimelString) &&
+ !name.StartsWith(GroupNodePrefix) &&
+ !name.Equals(GroupExecutionTimeString))
name = NodeModel?.Name;
return name;
}
@@ -102,8 +201,14 @@ public TimeSpan GroupExecutionTime
///
public int ExecutionMilliseconds
{
- get => (int)Math.Round(ExecutionTime.TotalMilliseconds);
+ get => executionMilliseconds;
+ set
+ {
+ executionMilliseconds = value;
+ RaisePropertyChanged(nameof(ExecutionMilliseconds));
+ }
}
+ private int executionMilliseconds;
///
/// Indicates whether this node was executed on the most recent graph run
@@ -176,6 +281,18 @@ public bool IsGroup
}
private bool isGroup;
+ public bool ShowGroupIndicator
+ {
+ get => showGroupIndicator;
+ set
+ {
+ showGroupIndicator = value;
+ RaisePropertyChanged(nameof(ShowGroupIndicator));
+ }
+ }
+ private bool showGroupIndicator;
+
+
///
/// The background brush for this node
/// If this node represents a group, it inherits the background color from the associated AnnotationModel
diff --git a/TuneUp/TuneUpViewExtension.cs b/TuneUp/TuneUpViewExtension.cs
index 57fe602..fabeb01 100644
--- a/TuneUp/TuneUpViewExtension.cs
+++ b/TuneUp/TuneUpViewExtension.cs
@@ -18,7 +18,6 @@ public class TuneUpViewExtension : ViewExtensionBase, IViewExtension
public override void Dispose()
{
- TuneUpView.Dispose();
}
public override void Startup(ViewStartupParams p)
@@ -34,7 +33,9 @@ public override void Loaded(ViewLoadedParams p)
TuneUpView = new TuneUpWindow(p, UniqueId)
{
// Set the data context for the main grid in the window.
- NodeAnalysisTable = { DataContext = ViewModel },
+ LatestRunTable = { DataContext = ViewModel },
+ PreviousRunTable = { DataContext = ViewModel },
+ NotExecutedTable = { DataContext = ViewModel },
MainGrid = { DataContext = ViewModel },
Owner = p.DynamoWindow
};
@@ -114,7 +115,7 @@ public override void Closed()
this.TuneUpMenuItem.IsChecked = false;
// Reset DataGrid sorting order & direction
- ViewModel.SortingOrder = "number";
+ ViewModel.SortingOrder = TuneUpWindowViewModel.SortByNumber;
ViewModel.SortDirection = System.ComponentModel.ListSortDirection.Ascending;
}
}
diff --git a/TuneUp/TuneUpWindow.xaml b/TuneUp/TuneUpWindow.xaml
index cbecbd3..cd15d62 100644
--- a/TuneUp/TuneUpWindow.xaml
+++ b/TuneUp/TuneUpWindow.xaml
@@ -1,89 +1,120 @@
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ #555555
+
+ #434343
+
+
-
+
-
-
-
-
-
+
-
+
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/TuneUp/TuneUpWindow.xaml.cs b/TuneUp/TuneUpWindow.xaml.cs
index 071e4b0..89c26bd 100644
--- a/TuneUp/TuneUpWindow.xaml.cs
+++ b/TuneUp/TuneUpWindow.xaml.cs
@@ -37,13 +37,7 @@ public partial class TuneUpWindow : Window
/// is initiated by the user (true) or programmatically (false).
///
private bool isUserInitiatedSelection = false;
-
- ///
- /// Since there is no API for height offset comparing to
- /// DynamoWindow height. Define it as static for now.
- ///
- private static double sidebarHeightOffset = 200;
-
+
///
/// Create the TuneUp Window
///
@@ -52,21 +46,11 @@ public TuneUpWindow(ViewLoadedParams vlp, string id)
{
InitializeComponent();
viewLoadedParams = vlp;
- // Initialize the height of the datagrid in order to make sure
- // vertical scrollbar can be displayed correctly.
- this.NodeAnalysisTable.Height = vlp.DynamoWindow.Height - sidebarHeightOffset;
- vlp.DynamoWindow.SizeChanged += DynamoWindow_SizeChanged;
commandExecutive = vlp.CommandExecutive;
viewModelCommandExecutive = vlp.ViewModelCommandExecutive;
uniqueId = id;
}
- private void DynamoWindow_SizeChanged(object sender, System.Windows.SizeChangedEventArgs e)
- {
- // Update the new height of datagrid
- this.NodeAnalysisTable.Height = e.NewSize.Height - sidebarHeightOffset;
- }
-
private void NodeAnalysisTable_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!isUserInitiatedSelection) return;
@@ -112,30 +96,25 @@ private void NodeAnalysisTable_MouseLeave(object sender, System.Windows.Input.Mo
isUserInitiatedSelection = false;
}
- internal void Dispose()
- {
- viewLoadedParams.DynamoWindow.SizeChanged -= DynamoWindow_SizeChanged;
- }
-
private void RecomputeGraph_Click(object sender, RoutedEventArgs e)
{
- (NodeAnalysisTable.DataContext as TuneUpWindowViewModel).ResetProfiling();
+ (LatestRunTable.DataContext as TuneUpWindowViewModel).ResetProfiling();
}
///
- /// Handles the sorting event for the NodeAnalysisTable DataGrid.
+ /// Handles the sorting event for the LatestRunTable DataGrid.
/// Updates the SortingOrder property in the view model based on the column header clicked by the user.
///
- private void NodeAnalysisTable_Sorting(object sender, DataGridSortingEventArgs e)
+ private void LatestRunTable_Sorting(object sender, DataGridSortingEventArgs e)
{
- var viewModel = NodeAnalysisTable.DataContext as TuneUpWindowViewModel;
+ var viewModel = LatestRunTable.DataContext as TuneUpWindowViewModel;
if (viewModel != null)
{
viewModel.SortingOrder = e.Column.Header switch
{
- "#" => "number",
- "Name" => "name",
- "Execution Time (ms)" => "time",
+ "#" => TuneUpWindowViewModel.SortByNumber,
+ "Name" => TuneUpWindowViewModel.SortByName,
+ "Execution Time (ms)" => TuneUpWindowViewModel.SortByTime,
_ => viewModel.SortingOrder
};
@@ -150,10 +129,25 @@ private void NodeAnalysisTable_Sorting(object sender, DataGridSortingEventArgs e
}
}
- private void ExportTimes_Click(object sender, RoutedEventArgs e)
+ private void NotExecutedTable_Sorting(object sender, DataGridSortingEventArgs e)
+ {
+ e.Handled = true;
+ }
+
+ private void ExportToJson_Click(object sender, RoutedEventArgs e)
+ {
+ (LatestRunTable.DataContext as TuneUpWindowViewModel)?.ExportToJson();
+ }
+
+ private void ExportToCsv_Click(object sender, RoutedEventArgs e)
{
- (NodeAnalysisTable.DataContext as TuneUpWindowViewModel).ExportToCsv();
+ (LatestRunTable.DataContext as TuneUpWindowViewModel)?.ExportToCsv();
}
+
+ private void ExportButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ ExportButton.ContextMenu.IsOpen = true;
+ }
}
#region Converters
@@ -162,15 +156,64 @@ public class IsGroupToMarginMultiConverter : IMultiValueConverter
{
private static readonly Guid DefaultGuid = Guid.Empty;
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values.Length == 3 &&
+ values[0] is bool isGroup &&
+ values[1] is Guid groupGuid &&
+ values[2] is bool showGroupIndicator && showGroupIndicator)
+ {
+ if ( isGroup || !groupGuid.Equals(DefaultGuid)) return new System.Windows.Thickness(5,0,0,0);
+ }
+ return new System.Windows.Thickness(-4,0,0,0);
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class IsInGroupToColorBrushMultiConverter : IMultiValueConverter
+ {
+ private static readonly Guid DefaultGuid = Guid.Empty;
+ private static readonly SolidColorBrush TransparentBrush = Brushes.Transparent;
+
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values.Length == 4 &&
+ values[0] is bool isGroup &&
+ values[1] is Guid groupGuid &&
+ values[2] is SolidColorBrush groupColorBrush &&
+ values[3] is bool showGroupIndicator)
+ {
+ if (showGroupIndicator && groupColorBrush != null)
+ {
+ if (isGroup || groupGuid != DefaultGuid) return groupColorBrush;
+ }
+ }
+ return TransparentBrush;
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class IsGroupToColorBrushMultiConverter : IMultiValueConverter
+ {
+ private static readonly SolidColorBrush TransparentBrush = Brushes.Transparent;
+
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2 &&
values[0] is bool isGroup &&
- values[1] is Guid groupGuid && groupGuid != DefaultGuid)
+ values[1] is SolidColorBrush groupColorBrush)
{
- return isGroup ? new System.Windows.Thickness(0) : new System.Windows.Thickness(30, 0, 0, 0);
+ if (isGroup && groupColorBrush != null) return groupColorBrush;
}
- return new System.Windows.Thickness(0);
+ return TransparentBrush;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
@@ -179,21 +222,21 @@ public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
}
}
- public class IsGroupToBrushConverter : IValueConverter
+ public class IsGroupToColorBrushConverter : IValueConverter
{
private static readonly SolidColorBrush GroupBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#333333"));
- private static readonly SolidColorBrush DefaultBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#AAAAAA"));
+ private static readonly SolidColorBrush DarkThemeBodyMediumBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F5F5F5"));
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
- return value is bool isGroup && isGroup ? GroupBrush : DefaultBrush;
+ return value is bool isGroup && isGroup ? GroupBrush : DarkThemeBodyMediumBrush;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
- }
+ }
public class IsGroupToVisibilityMultiConverter : IMultiValueConverter
{
@@ -220,5 +263,86 @@ public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
}
}
+ public class IsRenamedToVisibilityMultiConverter : IMultiValueConverter
+ {
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values.Length == 2 &&
+ values[0] is bool isGroup &&
+ values[1] is bool isRenamed && !isGroup)
+ {
+ if (isRenamed) return Visibility.Visible;
+ }
+ return Visibility.Collapsed;
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class ExecutionOrderNumberConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is ProfiledNodeViewModel node)
+ {
+ return node.ShowGroupIndicator ? node.GroupExecutionOrderNumber : node.ExecutionOrderNumber;
+ }
+ return null;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class ExecutionOrderNumberVisibilityConverter : IMultiValueConverter
+ {
+ private static readonly Guid DefaultGuid = Guid.Empty;
+
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values.Length == 3 &&
+ values[0] is bool isGroup &&
+ values[1] is Guid groupGuid &&
+ values[2] is bool showGroups)
+ {
+ if (showGroups)
+ {
+ if (isGroup || groupGuid == DefaultGuid) return Visibility.Visible;
+ else return Visibility.Collapsed;
+ }
+ if (!showGroups) return Visibility.Visible;
+ }
+ return Visibility.Collapsed;
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class ContainsStringConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is string name)
+ {
+ return (name.StartsWith(ProfiledNodeViewModel.ExecutionTimelString) ||
+ name.Equals(ProfiledNodeViewModel.GroupExecutionTimeString));
+ }
+ return false;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
#endregion
}
diff --git a/TuneUp/TuneUpWindowViewModel.cs b/TuneUp/TuneUpWindowViewModel.cs
index b9c85ef..ef2f2cb 100644
--- a/TuneUp/TuneUpWindowViewModel.cs
+++ b/TuneUp/TuneUpWindowViewModel.cs
@@ -6,6 +6,7 @@
using System.IO;
using System.Linq;
using System.Threading;
+using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
using Dynamo.Core;
@@ -16,6 +17,7 @@
using Dynamo.ViewModels;
using Dynamo.Wpf.Extensions;
using Microsoft.Win32;
+using Newtonsoft.Json;
namespace TuneUp
{
@@ -27,19 +29,19 @@ public enum ProfiledNodeState
[Display(Name = "Executing")]
Executing = 0,
- [Display(Name = "Executed On Current Run")]
+ [Display(Name = "Latest run")]
ExecutedOnCurrentRun = 1,
- [Display(Name = "Executed On Current Run")]
+ [Display(Name = "Latest run")]
ExecutedOnCurrentRunTotal = 2,
- [Display(Name = "Executed On Previous Run")]
+ [Display(Name = "Previous run")]
ExecutedOnPreviousRun = 3,
- [Display(Name = "Executed On Previous Run")]
+ [Display(Name = "Previous run")]
ExecutedOnPreviousRunTotal = 4,
- [Display(Name = "Not Executed")]
+ [Display(Name = "Not executed")]
NotExecuted = 5,
}
@@ -53,37 +55,22 @@ public class TuneUpWindowViewModel : NotificationObject, IDisposable
private ViewLoadedParams viewLoadedParams;
private IProfilingExecutionTimeData executionTimeData;
+ private HomeWorkspaceModel currentWorkspace;
+ private SynchronizationContext uiContext;
private int executedNodesNum;
private bool isProfilingEnabled = true;
private bool isRecomputeEnabled = true;
- private HomeWorkspaceModel currentWorkspace;
- private Dictionary nodeDictionary = new Dictionary();
- private Dictionary> groupDictionary = new Dictionary>();
- private SynchronizationContext uiContext;
private bool isTuneUpChecked = false;
+ private bool showGroups;
private ListSortDirection sortDirection;
- private string sortingOrder = "number";
-
- ///
- /// Name of the row to display current execution time
- ///
- private string CurrentExecutionString = ProfiledNodeViewModel.ExecutionTimelString + " On Current Run";
-
- ///
- /// Name of the row to display previous execution time
- ///
- private string PreviousExecutionString = ProfiledNodeViewModel.ExecutionTimelString + " On Previous Run";
-
- ///
- /// Shortcut to current execution time row
- ///
- private ProfiledNodeViewModel CurrentExecutionTimeRow => ProfiledNodes.FirstOrDefault(n => n.Name == CurrentExecutionString);
-
- ///
- /// Shortcut to previous execution time row
- ///
- private ProfiledNodeViewModel PreviousExecutionTimeRow => ProfiledNodes.FirstOrDefault(n => n.Name == PreviousExecutionString);
-
+ private const string defaultExecutionTime = "N/A";
+ private string defaultSortingOrder = "number";
+ private string latestGraphExecutionTime = defaultExecutionTime;
+ private string previousGraphExecutionTime = defaultExecutionTime;
+ private string totalGraphExecutionTime = defaultExecutionTime;
+ private Dictionary nodeDictionary = new Dictionary();
+ private Dictionary> groupDictionary = new Dictionary>();
+ private Dictionary executionTimeNodeDictionary = new Dictionary();
private HomeWorkspaceModel CurrentWorkspace
{
get => currentWorkspace;
@@ -108,43 +95,6 @@ private HomeWorkspaceModel CurrentWorkspace
}
}
- ///
- /// Gets or sets the sorting order and toggles the sort direction.
- ///
- public string SortingOrder
- {
- get => sortingOrder;
- set
- {
- if (sortingOrder != value)
- {
- sortingOrder = value;
- SortDirection = ListSortDirection.Ascending;
- }
- else
- {
- SortDirection = SortDirection == ListSortDirection.Ascending
- ? ListSortDirection.Descending
- : ListSortDirection.Ascending;
- }
- }
- }
-
- ///
- /// Gets or sets the sort direction and raises property change notification if the value changes.
- ///
- public ListSortDirection SortDirection
- {
- get => sortDirection;
- set
- {
- if (sortDirection != value)
- {
- sortDirection = value;
- }
- }
- }
-
#endregion
#region Public Properties
@@ -191,31 +141,138 @@ public bool IsTuneUpChecked
}
///
- /// Collection of profiling data for nodes in the current workspace
+ /// Collections of profiling data for nodes in the current workspace
+ ///
+ public ObservableCollection ProfiledNodesLatestRun { get; private set; }
+ public ObservableCollection ProfiledNodesPreviousRun { get; private set; }
+ public ObservableCollection ProfiledNodesNotExecuted { get; private set; }
+
+ ///
+ /// Collections of profiling data for nodes in the current workspace
///
- public ObservableCollection ProfiledNodes { get; set; } = new ObservableCollection();
+ public CollectionViewSource ProfiledNodesCollectionLatestRun { get; set; }
+ public CollectionViewSource ProfiledNodesCollectionPreviousRun { get; set; }
+ public CollectionViewSource ProfiledNodesCollectionNotExecuted { get; set; }
///
- /// Collection of profiling data for nodes in the current workspace.
- /// Profiling data in this collection is grouped by the profiled nodes' states.
+ /// Returns visibility status for the latest run, previous run, and not executed node collections,
+ /// based on whether each collection contains any nodes.
///
- public CollectionViewSource ProfiledNodesCollection { get; set; }
+ public Visibility LatestRunTableVisibility
+ {
+ get => ProfiledNodesLatestRun != null && ProfiledNodesLatestRun.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
+ }
+ public Visibility PreviousRunTableVisibility
+ {
+ get => ProfiledNodesPreviousRun != null && ProfiledNodesPreviousRun.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
+ }
+ public Visibility NotExecutedTableVisibility
+ {
+ get => ProfiledNodesNotExecuted != null && ProfiledNodesNotExecuted.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
+ }
///
/// Total graph execution time
///
- public string TotalGraphExecutiontime
+ public string TotalGraphExecutionTime
+ {
+ get => totalGraphExecutionTime;
+ set
+ {
+ if (totalGraphExecutionTime != value)
+ {
+ totalGraphExecutionTime = value;
+ RaisePropertyChanged(nameof(TotalGraphExecutionTime));
+ }
+ }
+ }
+ public string LatestGraphExecutionTime
{
- get
+ get => latestGraphExecutionTime;
+ set
{
- if (CurrentExecutionTimeRow == null)
+ if (latestGraphExecutionTime != value)
{
- return "N/A";
+ latestGraphExecutionTime = value;
+ RaisePropertyChanged(nameof(LatestGraphExecutionTime));
+ }
+ }
+ }
+ public string PreviousGraphExecutionTime
+ {
+ get => previousGraphExecutionTime;
+ set
+ {
+ if (previousGraphExecutionTime != value)
+ {
+ previousGraphExecutionTime = value;
+ RaisePropertyChanged(nameof(PreviousGraphExecutionTime));
}
- return (PreviousExecutionTimeRow?.ExecutionMilliseconds + CurrentExecutionTimeRow?.ExecutionMilliseconds).ToString() + "ms";
}
}
+ ///
+ /// Gets or sets whether node groups are displayed, and refreshes node collections based on this setting.
+ ///
+ public bool ShowGroups
+ {
+ get => showGroups;
+ set
+ {
+ if (showGroups != value)
+ {
+ showGroups = value;
+ RaisePropertyChanged(nameof(ShowGroups));
+
+ // Refresh all collections and apply group settings
+ UpdateGroupsVisibility(ProfiledNodesCollectionLatestRun, ProfiledNodesLatestRun);
+ UpdateGroupsVisibility(ProfiledNodesCollectionPreviousRun, ProfiledNodesPreviousRun);
+ UpdateGroupsVisibility(ProfiledNodesCollectionNotExecuted, ProfiledNodesNotExecuted);
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the sort direction and raises property change notification if the value changes.
+ ///
+ public ListSortDirection SortDirection
+ {
+ get => sortDirection;
+ set
+ {
+ if (sortDirection != value)
+ {
+ sortDirection = value;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the sorting order and toggles the sort direction.
+ ///
+ public string SortingOrder
+ {
+ get => defaultSortingOrder;
+ set
+ {
+ if (defaultSortingOrder != value)
+ {
+ defaultSortingOrder = value;
+ SortDirection = ListSortDirection.Ascending;
+ }
+ else
+ {
+ SortDirection = SortDirection == ListSortDirection.Ascending
+ ? ListSortDirection.Descending
+ : ListSortDirection.Ascending;
+ }
+ }
+ }
+
+ public const string SortByName = "name";
+ public const string SortByNumber = "number";
+ public const string SortByTime = "time";
+
#endregion
#region Constructor
@@ -246,65 +303,80 @@ internal void ResetProfiledNodes()
{
if (CurrentWorkspace == null) return;
- // Use temporary collections to minimize UI updates
- var newProfiledNodes = new ObservableCollection();
- var newNodeDictionary = new Dictionary();
- var newGroupDictionary = new Dictionary>();
+ // Clear existing collections if they are not null
+ ProfiledNodesLatestRun?.Clear();
+ ProfiledNodesPreviousRun?.Clear();
+ ProfiledNodesNotExecuted?.Clear();
- // Assign the new collection
- ProfiledNodes = newProfiledNodes;
- nodeDictionary = newNodeDictionary;
- groupDictionary = newGroupDictionary;
+ // Reset total times
+ LatestGraphExecutionTime = defaultExecutionTime;
+ PreviousGraphExecutionTime = defaultExecutionTime;
+ TotalGraphExecutionTime = defaultExecutionTime;
+
+ // Initialize observable collections and dictionaries
+ ProfiledNodesLatestRun = ProfiledNodesLatestRun ?? new ObservableCollection();
+ ProfiledNodesPreviousRun = ProfiledNodesPreviousRun ?? new ObservableCollection();
+ ProfiledNodesNotExecuted = ProfiledNodesNotExecuted ?? new ObservableCollection();
+ nodeDictionary = new Dictionary();
+ groupDictionary = new Dictionary>();
// Process groups and their nodes
foreach (var group in CurrentWorkspace.Annotations)
{
- var profiledGroup = new ProfiledNodeViewModel(group);
- nodeDictionary[group.GUID] = profiledGroup;
- ProfiledNodes.Add(profiledGroup);
- groupDictionary[group.GUID] = new List();
+ var groupGUID = group.GUID;
+ var groupBackgroundBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(group.Background));
- // Create profiledNode for each node in the group
- foreach (var node in group.Nodes)
+ // Create and add profiled group node
+ var profiledGroup = new ProfiledNodeViewModel(group)
{
- if (node is NodeModel nodeModel)
- {
- var profiledNode = new ProfiledNodeViewModel(nodeModel)
- {
- GroupGUID = group.GUID,
- GroupName = group.AnnotationText
- };
- nodeDictionary[node.GUID] = profiledNode;
- ProfiledNodes.Add(profiledNode);
- groupDictionary[group.GUID].Add(profiledNode);
- }
- }
- }
- // Create profiledNode for each of the standalone nodes
- foreach (var node in CurrentWorkspace.Nodes)
- {
- if (!nodeDictionary.ContainsKey(node.GUID))
+ BackgroundBrush = groupBackgroundBrush,
+ GroupGUID = groupGUID
+ };
+ ProfiledNodesNotExecuted.Add(profiledGroup);
+ nodeDictionary[group.GUID] = profiledGroup;
+
+ // Initialize group in group dictionary
+ groupDictionary[groupGUID] = new List();
+
+ // Add each node in the group
+ foreach (var node in group.Nodes.OfType())
{
var profiledNode = new ProfiledNodeViewModel(node)
{
- GroupName = node.Name
+ GroupGUID = groupGUID,
+ GroupName = group.AnnotationText,
+ BackgroundBrush = groupBackgroundBrush
};
+ ProfiledNodesNotExecuted.Add(profiledNode);
nodeDictionary[node.GUID] = profiledNode;
- ProfiledNodes.Add(profiledNode);
+ groupDictionary[groupGUID].Add(profiledNode);
}
}
- ProfiledNodesCollection = new CollectionViewSource();
- ProfiledNodesCollection.Source = ProfiledNodes;
- ProfiledNodesCollection.GroupDescriptions.Add(new PropertyGroupDescription(nameof(ProfiledNodeViewModel.StateDescription)));
+ // Process standalone nodes (those not in groups)
+ foreach (var node in CurrentWorkspace.Nodes.Where(n => !nodeDictionary.ContainsKey(n.GUID)))
+ {
+ var profiledNode = new ProfiledNodeViewModel(node)
+ {
+ GroupName = node.Name
+ };
+ ProfiledNodesNotExecuted.Add(profiledNode);
+ nodeDictionary[node.GUID] = profiledNode;
+ }
+
+ ProfiledNodesCollectionLatestRun = new CollectionViewSource { Source = ProfiledNodesLatestRun };
+ ProfiledNodesCollectionPreviousRun = new CollectionViewSource { Source = ProfiledNodesPreviousRun };
+ ProfiledNodesCollectionNotExecuted = new CollectionViewSource { Source = ProfiledNodesNotExecuted };
+
+ ApplyGroupNodeFilter();
+ ApplyCustomSorting(ProfiledNodesCollectionNotExecuted, SortByName);
- // Sort the data by execution state and then groupName to ensure nodes fall under groups
- ApplyDefaultSorting();
- ProfiledNodesCollection.View?.Refresh();
+ RefreshAllCollectionViews();
- RaisePropertyChanged(nameof(ProfiledNodesCollection));
- RaisePropertyChanged(nameof(ProfiledNodes));
- RaisePropertyChanged(nameof(TotalGraphExecutiontime));
+ RaisePropertyChanged(nameof(ProfiledNodesCollectionNotExecuted));
+ RaisePropertyChanged(nameof(ProfiledNodesNotExecuted));
+
+ RaisePropertyChanged(nameof(NotExecutedTableVisibility));
}
///
@@ -348,7 +420,7 @@ internal void EnableProfiling()
isProfilingEnabled = true;
executionTimeData = CurrentWorkspace.EngineController.ExecutionTimeData;
}
- RaisePropertyChanged(nameof(ProfiledNodesCollection));
+ RaisePropertyChanged(nameof(ProfiledNodesCollectionNotExecuted));
}
internal void DisableProfiling()
@@ -370,7 +442,6 @@ private void CurrentWorkspaceModel_EvaluationStarted(object sender, EventArgs e)
foreach (var node in nodeDictionary.Values)
{
// Reset Node Execution Order info
- node.ExecutionOrderNumber = null;
node.WasExecutedOnLastRun = false;
// Update Node state
@@ -378,23 +449,40 @@ private void CurrentWorkspaceModel_EvaluationStarted(object sender, EventArgs e)
{
node.State = ProfiledNodeState.ExecutedOnPreviousRun;
}
+ // Move to CollectionPreviousRun
+ if (node.State == ProfiledNodeState.ExecutedOnPreviousRun)
+ {
+ MoveNodeToCollection(node, null);
+ ProfiledNodesPreviousRun.Add(node);
+ }
}
- executedNodesNum = 0;
+ executedNodesNum = 1;
EnableProfiling();
}
private void CurrentWorkspaceModel_EvaluationCompleted(object sender, Dynamo.Models.EvaluationCompletedEventArgs e)
{
IsRecomputeEnabled = true;
+
CalculateGroupNodes();
UpdateExecutionTime();
- RaisePropertyChanged(nameof(ProfiledNodesCollection));
- RaisePropertyChanged(nameof(ProfiledNodes));
- ProfiledNodesCollection.Dispatcher.InvokeAsync(() =>
+ RaisePropertyChanged(nameof(LatestRunTableVisibility));
+ RaisePropertyChanged(nameof(PreviousRunTableVisibility));
+ RaisePropertyChanged(nameof(NotExecutedTableVisibility));
+
+ RaisePropertyChanged(nameof(ProfiledNodesCollectionLatestRun));
+ RaisePropertyChanged(nameof(ProfiledNodesCollectionPreviousRun));
+ RaisePropertyChanged(nameof(ProfiledNodesCollectionNotExecuted));
+
+ ProfiledNodesCollectionLatestRun.Dispatcher.InvokeAsync(() =>
{
- ApplyCustomSorting();
- ProfiledNodesCollection.View.Refresh();
+ ApplyCustomSorting(ProfiledNodesCollectionLatestRun);
+ ProfiledNodesCollectionLatestRun.View?.Refresh();
+ ApplyCustomSorting(ProfiledNodesCollectionPreviousRun);
+ ProfiledNodesCollectionPreviousRun.View?.Refresh();
+ ProfiledNodesCollectionNotExecuted.View?.Refresh();
+
});
}
@@ -407,18 +495,24 @@ private void UpdateExecutionTime()
// Reset execution time
uiContext.Send(
x =>
- {
- ProfiledNodes.Remove(CurrentExecutionTimeRow);
- ProfiledNodes.Remove(PreviousExecutionTimeRow);
- // After each evaluation, manually update execution time column(s)
- var totalSpanExecuted = new TimeSpan(ProfiledNodes.Where(n => n.WasExecutedOnLastRun).Where(n => !n.IsGroup).Sum(r => r.ExecutionTime.Ticks));
- var totalSpanUnexecuted = new TimeSpan(ProfiledNodes.Where(n => !n.WasExecutedOnLastRun).Where(n => !n.IsGroup).Sum(r => r.ExecutionTime.Ticks));
- ProfiledNodes.Add(new ProfiledNodeViewModel(
- CurrentExecutionString, totalSpanExecuted, ProfiledNodeState.ExecutedOnCurrentRunTotal));
- ProfiledNodes.Add(new ProfiledNodeViewModel(
- PreviousExecutionString, totalSpanUnexecuted, ProfiledNodeState.ExecutedOnPreviousRunTotal));
+ { // After each evaluation, manually update execution time column(s)
+ // Calculate total execution times using rounded node execution times, not exact values.
+ int totalLatestRun = ProfiledNodesLatestRun
+ .Where(n => n.WasExecutedOnLastRun && !n.IsGroup && !n.IsGroupExecutionTime)
+ .Sum(r => r?.ExecutionMilliseconds ?? 0);
+ int previousLatestRun = ProfiledNodesPreviousRun
+ .Where(n => !n.WasExecutedOnLastRun && !n.IsGroup && !n.IsGroupExecutionTime)
+ .Sum(r => r?.ExecutionMilliseconds ?? 0);
+
+ // Update latest and previous run times
+ latestGraphExecutionTime = totalLatestRun.ToString();
+ previousGraphExecutionTime = previousLatestRun.ToString();
+ totalGraphExecutionTime = (totalLatestRun + previousLatestRun).ToString();
}, null);
- RaisePropertyChanged(nameof(TotalGraphExecutiontime));
+
+ RaisePropertyChanged(nameof(TotalGraphExecutionTime));
+ RaisePropertyChanged(nameof(LatestGraphExecutionTime));
+ RaisePropertyChanged(nameof(PreviousGraphExecutionTime));
}
///
@@ -429,24 +523,31 @@ private void UpdateExecutionTime()
private void CalculateGroupNodes()
{
int groupExecutionCounter = 1;
- bool groupIsRenamed = false;
var processedNodes = new HashSet();
- var sortedProfiledNodes = ProfiledNodes.OrderBy(node => node.ExecutionOrderNumber).ToList();
+ var sortedProfiledNodes = ProfiledNodesLatestRun.OrderBy(node => node.ExecutionOrderNumber).ToList();
+
+ // Create lookup dictionaries
+ var annotationLookup = CurrentWorkspace.Annotations.ToDictionary(g => g.GUID);
foreach (var profiledNode in sortedProfiledNodes)
{
// Process nodes that belong to a group and have not been processed yet
- if (!profiledNode.IsGroup && profiledNode.GroupGUID != Guid.Empty && !processedNodes.Contains(profiledNode))
+ if (!profiledNode.IsGroup && !profiledNode.IsGroupExecutionTime && profiledNode.GroupGUID != Guid.Empty && !processedNodes.Contains(profiledNode))
{
if (nodeDictionary.TryGetValue(profiledNode.GroupGUID, out var profiledGroup) &&
groupDictionary.TryGetValue(profiledNode.GroupGUID, out var nodesInGroup))
{
+ ProfiledNodeViewModel groupTotalTimeNode = null;
+ bool groupIsRenamed = false;
+
+ // Reset group state
profiledGroup.State = profiledNode.State;
profiledGroup.GroupExecutionTime = TimeSpan.Zero; // Reset execution time
+ profiledGroup.ExecutionMilliseconds = 0; // Reset UI execution time
+ MoveNodeToCollection(profiledGroup, ProfiledNodesLatestRun); // Ensure the profiledGroup is in latest run
// Check if the group has been renamed
- var groupModel = CurrentWorkspace.Annotations.FirstOrDefault(g => g.GUID == profiledGroup.GroupGUID);
- if (groupModel != null && profiledGroup.GroupName != groupModel.AnnotationText)
+ if (annotationLookup.TryGetValue(profiledGroup.GroupGUID, out var groupModel) && profiledGroup.GroupName != groupModel.AnnotationText)
{
groupIsRenamed = true;
profiledGroup.GroupName = groupModel.AnnotationText;
@@ -456,19 +557,37 @@ private void CalculateGroupNodes()
// Iterate through the nodes in the group
foreach (var node in nodesInGroup)
{
- if (processedNodes.Add(node)) // Adds to HashSet and checks if it was added
+ // Find groupTotalExecutionTime node, if it already exists
+ if (node.IsGroupExecutionTime)
+ {
+ groupTotalTimeNode = node;
+ }
+ else if (processedNodes.Add(node))
{
// Update group state, execution order, and execution time
- profiledGroup.GroupExecutionTime += node.ExecutionTime;
+ profiledGroup.GroupExecutionTime += node.ExecutionTime; // accurate, for sorting
+ profiledGroup.ExecutionMilliseconds += node.ExecutionMilliseconds; // rounded, for display in UI
node.GroupExecutionOrderNumber = groupExecutionCounter;
- if (groupIsRenamed) node.GroupName = profiledGroup.GroupName;
+ if (groupIsRenamed)
+ {
+ node.GroupName = profiledGroup.GroupName;
+ }
}
}
- profiledGroup.ExecutionOrderNumber = groupExecutionCounter;
+
+ // Update the properties of the group node
profiledGroup.GroupExecutionOrderNumber = groupExecutionCounter++;
profiledGroup.ExecutionTime = profiledGroup.GroupExecutionTime;
+ profiledGroup.WasExecutedOnLastRun = true;
+
- // Update the total groupExecutionTime for the purposes of sorting
+ // Create and add group total execution time node if it doesn't exist
+ groupTotalTimeNode ??= CreateGroupTotalTimeNode(profiledGroup);
+
+ // Update the properties of the groupTotalTimeNode and move to latestRunCollection
+ UpdateGroupTotalTimeNodeProperties(groupTotalTimeNode, profiledGroup);
+
+ // Update the groupExecutionTime for all nodes of the group for the purposes of sorting
foreach (var node in nodesInGroup)
{
node.GroupExecutionTime = profiledGroup.GroupExecutionTime;
@@ -476,53 +595,16 @@ private void CalculateGroupNodes()
}
}
// Process standalone nodes
- else if (!profiledNode.IsGroup && processedNodes.Add(profiledNode) && !profiledNode.Name.Contains(ProfiledNodeViewModel.ExecutionTimelString))
+ else if (!profiledNode.IsGroup && processedNodes.Add(profiledNode) &&
+ !profiledNode.Name.Contains(ProfiledNodeViewModel.ExecutionTimelString) &&
+ !profiledNode.IsGroupExecutionTime)
{
- profiledNode.ExecutionOrderNumber = groupExecutionCounter;
profiledNode.GroupExecutionOrderNumber = groupExecutionCounter++;
profiledNode.GroupExecutionTime = profiledNode.ExecutionTime;
}
}
}
- ///
- /// Applies the sorting logic to the ProfiledNodesCollection.
- ///
- public void ApplyCustomSorting()
- {
- ProfiledNodesCollection.SortDescriptions.Clear();
- // Sort nodes into execution group
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.State), ListSortDirection.Ascending));
-
- switch (sortingOrder)
- {
- case "time":
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.GroupExecutionTime), sortDirection));
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.IsGroup), ListSortDirection.Descending));
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.ExecutionTime), sortDirection));
- break;
- case "name":
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.GroupName), sortDirection));
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.IsGroup), ListSortDirection.Descending));
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.Name), sortDirection));
- break;
- case "number":
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.GroupExecutionOrderNumber), sortDirection));
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.IsGroup), ListSortDirection.Descending));
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.ExecutionOrderNumber), sortDirection));
- break;
- }
- }
-
- private void ApplyDefaultSorting()
- {
- ProfiledNodesCollection.SortDescriptions.Clear();
- // Sort nodes into execution group
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.GroupName), ListSortDirection.Ascending));
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.IsGroup), ListSortDirection.Descending));
- ProfiledNodesCollection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.Name), ListSortDirection.Ascending));
- }
-
internal void OnNodeExecutionBegin(NodeModel nm)
{
var profiledNode = nodeDictionary[nm.GUID];
@@ -539,10 +621,15 @@ internal void OnNodeExecutionEnd(NodeModel nm)
if (executionTime > TimeSpan.Zero)
{
profiledNode.ExecutionTime = executionTime;
+ // Assign execution time and manually set the execution milliseconds value
+ // so that group node execution time is based on rounded millisecond values.
+ profiledNode.ExecutionMilliseconds = (int)Math.Round(executionTime.TotalMilliseconds);
if (!profiledNode.WasExecutedOnLastRun)
{
profiledNode.ExecutionOrderNumber = executedNodesNum++;
+ // Move to collection LatestRun
+ MoveNodeToCollection(profiledNode, ProfiledNodesLatestRun);
}
}
@@ -551,6 +638,70 @@ internal void OnNodeExecutionEnd(NodeModel nm)
profiledNode.State = ProfiledNodeState.ExecutedOnCurrentRun;
}
+ internal void OnNodePropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (sender is NodeModel nodeModel && nodeDictionary.TryGetValue(nodeModel.GUID, out var profiledNode))
+ {
+ bool hasChanges = false;
+
+ // Detect node renaming
+ if (e.PropertyName == nameof(nodeModel.Name))
+ {
+ profiledNode.Name = nodeModel.Name;
+ hasChanges = true;
+ }
+
+ // Refresh UI if any changes were made
+ if (hasChanges)
+ {
+ RefreshCollectionViewContainingNode(profiledNode);
+ }
+ }
+ }
+
+ internal void OnGroupPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (sender is AnnotationModel groupModel && nodeDictionary.TryGetValue(groupModel.GUID, out var profiledGroup))
+ {
+ bool hasChanges = false;
+
+ // Detect group renaming
+ if (e.PropertyName == nameof(groupModel.AnnotationText))
+ {
+ profiledGroup.Name = $"{ProfiledNodeViewModel.GroupNodePrefix}{groupModel.AnnotationText}";
+ profiledGroup.GroupName = groupModel.AnnotationText;
+
+ // Update the nodes in the group
+ foreach (var profiledNode in groupDictionary[groupModel.GUID])
+ {
+ profiledNode.GroupName = groupModel.AnnotationText;
+ }
+ hasChanges = true;
+ }
+
+ // Detect change of color
+ if (e.PropertyName == nameof(groupModel.Background))
+ {
+ var newBackgroundBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(groupModel.Background));
+ profiledGroup.BackgroundBrush = newBackgroundBrush;
+
+ // Update the nodes in the group
+ foreach (var profiledNode in groupDictionary[groupModel.GUID])
+ {
+ profiledNode.BackgroundBrush = newBackgroundBrush;
+ }
+ hasChanges = true;
+ }
+
+ // Refresh UI if any changes were made
+ if (hasChanges)
+ {
+ NotifyProfilingCollectionsChanged();
+ RefreshAllCollectionViews();
+ }
+ }
+ }
+
#endregion
#region Workspace Events
@@ -559,29 +710,39 @@ private void CurrentWorkspaceModel_NodeAdded(NodeModel node)
{
var profiledNode = new ProfiledNodeViewModel(node);
nodeDictionary[node.GUID] = profiledNode;
+
node.NodeExecutionBegin += OnNodeExecutionBegin;
node.NodeExecutionEnd += OnNodeExecutionEnd;
- ProfiledNodes.Add(profiledNode);
- RaisePropertyChanged(nameof(ProfiledNodesCollection));
+ node.PropertyChanged += OnNodePropertyChanged;
+
+ ProfiledNodesNotExecuted.Add(profiledNode);
+ RaisePropertyChanged(nameof(NotExecutedTableVisibility));
}
private void CurrentWorkspaceModel_NodeRemoved(NodeModel node)
{
var profiledNode = nodeDictionary[node.GUID];
nodeDictionary.Remove(node.GUID);
+
node.NodeExecutionBegin -= OnNodeExecutionBegin;
node.NodeExecutionEnd -= OnNodeExecutionEnd;
- ProfiledNodes.Remove(profiledNode);
- RaisePropertyChanged(nameof(ProfiledNodesCollection));
+ node.PropertyChanged -= OnNodePropertyChanged;
+
+ MoveNodeToCollection(profiledNode, null);
+ //Recalculate the execution times
+ UpdateExecutionTime();
}
private void CurrentWorkspaceModel_GroupAdded(AnnotationModel group)
{
var profiledGroup = new ProfiledNodeViewModel(group);
nodeDictionary[group.GUID] = profiledGroup;
- ProfiledNodes.Add(profiledGroup);
+ ProfiledNodesNotExecuted.Add(profiledGroup);
groupDictionary[group.GUID] = new List();
+ group.PropertyChanged += OnGroupPropertyChanged;
+
+ // Create profiledNode for each node in the group
foreach (var node in group.Nodes)
{
if (node is NodeModel nodeModel)
@@ -595,51 +756,57 @@ private void CurrentWorkspaceModel_GroupAdded(AnnotationModel group)
{
profiledNode = new ProfiledNodeViewModel(node as NodeModel);
nodeDictionary[node.GUID] = profiledNode;
- ProfiledNodes.Add(profiledNode);
+ ProfiledNodesNotExecuted.Add(profiledNode);
}
profiledNode.GroupGUID = group.GUID;
profiledNode.GroupName = group.AnnotationText;
profiledNode.GroupExecutionOrderNumber = profiledGroup.GroupExecutionOrderNumber;
+ profiledNode.BackgroundBrush = profiledGroup.BackgroundBrush;
+ profiledNode.ShowGroupIndicator = ShowGroups;
groupDictionary[group.GUID].Add(profiledNode);
- }
+ }
}
// Executes for each group when a graph with groups is open while TuneUp is enabled
// Ensures that group nodes are sorted properly and do not appear at the bottom of the DataGrid
- ApplyDefaultSorting();
- RaisePropertyChanged(nameof(ProfiledNodesCollection));
- RaisePropertyChanged(nameof(ProfiledNodes));
+ ApplyCustomSorting(ProfiledNodesCollectionNotExecuted, SortByName);
}
private void CurrentWorkspaceModel_GroupRemoved(AnnotationModel group)
{
var groupGUID = group.GUID;
+ group.PropertyChanged -= OnGroupPropertyChanged;
+
// Remove the group from nodeDictionary and ProfiledNodes
- if (nodeDictionary.TryGetValue(groupGUID, out var profiledGroup))
+ if (nodeDictionary.Remove(groupGUID, out var profiledGroup))
{
- nodeDictionary.Remove(groupGUID);
- ProfiledNodes.Remove(profiledGroup);
+ MoveNodeToCollection(profiledGroup, null);
}
// Reset grouped nodes' properties and remove them from groupDictionary
- if (groupDictionary.TryGetValue(groupGUID, out var groupedNodes))
+ if (groupDictionary.Remove(groupGUID, out var groupedNodes))
{
foreach (var profiledNode in groupedNodes)
{
+ // Remove group total execution time node
+ if (profiledNode.IsGroupExecutionTime &&
+ executionTimeNodeDictionary.TryGetValue(groupGUID, out var execTimeNodeGUID))
+ {
+ MoveNodeToCollection(profiledNode, null);
+ nodeDictionary.Remove(execTimeNodeGUID);
+ }
+
+ // Reset properties for each grouped node
profiledNode.GroupGUID = Guid.Empty;
profiledNode.GroupName = string.Empty;
- // Immediately after the group is removed, the node's execution order should not
- // be displayed, but the nodes will remain in the same location in the DataGrid.
- // The execution order will update correctly on the next graph execution.
profiledNode.ExecutionOrderNumber = null;
profiledNode.GroupExecutionTime = TimeSpan.Zero;
}
- groupDictionary.Remove(groupGUID);
}
-
- RaisePropertyChanged(nameof(ProfiledNodesCollection));
- RaisePropertyChanged(nameof(ProfiledNodes));
+
+ //Recalculate the execution times
+ UpdateExecutionTime();
}
private void OnCurrentWorkspaceChanged(IWorkspaceModel workspace)
@@ -658,6 +825,219 @@ private void OnCurrentWorkspaceCleared(IWorkspaceModel workspace)
#endregion
+ #region Helpers
+
+ private ProfiledNodeViewModel CreateGroupTotalTimeNode(ProfiledNodeViewModel profiledGroup)
+ {
+ var groupTotalTimeNode = new ProfiledNodeViewModel(
+ ProfiledNodeViewModel.GroupExecutionTimeString, TimeSpan.Zero, ProfiledNodeState.NotExecuted)
+ {
+ GroupGUID = profiledGroup.GroupGUID,
+ GroupName = profiledGroup.GroupName,
+ BackgroundBrush = profiledGroup.BackgroundBrush,
+ IsGroupExecutionTime = true
+ };
+
+ var totalExecTimeGUID = Guid.NewGuid();
+ nodeDictionary[totalExecTimeGUID] = groupTotalTimeNode;
+ groupDictionary[profiledGroup.GroupGUID].Add(groupTotalTimeNode);
+ executionTimeNodeDictionary[profiledGroup.GroupGUID] = totalExecTimeGUID;
+
+ return groupTotalTimeNode;
+ }
+
+ private void UpdateGroupTotalTimeNodeProperties(ProfiledNodeViewModel groupTotalTimeNode, ProfiledNodeViewModel profiledGroup)
+ {
+ groupTotalTimeNode.State = profiledGroup.State;
+ groupTotalTimeNode.GroupExecutionTime = profiledGroup.GroupExecutionTime; // Accurate, for sorting
+ groupTotalTimeNode.ExecutionMilliseconds = profiledGroup.ExecutionMilliseconds; // Rounded, for display in UI
+ groupTotalTimeNode.GroupExecutionOrderNumber = profiledGroup.GroupExecutionOrderNumber;
+ groupTotalTimeNode.WasExecutedOnLastRun = true;
+
+ // Move node to the latest run collection
+ MoveNodeToCollection(groupTotalTimeNode, ProfiledNodesLatestRun);
+ }
+
+ ///
+ /// Refreshes the profiling node collection that contains a given node and updates the view.
+ ///
+ private void RefreshCollectionViewContainingNode(ProfiledNodeViewModel profiledNode)
+ {
+ if (ProfiledNodesLatestRun.Contains(profiledNode))
+ {
+ ProfiledNodesCollectionLatestRun.View.Refresh();
+ }
+ else if (ProfiledNodesPreviousRun.Contains(profiledNode))
+ {
+ ProfiledNodesCollectionPreviousRun.View.Refresh();
+ }
+ else if (ProfiledNodesNotExecuted.Contains(profiledNode))
+ {
+ ProfiledNodesCollectionNotExecuted.View.Refresh();
+ }
+ }
+
+ ///
+ /// Refreshes all profiling node collections and updates the view.
+ ///
+ private void RefreshAllCollectionViews()
+ {
+ ProfiledNodesCollectionLatestRun?.View?.Refresh();
+ ProfiledNodesCollectionPreviousRun?.View?.Refresh();
+ ProfiledNodesCollectionNotExecuted?.View?.Refresh();
+ }
+ ///
+ /// Notifies the system that all profiling node collections have changed,
+ /// triggering any necessary updates in the user interface.
+ ///
+ private void NotifyProfilingCollectionsChanged()
+ {
+ RaisePropertyChanged(nameof(ProfiledNodesLatestRun));
+ RaisePropertyChanged(nameof(ProfiledNodesPreviousRun));
+ RaisePropertyChanged(nameof(ProfiledNodesNotExecuted));
+ }
+
+ ///
+ /// Updates the group visibility, refreshes the collection view, and applies appropriate sorting for the given nodes.
+ ///
+ private void UpdateGroupsVisibility(CollectionViewSource collectionView, ObservableCollection nodes)
+ {
+ foreach (var node in nodes)
+ {
+ node.ShowGroupIndicator = showGroups;
+ }
+
+ if (collectionView?.View?.Cast().Any() == true)
+ {
+ string sortingOrder = collectionView == ProfiledNodesCollectionNotExecuted ? SortByName : null;
+ ApplyCustomSorting(collectionView, sortingOrder);
+ }
+ }
+
+ ///
+ /// Filters the collection of profiled nodes based on group and execution time criteria.
+ /// If ShowGroups is true, all nodes are accepted.
+ /// Otherwise, nodes where either IsGroup or IsExecutionTime is true are filtered out (not accepted).
+ ///
+ private void GroupNodeFilter(object sender, FilterEventArgs e)
+ {
+ var node = e.Item as ProfiledNodeViewModel;
+ if (node == null) return;
+
+ if (ShowGroups) e.Accepted = true;
+ else e.Accepted = !(node.IsGroup || node.IsGroupExecutionTime);
+ }
+
+ ///
+ /// Applies the GroupNodeFilter to all node collections by removing and re-adding the filter.
+ ///
+ private void ApplyGroupNodeFilter()
+ {
+ ProfiledNodesCollectionLatestRun.Filter -= GroupNodeFilter;
+ ProfiledNodesCollectionPreviousRun.Filter -= GroupNodeFilter;
+ ProfiledNodesCollectionNotExecuted.Filter -= GroupNodeFilter;
+
+ ProfiledNodesCollectionLatestRun.Filter += GroupNodeFilter;
+ ProfiledNodesCollectionPreviousRun.Filter += GroupNodeFilter;
+ ProfiledNodesCollectionNotExecuted.Filter += GroupNodeFilter;
+ }
+
+ ///
+ /// Applies the sorting logic to all ProfiledNodesCollections.
+ ///
+ public void ApplyCustomSorting()
+ {
+ ApplyCustomSorting(ProfiledNodesCollectionLatestRun);
+ ApplyCustomSorting(ProfiledNodesCollectionPreviousRun);
+ // Apply custom sorting to NotExecuted collection only if sortingOrder is "name"
+ if (defaultSortingOrder == SortByName)
+ {
+ ApplyCustomSorting(ProfiledNodesCollectionNotExecuted);
+ }
+ }
+
+ ///
+ /// Applies the sorting logic to a given ProfiledNodesCollection.
+ ///
+ public void ApplyCustomSorting(CollectionViewSource collection, string explicitSortingOrder = null)
+ {
+ // Use the sorting parameter if specified; otherwise, use the private sortingOrder
+ string sortBy = explicitSortingOrder ?? defaultSortingOrder;
+ collection.SortDescriptions.Clear();
+
+ switch (sortBy)
+ {
+ case SortByTime:
+ if (showGroups)
+ {
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.GroupExecutionTime), sortDirection));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.GroupGUID), sortDirection));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.IsGroup), ListSortDirection.Descending));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.IsGroupExecutionTime), ListSortDirection.Ascending));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.ExecutionTime), sortDirection));
+ }
+ else
+ {
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.ExecutionTime), sortDirection));
+ }
+ break;
+ case SortByName:
+ if (showGroups)
+ {
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.GroupName), sortDirection));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.GroupGUID), sortDirection));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.IsGroup), ListSortDirection.Descending));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.IsGroupExecutionTime), ListSortDirection.Ascending));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.Name), sortDirection));
+ }
+ else
+ {
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.Name), sortDirection));
+ }
+ break;
+ case SortByNumber:
+ if (showGroups)
+ {
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.GroupExecutionOrderNumber), sortDirection));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.GroupName), sortDirection));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.GroupGUID), sortDirection));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.IsGroup), ListSortDirection.Descending));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.IsGroupExecutionTime), ListSortDirection.Ascending));
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.ExecutionOrderNumber), sortDirection));
+ }
+ else
+ {
+ collection.SortDescriptions.Add(new SortDescription(nameof(ProfiledNodeViewModel.ExecutionOrderNumber), sortDirection));
+ }
+ break;
+ }
+ }
+
+ ///
+ /// Moves a node between collections, removing it from all collections and adding it to the target collection if provided.
+ ///
+ private void MoveNodeToCollection(ProfiledNodeViewModel profiledNode, ObservableCollection targetCollection)
+ {
+ System.Windows.Application.Current.Dispatcher.Invoke(() =>
+ {
+ var collections = new[]
+ {
+ ProfiledNodesLatestRun,
+ ProfiledNodesPreviousRun,
+ ProfiledNodesNotExecuted
+ };
+
+ foreach (var collection in collections)
+ {
+ collection?.Remove(profiledNode);
+ }
+
+ targetCollection?.Add(profiledNode);
+ });
+ }
+
+ #endregion
+
#region Dispose or setup
///
@@ -684,7 +1064,13 @@ private void ManageWorkspaceEvents(HomeWorkspaceModel workspace, bool subscribe)
{
node.NodeExecutionBegin += OnNodeExecutionBegin;
node.NodeExecutionEnd += OnNodeExecutionEnd;
+ node.PropertyChanged += OnNodePropertyChanged;
}
+ foreach (var group in workspace.Annotations)
+ {
+ group.PropertyChanged += OnGroupPropertyChanged;
+ }
+
ResetProfiledNodes();
}
// Unsubscribe to workspace events
@@ -701,9 +1087,14 @@ private void ManageWorkspaceEvents(HomeWorkspaceModel workspace, bool subscribe)
{
node.NodeExecutionBegin -= OnNodeExecutionBegin;
node.NodeExecutionEnd -= OnNodeExecutionEnd;
+ node.PropertyChanged -= OnNodePropertyChanged;
+ }
+ foreach (var group in workspace.Annotations)
+ {
+ group.PropertyChanged -= OnGroupPropertyChanged;
}
}
- executedNodesNum = 0;
+ executedNodesNum = 1;
}
///
@@ -718,15 +1109,14 @@ public void Dispose()
#endregion
- #region Export Node times
+ #region Exporters
///
- /// Exports the ProfiledNodesCollection to a CSV file.
+ /// Exports the ProfiledNodesCollections to a CSV file.
///
public void ExportToCsv()
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
- //saveFileDialog.RestoreDirectory = false;
saveFileDialog.Filter = "CSV file (*.csv)|*.csv|All files (*.*)|*.*";
if (saveFileDialog.ShowDialog() == true)
@@ -735,11 +1125,153 @@ public void ExportToCsv()
{
writer.WriteLine("Execution Order,Name,Execution Time (ms)");
- foreach (ProfiledNodeViewModel node in ProfiledNodesCollection.View.Cast())
+ var collections = new (string Label, CollectionViewSource Collection, string TotalTime)[]
+ {
+ ("Latest Run", ProfiledNodesCollectionLatestRun, LatestGraphExecutionTime),
+ ("Previous Run", ProfiledNodesCollectionPreviousRun, PreviousGraphExecutionTime),
+ ("Not Executed", ProfiledNodesCollectionNotExecuted, null)
+ };
+
+ foreach (var (label, collection, totalTime) in collections)
+ {
+ var nodes = collection.View.Cast().ToList();
+ if (!nodes.Any()) continue;
+
+ writer.WriteLine(label);
+
+ foreach (var node in nodes)
+ {
+ if (showGroups)
+ {
+ if (node.IsGroup || node.GroupGUID == Guid.Empty)
+ {
+ writer.WriteLine($"{node.GroupExecutionOrderNumber},{node.Name},{node.ExecutionMilliseconds}");
+ }
+ else
+ {
+ writer.WriteLine($",{node.Name},{node.ExecutionMilliseconds}");
+ }
+ }
+ else if (!node.IsGroup || !node.IsGroupExecutionTime)
+ {
+ writer.WriteLine($"{node.ExecutionOrderNumber},{node.Name},{node.ExecutionMilliseconds}");
+ }
+ }
+
+ // Write total execution time, if applicable
+ if (!string.IsNullOrEmpty(totalTime))
+ {
+ writer.WriteLine($",Total, {totalTime}");
+ }
+ writer.WriteLine();
+ }
+ }
+ }
+ }
+
+ ///
+ /// Exports the ProfiledNodesCollections to a JSON file.
+ ///
+ public void ExportToJson()
+ {
+ SaveFileDialog saveFileDialog = new SaveFileDialog
+ {
+ Filter = "JSON file (*.json)|*.json|All files (*.*)|*.*"
+ };
+
+ if (saveFileDialog.ShowDialog() != true) return;
+
+ var exportData = new List