diff --git a/TuneUp/ProfiledNodeViewModel.cs b/TuneUp/ProfiledNodeViewModel.cs index 09a9252..d29c192 100644 --- a/TuneUp/ProfiledNodeViewModel.cs +++ b/TuneUp/ProfiledNodeViewModel.cs @@ -60,7 +60,16 @@ internal set /// /// Indicates whether this node represents the total execution time for its group /// - public bool IsGroupExecutionTime => NodeModel == null && GroupModel == null; + public bool IsGroupExecutionTime + { + get => isGroupExecutionTime; + set + { + isGroupExecutionTime = value; + RaisePropertyChanged(nameof(IsGroupExecutionTime)); + } + } + private bool isGroupExecutionTime; /// /// Prefix string of execution time. @@ -228,6 +237,20 @@ public Guid GroupGUID } private Guid groupGIUD; + /// + /// The GUID of this node + /// + public Guid NodeGUID + { + get => nodeGIUD; + set + { + nodeGIUD = value; + RaisePropertyChanged(nameof(NodeGUID)); + } + } + private Guid nodeGIUD; + /// /// The name of the group to which this node belongs /// This property is also applied to individual nodes and is used when sorting by name @@ -246,7 +269,16 @@ public string GroupName /// /// Indicates if this node is a group /// - public bool IsGroup => NodeModel == null && GroupModel != null; + public bool IsGroup + { + get => isGroup; + set + { + isGroup = value; + RaisePropertyChanged(nameof(IsGroup)); + } + } + private bool isGroup; public bool ShowGroupIndicator { @@ -341,22 +373,6 @@ private static string GetOriginalName(NodeModel node) return nodeType.FullName; } - internal void ResetGroupProperties() - { - GroupGUID = Guid.Empty; - GroupName = string.Empty; - GroupExecutionOrderNumber = null; - GroupExecutionMilliseconds = 0; - } - - internal void ApplyGroupProperties(ProfiledNodeViewModel profiledGroup) - { - GroupGUID = profiledGroup.GroupGUID; - GroupName = profiledGroup.GroupName; - GroupExecutionOrderNumber = profiledGroup.GroupExecutionOrderNumber; - BackgroundBrush = profiledGroup.BackgroundBrush; - } - /// /// Create a Profiled Node View Model from a NodeModel /// @@ -366,6 +382,7 @@ public ProfiledNodeViewModel(NodeModel node) NodeModel = node; State = ProfiledNodeState.NotExecuted; Stopwatch = new Stopwatch(); + NodeGUID = node.GUID; } /// @@ -377,6 +394,8 @@ public ProfiledNodeViewModel(string name, ProfiledNodeState state) { this.Name = name; State = state; + NodeGUID = Guid.NewGuid(); + IsGroupExecutionTime = true; } /// @@ -391,6 +410,24 @@ public ProfiledNodeViewModel(AnnotationModel group) BackgroundBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(group.Background)); State = ProfiledNodeState.NotExecuted; ShowGroupIndicator = true; + NodeGUID = Guid.NewGuid(); + IsGroup = true; + } + + /// + /// An alternative constructor to create a group or time node from another profiled node. + /// + /// the annotation model + public ProfiledNodeViewModel(ProfiledNodeViewModel pNode) + { + Name = pNode.GroupName == DefaultGroupName ? DefaultDisplayGroupName : pNode.GroupName; + GroupName = pNode.GroupName; + State = pNode.State; + NodeGUID = Guid.NewGuid(); + GroupGUID = pNode.GroupGUID; + IsGroup = true; + BackgroundBrush = pNode.BackgroundBrush; + ShowGroupIndicator = true; } } } diff --git a/TuneUp/TuneUpWindow.xaml.cs b/TuneUp/TuneUpWindow.xaml.cs index 8a490c3..7239093 100644 --- a/TuneUp/TuneUpWindow.xaml.cs +++ b/TuneUp/TuneUpWindow.xaml.cs @@ -178,7 +178,7 @@ private void LatestRunTable_Sorting(object sender, DataGridSortingEventArgs e) { "#" => TuneUpWindowViewModel.SortByNumber, "Name" => TuneUpWindowViewModel.SortByName, - "Execution Time (ms)" => TuneUpWindowViewModel.SortByTime, + "Execution time (ms)" => TuneUpWindowViewModel.SortByTime, _ => viewModel.SortingOrder }; @@ -188,7 +188,7 @@ private void LatestRunTable_Sorting(object sender, DataGridSortingEventArgs e) : ListSortDirection.Ascending; // Apply custom sorting to ensure total times are at the bottom - viewModel.ApplyCustomSorting(); + viewModel.ApplyCustomSortingToAllCollections(); e.Handled = true; } } diff --git a/TuneUp/TuneUpWindowViewModel.cs b/TuneUp/TuneUpWindowViewModel.cs index 7909b18..4fd2944 100644 --- a/TuneUp/TuneUpWindowViewModel.cs +++ b/TuneUp/TuneUpWindowViewModel.cs @@ -69,8 +69,10 @@ public class TuneUpWindowViewModel : NotificationObject, IDisposable private string previousGraphExecutionTime = defaultExecutionTime; private string totalGraphExecutionTime = defaultExecutionTime; private Dictionary nodeDictionary = new Dictionary(); - private Dictionary> groupDictionary = new Dictionary>(); - private Dictionary executionTimeNodeDictionary = new Dictionary(); + private Dictionary groupDictionary = new Dictionary(); + // Maps AnnotationModel GUIDs to a list of associated ProfiledNodeViewModel instances. + private Dictionary> groupModelDictionary = new Dictionary>(); + private HomeWorkspaceModel CurrentWorkspace { get => currentWorkspace; @@ -303,75 +305,59 @@ internal void ResetProfiledNodes() { if (CurrentWorkspace == null) return; - // Clear existing collections if they are not null + // Clear existing collections ProfiledNodesLatestRun?.Clear(); ProfiledNodesPreviousRun?.Clear(); ProfiledNodesNotExecuted?.Clear(); - // Reset total times - LatestGraphExecutionTime = defaultExecutionTime; - PreviousGraphExecutionTime = defaultExecutionTime; - TotalGraphExecutionTime = defaultExecutionTime; + // Reset execution time stats + LatestGraphExecutionTime = PreviousGraphExecutionTime = 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>(); + groupDictionary = new Dictionary(); + groupModelDictionary = new Dictionary>(); - // Process groups and their nodes - foreach (var group in CurrentWorkspace.Annotations) + // Create a profiled node for each NodeModel + foreach (var node in CurrentWorkspace.Nodes) { - var groupGUID = group.GUID; - var groupBackgroundBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(group.Background)); + var profiledNode = new ProfiledNodeViewModel(node) { GroupName = node.Name }; + ProfiledNodesNotExecuted.Add(profiledNode); + nodeDictionary[node.GUID] = profiledNode; + } - // Create and add profiled group node - var profiledGroup = new ProfiledNodeViewModel(group) - { - BackgroundBrush = groupBackgroundBrush, - GroupGUID = groupGUID - }; - ProfiledNodesNotExecuted.Add(profiledGroup); - nodeDictionary[group.GUID] = profiledGroup; + // Create a profiled node for each AnnotationModel + foreach (var group in CurrentWorkspace.Annotations) + { + var pGroup = new ProfiledNodeViewModel(group); + ProfiledNodesNotExecuted.Add(pGroup); + groupDictionary[pGroup.NodeGUID] = (pGroup); + groupModelDictionary[group.GUID] = new List { pGroup }; - // Initialize group in group dictionary - groupDictionary[groupGUID] = new List(); + var groupedNodeGUIDs = group.Nodes.OfType().Select(n => n.GUID); - // Add each node in the group - foreach (var node in group.Nodes.OfType()) + foreach (var nodeGuid in groupedNodeGUIDs) { - var profiledNode = new ProfiledNodeViewModel(node) + if (nodeDictionary.TryGetValue(nodeGuid, out var pNode)) { - GroupGUID = groupGUID, - GroupName = group.AnnotationText, - BackgroundBrush = groupBackgroundBrush, - ShowGroupIndicator = ShowGroups - }; - ProfiledNodesNotExecuted.Add(profiledNode); - nodeDictionary[node.GUID] = profiledNode; - groupDictionary[groupGUID].Add(profiledNode); + ApplyGroupPropertiesAndRegisterNode(pNode, pGroup); + } } } - // 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); + // Refresh UI if any changes were made RaisePropertyChanged(nameof(ProfiledNodesCollectionNotExecuted)); + ApplyCustomSorting(ProfiledNodesCollectionNotExecuted, SortByName); + + ApplyGroupNodeFilter(); // Ensure table visibility is updated in case TuneUp was closed and reopened with the same graph. RaisePropertyChanged(nameof(LatestRunTableVisibility)); @@ -452,8 +438,7 @@ private void CurrentWorkspaceModel_EvaluationStarted(object sender, EventArgs e) // Move to CollectionPreviousRun if (node.State == ProfiledNodeState.ExecutedOnPreviousRun) { - MoveNodeToCollection(node, null); - ProfiledNodesPreviousRun.Add(node); + MoveNodeToCollection(node, ProfiledNodesPreviousRun); } } executedNodesNum = 1; @@ -509,7 +494,7 @@ private void UpdateExecutionTime() previousGraphExecutionTime = previousLatestRun.ToString(); totalGraphExecutionTime = (totalLatestRun + previousLatestRun).ToString(); }, null); - + RaisePropertyChanged(nameof(TotalGraphExecutionTime)); RaisePropertyChanged(nameof(LatestGraphExecutionTime)); RaisePropertyChanged(nameof(PreviousGraphExecutionTime)); @@ -522,83 +507,52 @@ private void UpdateExecutionTime() /// private void CalculateGroupNodes() { - int groupExecutionCounter = 1; - var processedNodes = new HashSet(); - var sortedProfiledNodes = ProfiledNodesLatestRun.OrderBy(node => node.ExecutionOrderNumber).ToList(); - - // Create lookup dictionaries - var annotationLookup = CurrentWorkspace.Annotations.ToDictionary(g => g.GUID); - - foreach (var profiledNode in sortedProfiledNodes) + // Clean the collections from all group and time nodes + foreach (var node in groupDictionary.Values) { - // Process nodes that belong to a group and have not been processed yet - if (!profiledNode.IsGroup && !profiledNode.IsGroupExecutionTime && profiledNode.GroupGUID != Guid.Empty && !processedNodes.Contains(profiledNode)) + RemoveNodeFromStateCollection(node, node.State); + + if (groupModelDictionary.TryGetValue(node.GroupGUID, out var groupNodes)) { - if (nodeDictionary.TryGetValue(profiledNode.GroupGUID, out var profiledGroup) && - groupDictionary.TryGetValue(profiledNode.GroupGUID, out var nodesInGroup)) - { - ProfiledNodeViewModel groupTotalTimeNode = null; - bool groupIsRenamed = false; + groupNodes.Remove(node); + } + } + groupDictionary.Clear(); - // Reset group state and execution time - profiledGroup.State = profiledNode.State; - profiledNode.GroupExecutionMilliseconds = 0; - MoveNodeToCollection(profiledGroup, ProfiledNodesLatestRun); // Ensure the profiledGroup is in latest run + // Create group and time nodes for latest and previous runs + CreateGroupNodesForCollection(ProfiledNodesLatestRun); + CreateGroupNodesForCollection(ProfiledNodesPreviousRun); - // Check if the group has been renamed - if (annotationLookup.TryGetValue(profiledGroup.GroupGUID, out var groupModel) && profiledGroup.GroupName != groupModel.AnnotationText) - { - groupIsRenamed = true; - profiledGroup.GroupName = groupModel.AnnotationText; - profiledGroup.Name = $"{ProfiledNodeViewModel.GroupNodePrefix}{groupModel.AnnotationText}"; - } + // Create group nodes for not executed + var processedNodesNotExecuted = new HashSet(); - // Iterate through the nodes in the group - foreach (var node in nodesInGroup) - { - // 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.GroupExecutionMilliseconds += node.ExecutionMilliseconds; - node.GroupExecutionOrderNumber = groupExecutionCounter; - node.ShowGroupIndicator = ShowGroups; - if (groupIsRenamed) - { - node.GroupName = profiledGroup.GroupName; - } - } - } + // Create a copy of ProfiledNodesNotExecuted to iterate over + var profiledNodesCopy = ProfiledNodesNotExecuted.ToList(); - // Update the properties of the group node - profiledGroup.GroupExecutionOrderNumber = groupExecutionCounter++; - profiledGroup.WasExecutedOnLastRun = true; + foreach (var pNode in profiledNodesCopy) + { + if (pNode.GroupGUID != Guid.Empty && !processedNodesNotExecuted.Contains(pNode)) + { + // get the other nodes from this group + var nodesInGroup = ProfiledNodesNotExecuted + .Where(n => n.GroupGUID == pNode.GroupGUID) + .ToList(); + foreach (var node in nodesInGroup) + { + processedNodesNotExecuted.Add(node); + } - // Create and add group total execution time node if it doesn't exist - groupTotalTimeNode ??= CreateGroupTotalTimeNode(profiledGroup); + // create new group node + var pGroup = new ProfiledNodeViewModel(pNode); - // Update the properties of the groupTotalTimeNode and move to latestRunCollection - UpdateGroupTotalTimeNodeProperties(groupTotalTimeNode, profiledGroup); + groupDictionary[pGroup.NodeGUID] = pGroup; + groupModelDictionary[pNode.GroupGUID].Add(pGroup); - // Update the groupExecutionTime for all nodes of the group for the purposes of sorting - foreach (var node in nodesInGroup) - { - node.GroupExecutionMilliseconds = profiledGroup.GroupExecutionMilliseconds; - } - } - } - // Process standalone nodes - else if (!profiledNode.IsGroup && processedNodes.Add(profiledNode) && - !profiledNode.Name.Contains(ProfiledNodeViewModel.ExecutionTimelString) && - !profiledNode.IsGroupExecutionTime) - { - profiledNode.GroupExecutionOrderNumber = groupExecutionCounter++; - profiledNode.GroupExecutionMilliseconds = profiledNode.ExecutionMilliseconds; + System.Windows.Application.Current.Dispatcher.Invoke(() => + { + ProfiledNodesNotExecuted.Add(pGroup); + }); } } @@ -609,6 +563,72 @@ private void CalculateGroupNodes() ApplyCustomSorting(ProfiledNodesCollectionLatestRun); RaisePropertyChanged(nameof(ProfiledNodesCollectionLatestRun)); }); + ProfiledNodesCollectionPreviousRun.Dispatcher.Invoke(() => + { + ApplyCustomSorting(ProfiledNodesCollectionPreviousRun); + RaisePropertyChanged(nameof(ProfiledNodesCollectionPreviousRun)); + }); + } + + private void CreateGroupNodesForCollection(ObservableCollection collection) + { + int executionCounter = 1; + var processedNodes = new HashSet(); + + var sortedNodes = collection.OrderBy(n => n.ExecutionOrderNumber).ToList(); + + foreach (var pNode in sortedNodes) + { + // Process the standalone nodes + if (pNode.GroupGUID == Guid.Empty && !processedNodes.Contains(pNode)) + { + pNode.GroupExecutionMilliseconds = pNode.ExecutionMilliseconds; + pNode.ExecutionOrderNumber = executionCounter; + pNode.GroupExecutionOrderNumber = executionCounter++; + + processedNodes.Add(pNode); + } + + // Process the grouped nodes + else if (pNode.GroupGUID != Guid.Empty && !processedNodes.Contains(pNode)) + { + // Get all nodes in the same group and calculate the group execution time + int groupExecTime = 0; + var nodesInGroup = sortedNodes.Where(n => n.GroupGUID == pNode.GroupGUID).ToList(); + + foreach (var node in nodesInGroup) + { + processedNodes.Add(node); + groupExecTime += node.ExecutionMilliseconds; + } + + // Create and register a new group node using the current profiled node + var pGroup = new ProfiledNodeViewModel(pNode) + { + GroupExecutionOrderNumber = executionCounter++, + GroupExecutionMilliseconds = groupExecTime + }; + + groupDictionary[pGroup.NodeGUID] = pGroup; + groupModelDictionary[pNode.GroupGUID].Add(pGroup); + + // Create an register a new time node + var timeNode = CreateAndRegisterGroupTimeNode(pGroup); + + System.Windows.Application.Current.Dispatcher.Invoke(() => + { + collection.Add(timeNode); + collection.Add(pGroup); + }); + + // Update group-related properties for all nodes in the group + foreach (var node in nodesInGroup) + { + node.GroupExecutionOrderNumber = pGroup.GroupExecutionOrderNumber; + node.GroupExecutionMilliseconds = pGroup.GroupExecutionMilliseconds; + } + } + } } internal void OnNodeExecutionBegin(NodeModel nm) @@ -667,100 +687,173 @@ internal void OnNodePropertyChanged(object sender, PropertyChangedEventArgs e) internal void OnGroupPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (sender is AnnotationModel groupModel && nodeDictionary.TryGetValue(groupModel.GUID, out var profiledGroup)) + if (sender is AnnotationModel groupModel && groupModelDictionary.TryGetValue(groupModel.GUID, out var nodesInGroup)) { - bool hasChanges = false; + bool isRenamed = false; + ObservableCollection collection = null; // 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]) + foreach (var pNode in nodesInGroup) { - profiledNode.GroupName = groupModel.AnnotationText; + if (pNode.IsGroup) + { + pNode.Name = $"{ProfiledNodeViewModel.GroupNodePrefix}{groupModel.AnnotationText}"; + } + pNode.GroupName = groupModel.AnnotationText; } - hasChanges = true; + isRenamed = 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]) + foreach (var pNode in nodesInGroup) { - profiledNode.BackgroundBrush = newBackgroundBrush; + var newBackgroundBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(groupModel.Background)); + pNode.BackgroundBrush = newBackgroundBrush; } - hasChanges = true; } - // Detect if a node is removed from the group if (e.PropertyName == nameof(groupModel.Nodes)) { - var existingProfiledNodesInGroup = groupDictionary[groupModel.GUID].ToList(); - var currentGroupNodeGuids = groupModel.Nodes + var allNodesInGroup = groupModelDictionary[groupModel.GUID]; + + var modelNodeGuids = groupModel.Nodes .OfType() - .Select(node => node.GUID) - .ToList(); + .Select(n => n.GUID) + .ToHashSet(); - // REMOVE nodes that are no longer in the group - var profiledNodesToRemove = existingProfiledNodesInGroup - .Where(profiledNode => !profiledNode.IsGroupExecutionTime && !currentGroupNodeGuids.Contains(profiledNode.NodeModel.GUID)) - .ToList(); + // Determine if we adding or removing a node + var pNodeToRemove = allNodesInGroup + .FirstOrDefault(n => !n.IsGroup && !n.IsGroupExecutionTime && !modelNodeGuids.Contains(n.NodeGUID)); - foreach (var profiledNode in profiledNodesToRemove) - { - profiledNode.ResetGroupProperties(); - existingProfiledNodesInGroup.Remove(profiledNode); - groupDictionary[groupModel.GUID].Remove(profiledNode); - } + var pNodeToAdd = nodeDictionary.FirstOrDefault(kvp => modelNodeGuids.Contains(kvp.Key) && !allNodesInGroup.Contains(kvp.Value)).Value; - // ADD new nodes that are in the updated group but not in the logged group - var profiledNodesToAdd = nodeDictionary - .Where(kvp => currentGroupNodeGuids.Contains(kvp.Key) && !existingProfiledNodesInGroup.Contains(kvp.Value)) - .Select(kvp => kvp.Value) - .ToList(); + var (pNodeModified, addNode) = pNodeToRemove == null ? (pNodeToAdd, true) : (pNodeToRemove, false); + + // Safety check + if (pNodeModified == null) return; - foreach (var profiledNode in profiledNodesToAdd) + var state = pNodeModified.State; + collection = GetObservableCollectionFromState(state); + + // Get all nodes for this group in the same state + var allNodesInGroupForState = allNodesInGroup.Where(n => n.State == state).ToList(); + var pGroupToModify = allNodesInGroupForState.FirstOrDefault(n => n.IsGroup); + var timeNodeToModify = allNodesInGroupForState.FirstOrDefault(n => n.IsGroupExecutionTime); + var pNodesOfSameState = allNodesInGroupForState.Where(n => !n.IsGroupExecutionTime && !n.IsGroup).ToList(); + + // Case REMOVE + if (!addNode) { - profiledNode.ApplyGroupProperties(profiledGroup); - profiledNode.ShowGroupIndicator = ShowGroups; - existingProfiledNodesInGroup.Add(profiledNode); - groupDictionary[groupModel.GUID].Add(profiledNode); + ResetGroupPropertiesAndUnregisterNode(pNodeModified); + pNodesOfSameState.Remove(pNodeModified); + + // Update group execution time + if (state != ProfiledNodeState.NotExecuted && pGroupToModify != null && timeNodeToModify != null) + { + pGroupToModify.GroupExecutionMilliseconds -= pNodeModified.ExecutionMilliseconds; + pGroupToModify.ExecutionMilliseconds = pGroupToModify.GroupExecutionMilliseconds; + timeNodeToModify.GroupExecutionMilliseconds = pGroupToModify.GroupExecutionMilliseconds; + timeNodeToModify.ExecutionMilliseconds = pGroupToModify.GroupExecutionMilliseconds; + } } + // Case ADD + else + { + // Create a new group node if it doesn't exist for this state + if (pGroupToModify == null) + { + pGroupToModify = new ProfiledNodeViewModel(groupModel) { State = state }; + collection.Add(pGroupToModify); + groupDictionary[pGroupToModify.NodeGUID] = pGroupToModify; + groupModelDictionary[groupModel.GUID].Add(pGroupToModify); + + if (timeNodeToModify == null) + { + timeNodeToModify = CreateAndRegisterGroupTimeNode(pGroupToModify); + } + } - // Update group execution time - var totalExecutionMilliseconds = existingProfiledNodesInGroup - .Where(n => !n.IsGroupExecutionTime) - .Sum(n => n.ExecutionMilliseconds); + ApplyGroupPropertiesAndRegisterNode(pNodeModified, pGroupToModify); - profiledGroup.ExecutionMilliseconds = profiledGroup.GroupExecutionMilliseconds = totalExecutionMilliseconds; + // Update execution time if necessary + if (state != ProfiledNodeState.NotExecuted) + { + pGroupToModify.GroupExecutionMilliseconds += pNodeModified.ExecutionMilliseconds; + pGroupToModify.ExecutionMilliseconds = pGroupToModify.GroupExecutionMilliseconds; + timeNodeToModify.GroupExecutionMilliseconds = pGroupToModify.GroupExecutionMilliseconds; + timeNodeToModify.ExecutionMilliseconds = pGroupToModify.GroupExecutionMilliseconds; + } + } - // update the grouped nodes - foreach (var profiledNode in existingProfiledNodesInGroup) + // Update execution time for all nodes in the same state + if (state != ProfiledNodeState.NotExecuted) { - profiledNode.GroupExecutionMilliseconds = totalExecutionMilliseconds; - if (profiledNode.IsGroupExecutionTime) + foreach (var pNode in pNodesOfSameState) { - profiledNode.ExecutionMilliseconds = totalExecutionMilliseconds; + pNode.GroupExecutionMilliseconds = pGroupToModify.GroupExecutionMilliseconds; + if (pNode.IsGroupExecutionTime) + { + pNode.ExecutionMilliseconds = pGroupToModify.GroupExecutionMilliseconds; + } } } - hasChanges = true; + // Reset the group execution order + UpdateGroupExecutionOrders(collection); } - // Refresh UI if any changes were made - if (hasChanges) + // Refresh UI if any changes were made. + // Changes to the group background do not require a full UI refresh. + if (isRenamed) { - NotifyProfilingCollectionsChanged(); - // Refresh all collections as a group may contain nodes from multiple collections. RefreshAllCollectionViews(); } + if (collection != null) + { + SortCollectionViewForProfiledNodesCollection(collection); + } + } + } + + /// + /// Reorders the nodes in the collection by their execution order number, + /// ensuring that nodes in the same group receive the same execution order. + /// + private void UpdateGroupExecutionOrders(ObservableCollection collection) + { + var pNodesOfCollection = collection + .Where(n => !n.IsGroup && !n.IsGroupExecutionTime) + .OrderBy(n => n.ExecutionOrderNumber); + + int newExecutionCounter = 1; + var processedNodes = new HashSet(); + + foreach (var pNode in pNodesOfCollection) + { + if (!processedNodes.Contains(pNode)) + { + if (pNode.GroupGUID != Guid.Empty) + { + var pNodesOfGroup = collection.Where(n => n.GroupGUID == pNode.GroupGUID); + + foreach (var pNodeInGroup in pNodesOfGroup) + { + pNodeInGroup.GroupExecutionOrderNumber = newExecutionCounter; + processedNodes.Add(pNodeInGroup); + } + } + else + { + pNode.GroupExecutionOrderNumber = newExecutionCounter; + processedNodes.Add(pNode); + } + + newExecutionCounter++; + } } } @@ -790,78 +883,124 @@ private void CurrentWorkspaceModel_NodeRemoved(NodeModel node) node.NodeExecutionEnd -= OnNodeExecutionEnd; node.PropertyChanged -= OnNodePropertyChanged; - MoveNodeToCollection(profiledNode, null); + RemoveNodeFromStateCollection(profiledNode, profiledNode.State); + //Recalculate the execution times UpdateExecutionTime(); } private void CurrentWorkspaceModel_GroupAdded(AnnotationModel group) { - var profiledGroup = new ProfiledNodeViewModel(group); - nodeDictionary[group.GUID] = 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) + var groupGUID = group.GUID; + var pNodesInGroup = new List(); + + // Initialize the group in the dictionary + groupModelDictionary[groupGUID] = new List(); + + // Create or retrieve profiled nodes for each NodeModel in the group + foreach (var nodeModel in group.Nodes.OfType()) { - if (node is NodeModel nodeModel) + if (!nodeDictionary.TryGetValue(nodeModel.GUID, out var pNode)) { - ProfiledNodeViewModel profiledNode; - if (nodeDictionary.TryGetValue(node.GUID, out profiledNode)) - { - profiledGroup.State = profiledNode.State; - } - else + pNode = new ProfiledNodeViewModel(nodeModel); + nodeDictionary[nodeModel.GUID] = pNode; + ProfiledNodesNotExecuted.Add(pNode); + } + pNodesInGroup.Add(pNode); + } + + // Group profiled nodes by state and sort by execution order + var groupedNodesInGroup = pNodesInGroup + .Where(n => !n.IsGroupExecutionTime) + .GroupBy(n => n.State); + + // Process each group of nodes by state + foreach (var stateGroup in groupedNodesInGroup) + { + var state = stateGroup.Key; + var collection = GetObservableCollectionFromState(state); + + // Create and log new group node + var pGroup = new ProfiledNodeViewModel(group) { State = state }; + groupModelDictionary[groupGUID].Add(pGroup); + groupDictionary[pGroup.NodeGUID] = pGroup; + collection.Add(pGroup); + + // Accumulate execution times and create a time node + if (collection != ProfiledNodesNotExecuted) + { + int groupExecutionTime = 0; + foreach (var pNode in stateGroup) { - profiledNode = new ProfiledNodeViewModel(node as NodeModel); - nodeDictionary[node.GUID] = profiledNode; - ProfiledNodesNotExecuted.Add(profiledNode); + groupExecutionTime += pNode.ExecutionMilliseconds; + pNode.GroupExecutionOrderNumber = null; } - profiledNode.ApplyGroupProperties(profiledGroup); - profiledNode.ShowGroupIndicator = ShowGroups; - groupDictionary[group.GUID].Add(profiledNode); + pGroup.GroupExecutionMilliseconds = groupExecutionTime; + + // Create and register time node + var timeNode = CreateAndRegisterGroupTimeNode(pGroup); + collection.Add(timeNode); } + + // Apply group properties + foreach (var pNode in stateGroup) + { + ApplyGroupPropertiesAndRegisterNode(pNode, pGroup); + } + + // Update the group execution order in the collection + UpdateGroupExecutionOrders(collection); } - // 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 + + // Ensure new group nodes are sorted properly ApplyCustomSorting(ProfiledNodesCollectionNotExecuted, SortByName); } private void CurrentWorkspaceModel_GroupRemoved(AnnotationModel group) { + group.PropertyChanged -= OnGroupPropertyChanged; + var groupGUID = group.GUID; + groupModelDictionary.TryGetValue(groupGUID, out var allNodes); - group.PropertyChanged -= OnGroupPropertyChanged; + var pNodes = new List(); + var gNodes = new List(); + var states = new HashSet(); - // Remove the group from nodeDictionary and ProfiledNodes - if (nodeDictionary.Remove(groupGUID, out var profiledGroup)) + foreach (var node in allNodes) { - MoveNodeToCollection(profiledGroup, null); + if (node.IsGroup || node.IsGroupExecutionTime) gNodes.Add(node); + else pNodes.Add(node); + + states.Add(node.State); } - // Reset grouped nodes' properties and remove them from groupDictionary - if (groupDictionary.Remove(groupGUID, out var groupedNodes)) + // Remove the entire entry from the groupModelDictionary + groupModelDictionary.Remove(groupGUID); + + // Remove the group and time nodes + foreach (var node in gNodes) { - 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); - } + RemoveNodeFromStateCollection(node, node.State); + groupDictionary.Remove(node.NodeGUID); + } - // Reset properties for each grouped node - profiledNode.ResetGroupProperties(); - } + // Reset the properties of each pNode + foreach (var node in pNodes) + { + ResetGroupPropertiesAndUnregisterNode(node); } - //Recalculate the execution times - UpdateExecutionTime(); + // Reset the group execution order in the collection based on the affected states + foreach (var state in states) + { + var collection = GetObservableCollectionFromState(state); + UpdateGroupExecutionOrders(collection); + } + + RefreshAllCollectionViews(); } private void OnCurrentWorkspaceChanged(IWorkspaceModel workspace) @@ -882,34 +1021,60 @@ private void OnCurrentWorkspaceCleared(IWorkspaceModel workspace) #region Helpers - private ProfiledNodeViewModel CreateGroupTotalTimeNode(ProfiledNodeViewModel profiledGroup) + /// + /// Resets group-related properties of the node and unregisters it from the group model dictionary. + /// + internal void ResetGroupPropertiesAndUnregisterNode(ProfiledNodeViewModel profiledNode) { - var groupTotalTimeNode = new ProfiledNodeViewModel( - ProfiledNodeViewModel.GroupExecutionTimeString, ProfiledNodeState.NotExecuted) + if (groupModelDictionary.TryGetValue(profiledNode.GroupGUID, out var groupNodes)) { - GroupGUID = profiledGroup.GroupGUID, - GroupName = profiledGroup.GroupName, - BackgroundBrush = profiledGroup.BackgroundBrush, - ShowGroupIndicator = true - }; + groupNodes.Remove(profiledNode); + } - var totalExecTimeGUID = Guid.NewGuid(); - nodeDictionary[totalExecTimeGUID] = groupTotalTimeNode; - groupDictionary[profiledGroup.GroupGUID].Add(groupTotalTimeNode); - executionTimeNodeDictionary[profiledGroup.GroupGUID] = totalExecTimeGUID; + profiledNode.GroupGUID = Guid.Empty; + profiledNode.GroupName = profiledNode.Name; + profiledNode.GroupExecutionMilliseconds = 0; + profiledNode.GroupExecutionOrderNumber = null; + profiledNode.ShowGroupIndicator = false; + } - return groupTotalTimeNode; + /// + /// Applies group properties to the profiled node and registers it in the group model dictionary. + /// + internal void ApplyGroupPropertiesAndRegisterNode(ProfiledNodeViewModel profiledNode, ProfiledNodeViewModel profiledGroup) + { + profiledNode.GroupGUID = profiledGroup.GroupGUID; + profiledNode.GroupName = profiledGroup.GroupName; + profiledNode.BackgroundBrush = profiledGroup.BackgroundBrush; + profiledNode.GroupExecutionMilliseconds = profiledGroup.GroupExecutionMilliseconds; + profiledNode.GroupExecutionOrderNumber = profiledGroup.GroupExecutionOrderNumber; + profiledNode.ShowGroupIndicator = ShowGroups; + + if (groupModelDictionary.TryGetValue(profiledNode.GroupGUID, out var nodeList) && !nodeList.Contains(profiledNode)) + { + nodeList.Add(profiledNode); + } } - private void UpdateGroupTotalTimeNodeProperties(ProfiledNodeViewModel groupTotalTimeNode, ProfiledNodeViewModel profiledGroup) + /// + /// Creates and registers a group time node with execution time details. + /// + private ProfiledNodeViewModel CreateAndRegisterGroupTimeNode(ProfiledNodeViewModel pNode) { - groupTotalTimeNode.State = profiledGroup.State; - groupTotalTimeNode.GroupExecutionMilliseconds = groupTotalTimeNode.ExecutionMilliseconds = profiledGroup.GroupExecutionMilliseconds; - groupTotalTimeNode.GroupExecutionOrderNumber = profiledGroup.GroupExecutionOrderNumber; - groupTotalTimeNode.WasExecutedOnLastRun = true; + var timeNode = new ProfiledNodeViewModel(pNode) + { + Name = ProfiledNodeViewModel.GroupExecutionTimeString, + IsGroup = false, + IsGroupExecutionTime = true, + ExecutionMilliseconds = pNode.GroupExecutionMilliseconds, + GroupExecutionMilliseconds = pNode.GroupExecutionMilliseconds, + GroupExecutionOrderNumber = pNode.GroupExecutionOrderNumber + }; + + groupDictionary[timeNode.NodeGUID] = timeNode; + groupModelDictionary[timeNode.GroupGUID].Add(timeNode); - // Move node to the latest run collection - MoveNodeToCollection(groupTotalTimeNode, ProfiledNodesLatestRun); + return timeNode; } /// @@ -917,39 +1082,38 @@ private void UpdateGroupTotalTimeNodeProperties(ProfiledNodeViewModel groupTotal /// 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)) + switch (profiledNode.State) { - ProfiledNodesCollectionNotExecuted.View.Refresh(); + case ProfiledNodeState.ExecutedOnCurrentRun: + ProfiledNodesCollectionLatestRun.View.Refresh(); + break; + case ProfiledNodeState.ExecutedOnPreviousRun: + ProfiledNodesCollectionPreviousRun.View.Refresh(); + break; + case ProfiledNodeState.NotExecuted: + ProfiledNodesCollectionNotExecuted.View.Refresh(); + break; } } /// - /// Refreshes all profiling node collections and updates the view. + /// Returns the appropriate ObservableCollection based on the node's profiling state. /// - private void RefreshAllCollectionViews() + private ObservableCollection GetObservableCollectionFromState(ProfiledNodeState state) { - ProfiledNodesCollectionLatestRun?.View?.Refresh(); - ProfiledNodesCollectionPreviousRun?.View?.Refresh(); - ProfiledNodesCollectionNotExecuted?.View?.Refresh(); + if (state == ProfiledNodeState.ExecutedOnCurrentRun) return ProfiledNodesLatestRun; + else if (state == ProfiledNodeState.ExecutedOnPreviousRun) return ProfiledNodesPreviousRun; + else return ProfiledNodesNotExecuted; } /// - /// Notifies the system that all profiling node collections have changed, - /// triggering any necessary updates in the user interface. + /// Refreshes all profiling node collections and updates the view. /// - private void NotifyProfilingCollectionsChanged() + private void RefreshAllCollectionViews() { - RaisePropertyChanged(nameof(ProfiledNodesLatestRun)); - RaisePropertyChanged(nameof(ProfiledNodesPreviousRun)); - RaisePropertyChanged(nameof(ProfiledNodesNotExecuted)); + ProfiledNodesCollectionLatestRun?.View?.Refresh(); + ProfiledNodesCollectionPreviousRun?.View?.Refresh(); + ProfiledNodesCollectionNotExecuted?.View?.Refresh(); } /// @@ -1000,7 +1164,7 @@ private void ApplyGroupNodeFilter() /// /// Applies the sorting logic to all ProfiledNodesCollections. /// - public void ApplyCustomSorting() + public void ApplyCustomSortingToAllCollections() { ApplyCustomSorting(ProfiledNodesCollectionLatestRun); ApplyCustomSorting(ProfiledNodesCollectionPreviousRun); @@ -1068,6 +1232,27 @@ public void ApplyCustomSorting(CollectionViewSource collection, string explicitS } } + /// + /// Sorts the appropriate collection view based on the provided observable collection of profiled nodes. + /// + private void SortCollectionViewForProfiledNodesCollection(ObservableCollection collection) + { + if (collection == null) return; + + switch (collection) + { + case var _ when collection == ProfiledNodesLatestRun: + ApplyCustomSorting(ProfiledNodesCollectionLatestRun); + break; + case var _ when collection == ProfiledNodesPreviousRun: + ApplyCustomSorting(ProfiledNodesCollectionPreviousRun); + break; + default: + ApplyCustomSorting(ProfiledNodesCollectionNotExecuted, SortByName); + break; + } + } + /// /// Moves a node between collections, removing it from all collections and adding it to the target collection if provided. /// @@ -1091,6 +1276,19 @@ private void MoveNodeToCollection(ProfiledNodeViewModel profiledNode, Observable }); } + /// + /// Removes a node from the appropriate collection based on its state. + /// + private void RemoveNodeFromStateCollection(ProfiledNodeViewModel pNode, ProfiledNodeState state) + { + var collection = GetObservableCollectionFromState(state); + + System.Windows.Application.Current.Dispatcher.Invoke(() => + { + collection?.Remove(pNode); + }); + } + #endregion #region Dispose or setup