diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b4d260..1567a20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Fixed - +- [#450] Improved performance when a large number of object change events are generated (e.g. when exiting animation + mode) - [#441] Fixed an issue where the preview object pickable status could get out of sync with the original ### Changed diff --git a/Editor/ChangeStream/ChangeStreamMonitor.cs b/Editor/ChangeStream/ChangeStreamMonitor.cs index 9ba6627..d77b260 100644 --- a/Editor/ChangeStream/ChangeStreamMonitor.cs +++ b/Editor/ChangeStream/ChangeStreamMonitor.cs @@ -27,24 +27,28 @@ private static void OnChange(ref ObjectChangeEventStream stream) Profiler.BeginSample("ChangeStreamMonitor.OnChange"); int length = stream.length; - for (int i = 0; i < length; i++) + + using (ObjectWatcher.Instance.Hierarchy.SuspendEvents()) { - try + for (int i = 0; i < length; i++) { - _handleEventSampler.Begin(); + try + { + _handleEventSampler.Begin(); - HandleEvent(stream, i); - } - catch (Exception e) - { - Debug.LogError($"Error handling event {i}: {e}"); - } - finally - { - _handleEventSampler.End(); + HandleEvent(stream, i); + } + catch (Exception e) + { + Debug.LogError($"Error handling event {i}: {e}"); + } + finally + { + _handleEventSampler.End(); + } } } - + Profiler.BeginSample("ComputeContext.FlushInvalidates"); ComputeContext.FlushInvalidates(); Profiler.EndSample(); @@ -52,23 +56,29 @@ private static void OnChange(ref ObjectChangeEventStream stream) Profiler.EndSample(); } - private static void HandleEvent(ObjectChangeEventStream stream, int i) + private static TraceScope OpenTrace(ObjectChangeEventStream stream, int i) { - var trace = TraceBuffer.RecordTraceEvent( + return TraceBuffer.RecordTraceEvent( "ChangeStreamMonitor.HandleEvent", - (ev) => $"Handling event {ev.Arg0}", + ev => $"Handling event {ev.Arg0}", stream.GetEventType(i), level: TraceEventLevel.Trace - ); - - using (trace.Scope()) + ).Scope(); + } + + private static void HandleEvent(ObjectChangeEventStream stream, int i) + { switch (stream.GetEventType(i)) { case ObjectChangeKind.None: break; case ObjectChangeKind.ChangeScene: { - ObjectWatcher.Instance.Hierarchy.InvalidateAll(); + using (OpenTrace(stream, i)) + using (new ProfilerScope("ChangeScene")) + { + ObjectWatcher.Instance.Hierarchy.InvalidateAll(); + } break; } @@ -77,7 +87,12 @@ private static void HandleEvent(ObjectChangeEventStream stream, int i) { stream.GetCreateGameObjectHierarchyEvent(i, out var data); - ObjectWatcher.Instance.Hierarchy.FireGameObjectCreate(data.instanceId); + using (OpenTrace(stream, i)) + using (new ProfilerScope("CreateGameObjectHierarchy")) + { + ObjectWatcher.Instance.Hierarchy.FireGameObjectCreate(data.instanceId); + } + break; } @@ -85,7 +100,11 @@ private static void HandleEvent(ObjectChangeEventStream stream, int i) { stream.GetChangeGameObjectStructureHierarchyEvent(i, out var data); - OnChangeGameObjectStructureHierarchy(data); + using (OpenTrace(stream, i)) + using (new ProfilerScope("OnChangeGameObjectStructHierarchy")) + { + OnChangeGameObjectStructureHierarchy(data); + } break; } @@ -93,7 +112,12 @@ private static void HandleEvent(ObjectChangeEventStream stream, int i) case ObjectChangeKind.ChangeGameObjectStructure: // add/remove components { stream.GetChangeGameObjectStructureEvent(i, out var data); - OnChangeGameObjectStructure(data); + + using (OpenTrace(stream, i)) + using (new ProfilerScope("OnChangeGameObjectStructure")) + { + OnChangeGameObjectStructure(data); + } break; } @@ -101,7 +125,12 @@ private static void HandleEvent(ObjectChangeEventStream stream, int i) case ObjectChangeKind.ChangeGameObjectParent: { stream.GetChangeGameObjectParentEvent(i, out var data); - OnChangeGameObjectParent(data); + + using (OpenTrace(stream, i)) + using (new ProfilerScope("OnChangeGameObjectParent")) + { + OnChangeGameObjectParent(data); + } break; } @@ -109,7 +138,12 @@ private static void HandleEvent(ObjectChangeEventStream stream, int i) case ObjectChangeKind.ChangeGameObjectOrComponentProperties: { stream.GetChangeGameObjectOrComponentPropertiesEvent(i, out var data); - OnChangeGameObjectOrComponentProperties(data); + + using (OpenTrace(stream, i)) + using (new ProfilerScope("OnChangeGameObjectOrComponentProperties")) + { + OnChangeGameObjectOrComponentProperties(data); + } break; } @@ -117,7 +151,12 @@ private static void HandleEvent(ObjectChangeEventStream stream, int i) case ObjectChangeKind.DestroyGameObjectHierarchy: { stream.GetDestroyGameObjectHierarchyEvent(i, out var data); - OnDestroyGameObjectHierarchy(data); + + using (OpenTrace(stream, i)) + using (new ProfilerScope("OnDestroyGameObjectHierarchy")) + { + OnDestroyGameObjectHierarchy(data); + } break; } @@ -126,7 +165,12 @@ private static void HandleEvent(ObjectChangeEventStream stream, int i) case ObjectChangeKind.DestroyAssetObject: { stream.GetDestroyAssetObjectEvent(i, out var data); - OnDestroyAssetObject(data); + + using (OpenTrace(stream, i)) + using (new ProfilerScope("OnDestroyAssetObject")) + { + OnDestroyAssetObject(data); + } break; } @@ -134,7 +178,12 @@ private static void HandleEvent(ObjectChangeEventStream stream, int i) case ObjectChangeKind.ChangeAssetObjectProperties: { stream.GetChangeAssetObjectPropertiesEvent(i, out var data); - OnChangeAssetObjectProperties(data); + + using (OpenTrace(stream, i)) + using (new ProfilerScope("OnChangeAssetObjectProperties")) + { + OnChangeAssetObjectProperties(data); + } break; } @@ -142,7 +191,12 @@ private static void HandleEvent(ObjectChangeEventStream stream, int i) case ObjectChangeKind.UpdatePrefabInstances: { stream.GetUpdatePrefabInstancesEvent(i, out var data); - OnUpdatePrefabInstances(data); + + using (OpenTrace(stream, i)) + using (new ProfilerScope("OnUpdatePrefabInstances")) + { + OnUpdatePrefabInstances(data); + } break; } @@ -150,7 +204,12 @@ private static void HandleEvent(ObjectChangeEventStream stream, int i) case ObjectChangeKind.ChangeChildrenOrder: { stream.GetChangeChildrenOrderEvent(i, out var data); - OnChangeChildrenOrder(data); + + using (OpenTrace(stream, i)) + using (new ProfilerScope("OnChangeChildrenOrder")) + { + OnChangeChildrenOrder(data); + } break; } diff --git a/Editor/ChangeStream/ShadowGameObject.cs b/Editor/ChangeStream/ShadowGameObject.cs index ab2fad3..ece43b1 100644 --- a/Editor/ChangeStream/ShadowGameObject.cs +++ b/Editor/ChangeStream/ShadowGameObject.cs @@ -7,8 +7,10 @@ using System.Collections.Generic; using System.Threading; using nadena.dev.ndmf.preview; +using nadena.dev.ndmf.preview.trace; using UnityEditor; using UnityEngine; +using UnityEngine.Profiling; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; @@ -57,6 +59,152 @@ internal class ShadowHierarchy internal Dictionary _otherObjects = new(); internal ListenerSet _rootSetListener = new(); + private struct PendingEvent + { + public bool ObjectDirty; + public long ObjectDirtyTraceEvent; + public bool PathChange; + public long PathChangeTraceEvent; + public bool SelfComponentsChanged; + public long SelfComponentsChangedTraceEvent; + public bool ChildComponentsChanged; + public long ChildComponentsChangedTraceEvent; + public bool ForceInvalidate; + public long ForceInvalidateTraceEvent; + } + + private bool _suspended; + private readonly Dictionary, PendingEvent> _pendingEvents = new(); + + private void FireEvent(ListenerSet listeners, HierarchyEvent eventToSend) + { + if (!_suspended) + { + listeners.Fire(eventToSend); + return; + } + + if (!_pendingEvents.TryGetValue(listeners, out var pending)) + { + pending = new PendingEvent(); + } + + switch (eventToSend) + { + case HierarchyEvent.ObjectDirty: + { + pending.ObjectDirty = true; + pending.ObjectDirtyTraceEvent = TraceScope.CurrentTraceEvent.Value ?? -1; + break; + } + case HierarchyEvent.PathChange: + { + pending.PathChange = true; + pending.PathChangeTraceEvent = TraceScope.CurrentTraceEvent.Value ?? -1; + break; + } + case HierarchyEvent.SelfComponentsChanged: + { + pending.SelfComponentsChanged = true; + pending.SelfComponentsChangedTraceEvent = TraceScope.CurrentTraceEvent.Value ?? -1; + break; + } + case HierarchyEvent.ChildComponentsChanged: + { + pending.ChildComponentsChanged = true; + pending.ChildComponentsChangedTraceEvent = TraceScope.CurrentTraceEvent.Value ?? -1; + break; + } + case HierarchyEvent.ForceInvalidate: + { + // Discard other events, since force invalidate subsumes them + pending = new PendingEvent + { + ForceInvalidate = true, + ForceInvalidateTraceEvent = TraceScope.CurrentTraceEvent.Value ?? -1 + }; + break; + } + } + + _pendingEvents[listeners] = pending; + } + + private void FlushEvents() + { + Profiler.BeginSample("ShadowHierarchy.FlushEvents"); + foreach (var (listeners, pending) in _pendingEvents) + { + if (pending.ObjectDirty) + { + using (new TraceScope(pending.ObjectDirtyTraceEvent)) + { + listeners.Fire(HierarchyEvent.ObjectDirty); + } + } + + if (pending.PathChange) + { + using (new TraceScope(pending.PathChangeTraceEvent)) + { + listeners.Fire(HierarchyEvent.PathChange); + } + } + + if (pending.SelfComponentsChanged) + { + using (new TraceScope(pending.SelfComponentsChangedTraceEvent)) + { + listeners.Fire(HierarchyEvent.SelfComponentsChanged); + } + } + + if (pending.ChildComponentsChanged) + { + using (new TraceScope(pending.ChildComponentsChangedTraceEvent)) + { + listeners.Fire(HierarchyEvent.ChildComponentsChanged); + } + } + + if (pending.ForceInvalidate) + { + using (new TraceScope(pending.ForceInvalidateTraceEvent)) + { + listeners.Fire(HierarchyEvent.ForceInvalidate); + } + } + } + + _pendingEvents.Clear(); + Profiler.EndSample(); + } + + internal IDisposable SuspendEvents() + { + _suspended = true; + return new ActionDisposable(() => + { + _suspended = false; + FlushEvents(); + }); + } + + private class ActionDisposable : IDisposable + { + private readonly Action _action; + + public ActionDisposable(Action action) + { + _action = action; + } + + public void Dispose() + { + _action(); + } + } + #if NDMF_DEBUG [MenuItem("Tools/NDM Framework/Debug Tools/Dump shadow hierarchy")] static void StaticDumpShadowHierarchy() @@ -259,7 +407,7 @@ private ShadowGameObject ActivateShadowObject(GameObject targetObject) if (parent == null) { shadow.SetParent(null, false); - _rootSetListener.Fire(HierarchyEvent.ForceInvalidate); + FireEvent(_rootSetListener, HierarchyEvent.ForceInvalidate); } else { @@ -289,7 +437,7 @@ internal void FireObjectChangeNotification(int instanceId) if (_gameObjects.TryGetValue(instanceId, out var shadow)) { - shadow._listeners.Fire(HierarchyEvent.ObjectDirty); + FireEvent(shadow._listeners, HierarchyEvent.ObjectDirty); if (shadow.IsActive != shadow.GameObject.activeSelf) { shadow.IsActive = shadow.GameObject.activeSelf; @@ -308,7 +456,7 @@ internal void FireObjectChangeNotification(int instanceId) if (_otherObjects.TryGetValue(instanceId, out var shadowComponent)) { - shadowComponent._listeners.Fire(HierarchyEvent.ObjectDirty); + FireEvent(shadowComponent._listeners, HierarchyEvent.ObjectDirty); } } @@ -344,7 +492,7 @@ internal void FireReparentNotification(int instanceId) // Ensure the new parent is marked as dirty, in case this is a new object and we suppressed the dirty // notifications. - if (shadow.Parent != null) shadow.Parent._listeners.Fire(HierarchyEvent.ObjectDirty); + if (shadow.Parent != null) FireEvent(shadow.Parent._listeners, HierarchyEvent.ObjectDirty); // Update parentage and refire @@ -352,11 +500,11 @@ internal void FireReparentNotification(int instanceId) if (newParent == null) { shadow.Parent = null; - _rootSetListener.Fire(HierarchyEvent.ForceInvalidate); + FireEvent(_rootSetListener, HierarchyEvent.ForceInvalidate); } else if (newParent != shadow.Parent?.GameObject) { - if (shadow.Parent == null) _rootSetListener.Fire(HierarchyEvent.ForceInvalidate); + if (shadow.Parent == null) FireEvent(_rootSetListener, HierarchyEvent.ForceInvalidate); shadow.Parent = ActivateShadowObject(newParent); FireParentComponentChangeNotifications(shadow.Parent); @@ -377,7 +525,7 @@ internal void FireReparentNotification(int instanceId) private void FirePathChangeNotifications(ShadowGameObject shadow) { if (!shadow.PathMonitoring) return; - shadow._listeners.Fire(HierarchyEvent.PathChange); + FireEvent(shadow._listeners, HierarchyEvent.PathChange); foreach (var child in shadow.Children) { FirePathChangeNotifications(child); @@ -388,7 +536,7 @@ private void FireParentComponentChangeNotifications(ShadowGameObject obj) { while (obj != null) { - obj._listeners.Fire(HierarchyEvent.ChildComponentsChanged); + FireEvent(obj._listeners, HierarchyEvent.ChildComponentsChanged); obj = obj.Parent; } } @@ -412,8 +560,8 @@ void ForceInvalidateHierarchy(ShadowGameObject obj) var resolvedName = obj.GameObject == null ? "" : obj.GameObject.name; System.Diagnostics.Debug.WriteLine($"[ShadowHierarchy] ForceInvalidateHierarchy({obj.InstanceID}:{resolvedName})"); #endif - - obj._listeners.Fire(HierarchyEvent.ForceInvalidate); + + FireEvent(obj._listeners, HierarchyEvent.ForceInvalidate); _gameObjects.Remove(obj.InstanceID); foreach (var child in obj.Children) @@ -447,7 +595,7 @@ internal void FireStructureChangeEvent(int instanceId) return; } - shadow._listeners.Fire(HierarchyEvent.SelfComponentsChanged); + FireEvent(shadow._listeners, HierarchyEvent.SelfComponentsChanged); FireParentComponentChangeNotifications(shadow.Parent); } @@ -456,6 +604,7 @@ internal void InvalidateAll() #if NDMF_TRACE_SHADOW System.Diagnostics.Debug.WriteLine("[ShadowHierarchy] InvalidateAll()"); #endif + FlushEvents(); var oldDict = _gameObjects; _gameObjects = new Dictionary(); @@ -491,7 +640,7 @@ public void InvalidateTree(int instanceId) if (_gameObjects.TryGetValue(instanceId, out var shadow)) { _gameObjects.Remove(instanceId); - shadow._listeners.Fire(HierarchyEvent.ForceInvalidate); + FireEvent(shadow._listeners, HierarchyEvent.ForceInvalidate); FireParentComponentChangeNotifications(shadow.Parent); var parentGameObject = shadow.Parent?.GameObject; @@ -516,7 +665,7 @@ public void InvalidateTree(int instanceId) if (_otherObjects.TryGetValue(instanceId, out var otherObj)) { _otherObjects.Remove(instanceId); - otherObj._listeners.Fire(HierarchyEvent.ForceInvalidate); + FireEvent(otherObj._listeners, HierarchyEvent.ForceInvalidate); } } @@ -532,7 +681,7 @@ public void FireGameObjectCreate(int instanceId) var shadow = ActivateShadowObject(obj); // Ensure the new parent is marked as dirty - if (shadow.Parent != null) shadow.Parent._listeners.Fire(HierarchyEvent.ObjectDirty); + if (shadow.Parent != null) FireEvent(shadow.Parent._listeners, HierarchyEvent.ObjectDirty); } } diff --git a/Editor/ProfilerScope.cs b/Editor/ProfilerScope.cs new file mode 100644 index 0000000..fc1f7e5 --- /dev/null +++ b/Editor/ProfilerScope.cs @@ -0,0 +1,18 @@ +using System; +using UnityEngine.Profiling; + +namespace nadena.dev.ndmf +{ + internal struct ProfilerScope : IDisposable + { + public ProfilerScope(string name) + { + Profiler.BeginSample(name); + } + + public void Dispose() + { + Profiler.EndSample(); + } + } +} \ No newline at end of file diff --git a/Editor/ProfilerScope.cs.meta b/Editor/ProfilerScope.cs.meta new file mode 100644 index 0000000..7d55221 --- /dev/null +++ b/Editor/ProfilerScope.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ce8435b62cc241a89bb639a5a098b612 +timeCreated: 1728845102 \ No newline at end of file