From 38a5f04804ff7363af05d4c2be4a70c04594eed0 Mon Sep 17 00:00:00 2001 From: bd_ Date: Sat, 28 Sep 2024 18:17:31 -0700 Subject: [PATCH] feat: add a debugging trace feature for the preview system --- CHANGELOG.md | 1 + Editor/ChangeStream/ChangeStreamMonitor.cs | 9 + Editor/ChangeStream/ListenerSet.cs | 36 ++- Editor/ChangeStream/ObjectWatcher.cs | 34 ++- Editor/ChangeStream/PropertyMonitor.cs | 43 +++- Editor/PreviewSystem/ComputeContext.cs | 60 +++-- .../ComputeContext/PublishedValue.cs | 43 +++- .../ComputeContext/SingleObjectQueries.cs | 90 +++++-- .../PreviewSystem/Rendering/NodeController.cs | 163 +++++++----- .../Rendering/ProxyObjectController.cs | 9 +- .../PreviewSystem/Rendering/ProxyPipeline.cs | 20 +- .../PreviewSystem/Rendering/ProxySession.cs | 73 +++--- Editor/PreviewSystem/Rendering/TargetSet.cs | 8 +- Editor/PreviewSystem/TogglablePreviewNode.cs | 2 +- Editor/PreviewSystem/Trace.meta | 3 + Editor/PreviewSystem/Trace/TraceBuffer.cs | 243 ++++++++++++++++++ .../PreviewSystem/Trace/TraceBuffer.cs.meta | 3 + Editor/PreviewSystem/Trace/TraceEvent.cs | 85 ++++++ Editor/PreviewSystem/Trace/TraceEvent.cs.meta | 3 + Editor/PreviewSystem/Trace/TraceWindow.cs | 88 +++++++ .../PreviewSystem/Trace/TraceWindow.cs.meta | 14 + Editor/PreviewSystem/Trace/TraceWindow.uss | 34 +++ .../PreviewSystem/Trace/TraceWindow.uss.meta | 3 + Editor/PreviewSystem/Trace/TraceWindow.uxml | 17 ++ .../PreviewSystem/Trace/TraceWindow.uxml.meta | 3 + 25 files changed, 919 insertions(+), 168 deletions(-) create mode 100644 Editor/PreviewSystem/Trace.meta create mode 100644 Editor/PreviewSystem/Trace/TraceBuffer.cs create mode 100644 Editor/PreviewSystem/Trace/TraceBuffer.cs.meta create mode 100644 Editor/PreviewSystem/Trace/TraceEvent.cs create mode 100644 Editor/PreviewSystem/Trace/TraceEvent.cs.meta create mode 100644 Editor/PreviewSystem/Trace/TraceWindow.cs create mode 100644 Editor/PreviewSystem/Trace/TraceWindow.cs.meta create mode 100644 Editor/PreviewSystem/Trace/TraceWindow.uss create mode 100644 Editor/PreviewSystem/Trace/TraceWindow.uss.meta create mode 100644 Editor/PreviewSystem/Trace/TraceWindow.uxml create mode 100644 Editor/PreviewSystem/Trace/TraceWindow.uxml.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d7a5da5..c7b7a22d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- [#424] Added tracing system for the preview/invalidation system ### Fixed diff --git a/Editor/ChangeStream/ChangeStreamMonitor.cs b/Editor/ChangeStream/ChangeStreamMonitor.cs index cce121d9..9ba6627e 100644 --- a/Editor/ChangeStream/ChangeStreamMonitor.cs +++ b/Editor/ChangeStream/ChangeStreamMonitor.cs @@ -2,6 +2,7 @@ using System; using nadena.dev.ndmf.preview; +using nadena.dev.ndmf.preview.trace; using UnityEditor; using UnityEngine.Profiling; using Debug = UnityEngine.Debug; @@ -53,6 +54,14 @@ private static void OnChange(ref ObjectChangeEventStream stream) private static void HandleEvent(ObjectChangeEventStream stream, int i) { + var trace = TraceBuffer.RecordTraceEvent( + "ChangeStreamMonitor.HandleEvent", + (ev) => $"Handling event {ev.Arg0}", + stream.GetEventType(i), + level: TraceEventLevel.Trace + ); + + using (trace.Scope()) switch (stream.GetEventType(i)) { case ObjectChangeKind.None: break; diff --git a/Editor/ChangeStream/ListenerSet.cs b/Editor/ChangeStream/ListenerSet.cs index eeb5fc78..fdc96ca3 100644 --- a/Editor/ChangeStream/ListenerSet.cs +++ b/Editor/ChangeStream/ListenerSet.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Runtime.CompilerServices; using nadena.dev.ndmf.preview; +using nadena.dev.ndmf.preview.trace; using UnityEngine; #endregion @@ -53,13 +54,33 @@ public bool TryFire(T ev) { if (_targetRef.TryGetTarget(out var target)) { - if (TargetIsExpended(target)) return true; + if (TargetIsExpended(target)) + { + TraceBuffer.RecordTraceEvent( + eventType: "ListenerSet.Expired", + formatEvent: e => $"Listener for {e.Arg0} expired", + arg0: target + ); + return true; + } try { - if (!_filter(ev)) return false; - - _receiver(target); + if (!_filter(ev)) + { + return false; + } + + var tev = TraceBuffer.RecordTraceEvent( + eventType: "ListenerSet.Fire", + formatEvent: e => $"Listener for {e.Arg0} fired with {e.Arg1}", + arg0: target, + arg1: ev + ); + using (tev.Scope()) + { + _receiver(target); + } RepaintTrigger.RequestRepaint(); } @@ -71,6 +92,13 @@ public bool TryFire(T ev) return true; } + else + { + TraceBuffer.RecordTraceEvent( + eventType: "ListenerSet.GC", + formatEvent: e => "Listener expired" + ); + } return true; } diff --git a/Editor/ChangeStream/ObjectWatcher.cs b/Editor/ChangeStream/ObjectWatcher.cs index 2887c58d..e4ba27ea 100644 --- a/Editor/ChangeStream/ObjectWatcher.cs +++ b/Editor/ChangeStream/ObjectWatcher.cs @@ -5,7 +5,9 @@ using System.Collections.Immutable; using System.Linq; using System.Threading; +using JetBrains.Annotations; using nadena.dev.ndmf.preview; +using nadena.dev.ndmf.preview.trace; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; @@ -156,7 +158,7 @@ private void BindCancel(ComputeContext ctx, IDisposable cancel) } public R MonitorObjectProps(T obj, ComputeContext ctx, Func extract, Func compare, - bool usePropMonitor) + bool usePropMonitor, [CanBeNull] string file, int? line) where T : UnityObject { var curVal = extract(obj); @@ -172,7 +174,19 @@ public R MonitorObjectProps(T obj, ComputeContext ctx, Func extract, { case HierarchyEvent.ObjectDirty: case HierarchyEvent.ForceInvalidate: - return obj == null || !compare(curVal, extract(obj)); + if (obj != null && compare(curVal, extract(obj))) + { + TraceBuffer.RecordTraceEvent( + "ObjectWatcher.MonitorObjectProps", + ev => $"[{ev.FilePath}:{ev.Line}] Object {ev.Arg0} unchanged", + go.name, + filename: file ?? "???", + line: line ?? 0 + ); + return false; + } + + return true; default: return false; } @@ -182,13 +196,27 @@ public R MonitorObjectProps(T obj, ComputeContext ctx, Func extract, } else { + object objName = (obj is Component c_) ? c_.gameObject.name : obj; + var cancel = Hierarchy.RegisterObjectListener(obj, e => { switch (e) { case HierarchyEvent.ObjectDirty: case HierarchyEvent.ForceInvalidate: - return obj == null || !compare(curVal, extract(obj)); + if (obj != null && compare(curVal, extract(obj))) + { + TraceBuffer.RecordTraceEvent( + "ObjectWatcher.MonitorObjectProps", + ev => $"[{ev.FilePath}:{ev.Line}] Object {ev.Arg0} unchanged", + objName, + filename: file ?? "???", + line: line ?? 0 + ); + return false; + } + + return true; default: return false; } diff --git a/Editor/ChangeStream/PropertyMonitor.cs b/Editor/ChangeStream/PropertyMonitor.cs index 0303cc1f..79329a59 100644 --- a/Editor/ChangeStream/PropertyMonitor.cs +++ b/Editor/ChangeStream/PropertyMonitor.cs @@ -18,19 +18,42 @@ internal class PropertyMonitor private Task _activeRefreshTask = Task.CompletedTask; private Task _pendingRefreshTask = Task.CompletedTask; - internal void MaybeStartRefreshTimer() + private bool _isEnabled; + internal bool IsEnabled { - using (var scope = NDMFSyncContext.Scope()) + get => _isEnabled; + set { - _activeRefreshTask = Task.Factory.StartNew( - CheckAllObjectsLoop, - CancellationToken.None, - TaskCreationOptions.None, - TaskScheduler.FromCurrentSynchronizationContext() - ); + if (_isEnabled == value) return; + + _isEnabled = value; + + if (_isEnabled) + { + using (var scope = NDMFSyncContext.Scope()) + { + + var curRefreshTask = _activeRefreshTask; + _activeRefreshTask = Task.Factory.StartNew( + async () => + { + await curRefreshTask; + await CheckAllObjectsLoop(); + }, + CancellationToken.None, + TaskCreationOptions.None, + TaskScheduler.FromCurrentSynchronizationContext() + ).Unwrap(); + } + } } } + internal void MaybeStartRefreshTimer() + { + IsEnabled = true; + } + public enum PropertyMonitorEvent { PropsUpdated @@ -62,7 +85,7 @@ public ListenerSet MonitorObjectProps(Object obj) private async Task CheckAllObjectsLoop() { - while (true) + while (_isEnabled) { await CheckAllObjects(); await NextFrame(); @@ -81,6 +104,8 @@ public async Task CheckAllObjects() foreach (var pair in _registeredObjects.ToList()) { + if (!_isEnabled) break; + var (instanceId, reg) = pair; // Wake up all listeners to see if their monitored value has changed diff --git a/Editor/PreviewSystem/ComputeContext.cs b/Editor/PreviewSystem/ComputeContext.cs index 4fdbff79..153e6138 100644 --- a/Editor/PreviewSystem/ComputeContext.cs +++ b/Editor/PreviewSystem/ComputeContext.cs @@ -5,8 +5,10 @@ using System.Threading.Tasks; using JetBrains.Annotations; using nadena.dev.ndmf.cs; +using nadena.dev.ndmf.preview.trace; using UnityEditor; using UnityEngine; +using UnityEngine.TestTools.Constraints; #endregion @@ -59,6 +61,19 @@ public static void FlushInvalidates() ctx._onInvalidateListeners.FireAll(); ctx._onInvalidateTask.TrySetResult(true); } + + lock (_pendingInvalidatesLock) + { + if (_pendingInvalidates.Count > 0) + { + list = _pendingInvalidates; + _pendingInvalidates = new(); + } + else + { + list.Clear(); + } + } } } @@ -66,16 +81,19 @@ static ComputeContext() { NullContext = new ComputeContext("null", null); } - - #if NDMF_TRACE - + ~ComputeContext() { if (!IsInvalidated) - Debug.LogError("ComputeContext " + Description + " was GCed without being invalidated!"); + { + TraceBuffer.RecordTraceEvent( + "ComputeContext.Leak", + (ev) => "Leaked context: " + ((ComputeContext)ev.Arg0).Description, + arg0: this + ); + } } - #endif internal string Description { get; } @@ -102,14 +120,29 @@ internal Task OnInvalidate public bool IsInvalidated => _invalidatePending || _invalidater.Task.IsCompleted; private bool _invalidatePending; + private long? _invalidateTriggerEvent; public ComputeContext(string description) { + if (string.IsNullOrEmpty(description)) + { + Debug.LogWarning("ComputeContext created with empty description"); + } + Invalidate = () => { #if NDMF_TRACE Debug.Log("Invalidating " + Description); #endif + + if (_invalidatePending || IsInvalidated) return; + + _invalidateTriggerEvent = TraceBuffer.RecordTraceEvent( + "ComputeContext.Invalidate", + (ev) => "Invalidate: " + ev.Arg0, + arg0: this + ).EventId; + TaskUtil.OnMainThread(this, DoInvalidate); }; Description = description; @@ -128,11 +161,6 @@ private static void DoInvalidate(ComputeContext ctx) } } - private static void InvalidateInternal(ComputeContext ctx) - { - ctx._invalidater.TrySetResult(null); - } - private ComputeContext(string description, object nullToken) { Invalidate = () => { }; @@ -148,11 +176,13 @@ private ComputeContext(string description, object nullToken) public void Invalidates(ComputeContext other) { if (other.IsInvalidated) return; - - _invalidater.Task.ContinueWith(_ => - { - InvalidateInternal(other); - }); + + InvokeOnInvalidate(other, ForwardInvalidation); + } + + private void ForwardInvalidation(ComputeContext obj) + { + obj.Invalidate(); } /// diff --git a/Editor/PreviewSystem/ComputeContext/PublishedValue.cs b/Editor/PreviewSystem/ComputeContext/PublishedValue.cs index f94c3210..5752b373 100644 --- a/Editor/PreviewSystem/ComputeContext/PublishedValue.cs +++ b/Editor/PreviewSystem/ComputeContext/PublishedValue.cs @@ -1,5 +1,7 @@ using System; +using System.Runtime.CompilerServices; using nadena.dev.ndmf.cs; +using nadena.dev.ndmf.preview.trace; using UnityEngine; namespace nadena.dev.ndmf.preview @@ -14,6 +16,7 @@ public sealed class PublishedValue private T _value; public event Action OnChange; + public string DebugName; public T Value { @@ -21,12 +24,23 @@ public T Value set { if (ReferenceEquals(_value, value)) return; - _value = value; - _listeners.Fire(null); - var listeners = OnChange; - OnChange = default; + + var ev = TraceBuffer.RecordTraceEvent( + "PublishedValue.Set", + (ev) => $"[PublishedValue/{ev.Arg0}] Set value to {ev.Arg1}", + DebugName, + value + ); - listeners?.Invoke(value); + using (ev.Scope()) + { + _value = value; + _listeners.Fire(null); + var listeners = OnChange; + OnChange = default; + + listeners?.Invoke(value); + } } } @@ -35,9 +49,10 @@ public void SetWithoutNotify(T value) _value = value; } - public PublishedValue(T value) + public PublishedValue(T value, string debugName = null) { _value = value; + DebugName = debugName ?? typeof(T).Name; } private readonly ListenerSet _listeners = new(); @@ -45,7 +60,9 @@ public PublishedValue(T value) internal R Observe( ComputeContext context, Func extract, - Func eq + Func eq, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 ) { var initialValue = extract(_value); @@ -54,7 +71,17 @@ Func eq { try { - return !eq(initialValue, extract(_value)); + if (eq(initialValue, extract(_value))) + { + TraceBuffer.RecordTraceEvent( + "PublishedValue.Observe", + (ev) => $"[PublishedValue/{ev.Arg0}] No change detected", + DebugName + ); + return false; + } + + return true; } catch (Exception e) { diff --git a/Editor/PreviewSystem/ComputeContext/SingleObjectQueries.cs b/Editor/PreviewSystem/ComputeContext/SingleObjectQueries.cs index 44261c51..e7e6d522 100644 --- a/Editor/PreviewSystem/ComputeContext/SingleObjectQueries.cs +++ b/Editor/PreviewSystem/ComputeContext/SingleObjectQueries.cs @@ -5,6 +5,8 @@ #endif using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using JetBrains.Annotations; using nadena.dev.ndmf.cs; using UnityEngine; @@ -15,6 +17,7 @@ namespace nadena.dev.ndmf.preview { [PublicAPI] + [SuppressMessage("ReSharper", "ExplicitCallerInfoArgument")] public static partial class ComputeContextQueries { /// @@ -23,7 +26,11 @@ public static partial class ComputeContextQueries /// /// /// - public static GameObject GetAvatarRoot(this ComputeContext context, GameObject obj) + public static GameObject GetAvatarRoot( + this ComputeContext context, GameObject obj, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) { if (obj == null) return null; @@ -55,10 +62,14 @@ public static GameObject GetAvatarRoot(this ComputeContext context, GameObject o /// /// /// - public static T Observe(this ComputeContext ctx, PublishedValue val) + public static T Observe( + this ComputeContext ctx, PublishedValue val, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) where T : IEquatable { - return ctx.Observe(val, v => v); + return ctx.Observe(val, v => v, callerPath: callerPath, callerLine: callerLine); } /// @@ -72,10 +83,14 @@ public static T Observe(this ComputeContext ctx, PublishedValue val) /// Type of the value in the PublishedValue /// Type of the extracted value /// The extracted value - public static R Observe(this ComputeContext ctx, PublishedValue val, Func extract) + public static R Observe( + this ComputeContext ctx, PublishedValue val, Func extract, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) where R : IEquatable { - return ctx.Observe(val, extract, (a, b) => a.Equals(b)); + return ctx.Observe(val, extract, (a, b) => a.Equals(b), callerPath: callerPath, callerLine: callerLine); } /// @@ -91,9 +106,12 @@ public static R Observe(this ComputeContext ctx, PublishedValue val, Fu /// Type of the extracted value /// The extracted value public static R Observe(this ComputeContext ctx, PublishedValue val, Func extract, - Func eq) + Func eq, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) { - return val.Observe(ctx, extract, eq); + return val.Observe(ctx, extract, eq, callerPath: callerPath, callerLine: callerLine); } /// @@ -108,9 +126,14 @@ public static R Observe(this ComputeContext ctx, PublishedValue val, Fu /// /// /// - public static T Observe(this ComputeContext ctx, T obj) where T : Object + public static T Observe(this ComputeContext ctx, T obj, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) where T : Object { - ObjectWatcher.Instance.MonitorObjectProps(obj, ctx, _ => 0, (_, _) => false, false); + ObjectWatcher.Instance.MonitorObjectProps(obj, ctx, _ => 0, (_, _) => false, false, + callerPath, callerLine + ); return obj; } @@ -128,10 +151,11 @@ public static T Observe(this ComputeContext ctx, T obj) where T : Object /// /// public static R Observe(this ComputeContext ctx, T obj, Func extract, - Func compare = null) + Func compare = null, [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0) where T : Object { - return ObjectWatcher.Instance.MonitorObjectProps(obj, ctx, extract, compare, true); + return ObjectWatcher.Instance.MonitorObjectProps(obj, ctx, extract, compare, true, callerPath, callerLine); } /// @@ -183,9 +207,12 @@ public static Transform ObserveTransformPosition(this ComputeContext ctx, Transf /// /// /// - public static bool ActiveInHierarchy(this ComputeContext ctx, GameObject obj) + public static bool ActiveInHierarchy(this ComputeContext ctx, GameObject obj, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) { - foreach (var node in ObservePath(ctx, obj.transform)) ctx.Observe(node, n => n.gameObject.activeSelf); + foreach (var node in ObservePath(ctx, obj.transform)) ctx.Observe(node, n => n.gameObject.activeSelf, callerPath: callerPath, callerLine: callerLine); return obj.activeInHierarchy; } @@ -195,9 +222,12 @@ public static bool ActiveInHierarchy(this ComputeContext ctx, GameObject obj) /// /// /// - public static bool ActiveAndEnabled(this ComputeContext ctx, Behaviour c) + public static bool ActiveAndEnabled(this ComputeContext ctx, Behaviour c, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) { - return ActiveInHierarchy(ctx, c.gameObject) && ctx.Observe(c, c2 => c2.enabled); + return ActiveInHierarchy(ctx, c.gameObject) && ctx.Observe(c, c2 => c2.enabled, callerPath: callerPath, callerLine: callerLine); } private static C InternalGetComponent(GameObject obj) where C : class @@ -212,7 +242,10 @@ private static Component InternalGetComponent(GameObject obj, Type type) return null; } - public static C GetComponent(this ComputeContext ctx, GameObject obj) where C : class + public static C GetComponent(this ComputeContext ctx, GameObject obj, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) where C : class { if (obj == null) return null; @@ -220,7 +253,10 @@ public static C GetComponent(this ComputeContext ctx, GameObject obj) where C () => obj != null ? InternalGetComponent(obj) : null); } - public static Component GetComponent(this ComputeContext ctx, GameObject obj, Type type) + public static Component GetComponent(this ComputeContext ctx, GameObject obj, Type type, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) { if (obj == null) return null; @@ -228,7 +264,10 @@ public static Component GetComponent(this ComputeContext ctx, GameObject obj, Ty () => obj != null ? InternalGetComponent(obj, type) : null); } - public static C[] GetComponents(this ComputeContext ctx, GameObject obj) where C : class + public static C[] GetComponents(this ComputeContext ctx, GameObject obj, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) where C : class { if (obj == null) return Array.Empty(); @@ -236,7 +275,10 @@ public static C[] GetComponents(this ComputeContext ctx, GameObject obj) wher () => obj != null ? obj.GetComponents() : Array.Empty(), false); } - public static Component[] GetComponents(this ComputeContext ctx, GameObject obj, Type type) + public static Component[] GetComponents(this ComputeContext ctx, GameObject obj, Type type, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) { if (obj == null) return Array.Empty(); @@ -244,7 +286,10 @@ public static Component[] GetComponents(this ComputeContext ctx, GameObject obj, () => obj != null ? obj.GetComponents(type) : Array.Empty(), false); } - public static C[] GetComponentsInChildren(this ComputeContext ctx, GameObject obj, bool includeInactive) + public static C[] GetComponentsInChildren(this ComputeContext ctx, GameObject obj, bool includeInactive, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) where C : class { if (obj == null) return Array.Empty(); @@ -254,7 +299,10 @@ public static C[] GetComponentsInChildren(this ComputeContext ctx, GameObject } public static Component[] GetComponentsInChildren(this ComputeContext ctx, GameObject obj, Type type, - bool includeInactive) + bool includeInactive, + [CallerFilePath] string callerPath = "", + [CallerLineNumber] int callerLine = 0 + ) { if (obj == null) return Array.Empty(); diff --git a/Editor/PreviewSystem/Rendering/NodeController.cs b/Editor/PreviewSystem/Rendering/NodeController.cs index 455c27fa..76caad14 100644 --- a/Editor/PreviewSystem/Rendering/NodeController.cs +++ b/Editor/PreviewSystem/Rendering/NodeController.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using nadena.dev.ndmf.preview.trace; using UnityEngine; using UnityEngine.Profiling; @@ -104,44 +105,56 @@ private static async Task Create( string trace ) { - AsyncProfiler.PushProfilerContext("NodeController.Create[" + filter + "]", group.Renderers[0].gameObject); - var context = - new ComputeContext("NodeController " + trace + " for " + filter + " on " + - group.Renderers[0].gameObject.name); + var ev = TraceBuffer.RecordTraceEvent( + "NodeController.Create", + (ev) => $"NodeController: Create for {ev.Arg0} on {ev.Arg1}", + arg0: filter, + arg1: group + ); + + using (ev.Scope()) + { + + AsyncProfiler.PushProfilerContext("NodeController.Create[" + filter + "]", + group.Renderers[0].gameObject); + var context = + new ComputeContext("NodeController " + trace + " for " + filter + " on " + + group.Renderers[0].gameObject.name); #if NDMF_TRACE_FILTERS UnityEngine.Debug.Log("[NodeController Create] " + trace + " Filter=" + filter + " Group=" + group.Renderers[0].gameObject.name + " Registry dump:\n" + registry.RegistryDump()); #endif - IRenderFilterNode node; - using (var scope = new ObjectRegistryScope(registry)) - { - var savedMaterials = group.Renderers.Select(r => r.sharedMaterials).ToArray(); - - node = await filter.Instantiate( - group, - proxies.Select(p => (p.Item1, p.Item2.Renderer)), - context - ); - - for (var i = 0; i < group.Renderers.Count; i++) + IRenderFilterNode node; + using (var scope = new ObjectRegistryScope(registry)) { - if (group.Renderers[i].sharedMaterials.SequenceEqual(savedMaterials[i])) continue; + var savedMaterials = group.Renderers.Select(r => r.sharedMaterials).ToArray(); + + node = await filter.Instantiate( + group, + proxies.Select(p => (p.Item1, p.Item2.Renderer)), + context + ); + + for (var i = 0; i < group.Renderers.Count; i++) + { + if (group.Renderers[i].sharedMaterials.SequenceEqual(savedMaterials[i])) continue; - Debug.LogWarning("[NodeController Create] Renderer " + group.Renderers[i].gameObject.name + - " sharedMaterials changed during instantiation of " + filter + " in " + - " group " + group + ". Restoring original materials."); - group.Renderers[i].sharedMaterials = savedMaterials[i]; + Debug.LogWarning("[NodeController Create] Renderer " + group.Renderers[i].gameObject.name + + " sharedMaterials changed during instantiation of " + filter + " in " + + " group " + group + ". Restoring original materials."); + group.Renderers[i].sharedMaterials = savedMaterials[i]; + } } - } #if NDMF_TRACE_FILTERS Debug.Log("[NodeController Post-Create] " + trace + " Filter=" + filter + " Group=" + group.Renderers[0].gameObject.name + " Registry dump:\n" + registry.RegistryDump()); #endif - - return new NodeController(filter, group, node, proxies, new RefCount(), context, registry); + + return new NodeController(filter, group, node, proxies, new RefCount(), context, registry); + } } public async Task Refresh( @@ -150,59 +163,71 @@ public async Task Refresh( string trace ) { - AsyncProfiler.PushProfilerContext("NodeController.Refresh[" + _filter + "]", _group.Renderers[0].gameObject); - var registry = ObjectRegistry.Merge(null, proxies.Select(p => p.Item3) - .Append(ObjectRegistry)); - var context = new ComputeContext("NodeController (refresh) for " + _filter + " on " + - _group.Renderers[0].gameObject.name); + var ev = TraceBuffer.RecordTraceEvent( + "NodeController.Refresh", + (ev) => $"NodeController: Refresh for {ev.Arg0} on {ev.Arg1}", + arg0: _filter, + arg1: _group + ); + + using (ev.Scope()) + { - IRenderFilterNode node; + AsyncProfiler.PushProfilerContext("NodeController.Refresh[" + _filter + "]", + _group.Renderers[0].gameObject); + var registry = ObjectRegistry.Merge(null, proxies.Select(p => p.Item3) + .Append(ObjectRegistry)); + var context = new ComputeContext("NodeController (refresh) for " + _filter + " on " + + _group.Renderers[0].gameObject.name); - if (changes == 0 && !IsInvalidated) - { - // Reuse the old node in its entirety - node = _node; - context = _context; - } - else - { - using (var scope = new ObjectRegistryScope(registry)) + IRenderFilterNode node; + + if (changes == 0 && !IsInvalidated) { - node = await _node.Refresh( - proxies.Select(p => (p.Item1, p.Item2.Renderer)), - context, - changes - ); + // Reuse the old node in its entirety + node = _node; + context = _context; + } + else + { + using (var scope = new ObjectRegistryScope(registry)) + { + node = await _node.Refresh( + proxies.Select(p => (p.Item1, p.Item2.Renderer)), + context, + changes + ); + } } - } - RefCount refCount; - if (node == _node) - { - refCount = _refCount; - refCount.Count++; - } - else if (node == null) - { - // rebuild registry so we forget any garbage left by the aborted Refresh call - registry = ObjectRegistry.Merge(null, proxies.Select(p => p.Item3)); - - return await Create(_filter, _group, registry, proxies, trace); - } - else - { - refCount = new RefCount(); - } + RefCount refCount; + if (node == _node) + { + refCount = _refCount; + refCount.Count++; + } + else if (node == null) + { + // rebuild registry so we forget any garbage left by the aborted Refresh call + registry = ObjectRegistry.Merge(null, proxies.Select(p => p.Item3)); - var controller = new NodeController(_filter, _group, node, proxies, refCount, context, registry); - controller.WhatChanged = changes | node.WhatChanged; + return await Create(_filter, _group, registry, proxies, trace); + } + else + { + refCount = new RefCount(); + } - foreach (var proxy in proxies) - { - proxy.Item2.ChangeFlags |= node.WhatChanged; + var controller = new NodeController(_filter, _group, node, proxies, refCount, context, registry); + controller.WhatChanged = changes | node.WhatChanged; + + foreach (var proxy in proxies) + { + proxy.Item2.ChangeFlags |= node.WhatChanged; + } + + return controller; } - - return controller; } public void Dispose() diff --git a/Editor/PreviewSystem/Rendering/ProxyObjectController.cs b/Editor/PreviewSystem/Rendering/ProxyObjectController.cs index 7b9a992a..a86db9db 100644 --- a/Editor/PreviewSystem/Rendering/ProxyObjectController.cs +++ b/Editor/PreviewSystem/Rendering/ProxyObjectController.cs @@ -27,7 +27,7 @@ internal class ProxyObjectController : IDisposable internal Mesh _initialSharedMesh; internal ComputeContext _monitorRenderer, _monitorMaterials, _monitorMesh; - internal Task OnInvalidate; + internal ComputeContext InvalidateMonitor; private bool _visibilityOffOriginal; private bool _pickingOffOriginal, _pickingOffReplacement; @@ -82,7 +82,7 @@ private void SetupRendererMonitoring(Renderer r) { if (r == null) { - OnInvalidate = Task.CompletedTask; + InvalidateMonitor = ComputeContext.NullContext; return; } @@ -124,7 +124,10 @@ private void SetupRendererMonitoring(Renderer r) } } - OnInvalidate = Task.WhenAny(_monitorRenderer.OnInvalidate, _monitorMaterials.OnInvalidate, _monitorMesh.OnInvalidate); + InvalidateMonitor = new ComputeContext("ProxyObjectController for " + gameObjectName); + _monitorMesh.Invalidates(InvalidateMonitor); + _monitorMaterials.Invalidates(InvalidateMonitor); + _monitorRenderer.Invalidates(InvalidateMonitor); } internal bool OnPreFrame() diff --git a/Editor/PreviewSystem/Rendering/ProxyPipeline.cs b/Editor/PreviewSystem/Rendering/ProxyPipeline.cs index 19865dd4..f29aac67 100644 --- a/Editor/PreviewSystem/Rendering/ProxyPipeline.cs +++ b/Editor/PreviewSystem/Rendering/ProxyPipeline.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using nadena.dev.ndmf.cs; +using nadena.dev.ndmf.preview.trace; using UnityEngine; using UnityEngine.Profiling; @@ -89,12 +90,20 @@ internal void Invalidate() public IEnumerable<(Renderer, Renderer)> Renderers => _proxies.Select(kvp => (kvp.Key, kvp.Value.Renderer)); - public ProxyPipeline(ProxyObjectCache proxyCache, IEnumerable filters, ProxyPipeline priorPipeline = null) + public ProxyPipeline(ProxyObjectCache proxyCache, IEnumerable filters, + ProxyPipeline priorPipeline = null) { _generation = (priorPipeline?._generation ?? 0) + 1; InvalidateAction = Invalidate; + var buildEvent = TraceBuffer.RecordTraceEvent( + "ProxyPipeline.Build", + (ev) => $"Pipeline {((ProxyPipeline)ev.Arg0)._generation}: Start build", + arg0: this + ); + using (var scope = NDMFSyncContext.Scope()) + using (var evScope = buildEvent.Scope()) { _buildTask = Task.Factory.StartNew( _ => Build(proxyCache, filters, priorPipeline), @@ -193,8 +202,8 @@ private async Task Build(ProxyObjectCache proxyCache, IEnumerable priorPipeline?._proxies.TryGetValue(r, out priorProxy); var proxy = new ProxyObjectController(proxyCache, r, priorProxy); - proxy.OnInvalidate.ContinueWith(_ => InvalidateAction(), - TaskContinuationOptions.ExecuteSynchronously); + proxy.InvalidateMonitor.Invalidates(context); + if (!proxy.OnPreFrame()) Invalidate(); // OnPreFrame can enable rendering, turn it off for now (until the pipeline goes active and // we render for real). @@ -268,6 +277,11 @@ private async Task Build(ProxyObjectCache proxyCache, IEnumerable await Task.WhenAll(_stages.SelectMany(s => s.NodeTasks)) .ContinueWith(result => { + TraceBuffer.RecordTraceEvent( + "ProxyPipeline.Build", + (ev) => $"Pipeline {((ProxyPipeline)ev.Arg0)._generation}: Build complete", + arg0: this + ); _completedBuild.TrySetResult(null); RepaintTrigger.RequestRepaint(); }); diff --git a/Editor/PreviewSystem/Rendering/ProxySession.cs b/Editor/PreviewSystem/Rendering/ProxySession.cs index 68f1fdfa..f6b8b6f9 100644 --- a/Editor/PreviewSystem/Rendering/ProxySession.cs +++ b/Editor/PreviewSystem/Rendering/ProxySession.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using nadena.dev.ndmf.preview.trace; using UnityEditor; using UnityEngine; @@ -72,38 +73,48 @@ public void Dispose() public IEnumerable<(Renderer, Renderer)> OnPreCull(bool isSceneCamera) { - ShadowBoneManager.Instance.Update(); - - bool activeIsReady = _active?.IsReady == true; - bool activeNeedsReplacement = _active?.IsInvalidated != false; - - if (_next?.IsFailed == true) - { - _next.ShowError(); - _next?.Dispose(); - _next = null; - } - - if (activeNeedsReplacement && _next == null) - { - _next = new ProxyPipeline(_proxyCache, _filters.ToList(), _active); - } - - if (activeNeedsReplacement && _next?.IsReady == true) - { - _active?.Dispose(); - _active = _next; - _next = null; - } - - if (activeIsReady) - { - _active.OnFrame(isSceneCamera); - return _active.Renderers; - } - else + var ev = TraceBuffer.RecordTraceEvent( + "ProxySession.OnPreCull", + (ev) => $"Camera render (scene camera: {ev.Arg0})", + arg0: isSceneCamera, + collapse: true + ); + + using (var scope = ev.Scope()) { - return Array.Empty<(Renderer, Renderer)>(); + ShadowBoneManager.Instance.Update(); + + bool activeIsReady = _active?.IsReady == true; + bool activeNeedsReplacement = _active?.IsInvalidated != false; + + if (_next?.IsFailed == true) + { + _next.ShowError(); + _next?.Dispose(); + _next = null; + } + + if (activeNeedsReplacement && _next == null) + { + _next = new ProxyPipeline(_proxyCache, _filters.ToList(), _active); + } + + if (activeNeedsReplacement && _next?.IsReady == true) + { + _active?.Dispose(); + _active = _next; + _next = null; + } + + if (activeIsReady) + { + _active.OnFrame(isSceneCamera); + return _active.Renderers; + } + else + { + return Array.Empty<(Renderer, Renderer)>(); + } } } } diff --git a/Editor/PreviewSystem/Rendering/TargetSet.cs b/Editor/PreviewSystem/Rendering/TargetSet.cs index 409f1195..b4b366a3 100644 --- a/Editor/PreviewSystem/Rendering/TargetSet.cs +++ b/Editor/PreviewSystem/Rendering/TargetSet.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using nadena.dev.ndmf.preview.trace; using UnityEditor; using UnityEngine; using UnityEngine.Profiling; @@ -28,6 +29,8 @@ public TargetSet(ImmutableList filters) { _filters = filters; + TraceBuffer.RecordTraceEvent("TargetSet.ctor", (ev) => "Get target groups"); + Profiler.BeginSample("TargetSet.ctor"); try { @@ -35,7 +38,7 @@ public TargetSet(ImmutableList filters) foreach (var filter in _filters) { if (!filter.IsEnabled(_targetSetContext)) continue; - + Profiler.BeginSample("TargetSet.GetTargetGroups[" + filter + "]"); var groups = filter.GetTargetGroups(_targetSetContext); Profiler.EndSample(); @@ -98,6 +101,9 @@ private static bool RendererIsShown(ComputeContext context, Renderer renderer) public ImmutableList ResolveActiveStages(ComputeContext context) { Profiler.BeginSample("TargetSet.ResolveActiveStages"); + + TraceBuffer.RecordTraceEvent("TargetSet.ResolveActiveStages", (ev) => "TargetSet: Resolve active stages"); + _targetSetContext.Invalidates(context); var targetRenderers = _stages diff --git a/Editor/PreviewSystem/TogglablePreviewNode.cs b/Editor/PreviewSystem/TogglablePreviewNode.cs index 622c998f..0168ee5e 100644 --- a/Editor/PreviewSystem/TogglablePreviewNode.cs +++ b/Editor/PreviewSystem/TogglablePreviewNode.cs @@ -27,7 +27,7 @@ private static void Init() private TogglablePreviewNode(Func displayName, bool initialState) { DisplayName = displayName; - IsEnabled = new PublishedValue(initialState); + IsEnabled = new PublishedValue(initialState, debugName: "PreviewToggle/" + displayName()); } /// diff --git a/Editor/PreviewSystem/Trace.meta b/Editor/PreviewSystem/Trace.meta new file mode 100644 index 00000000..4ed84f88 --- /dev/null +++ b/Editor/PreviewSystem/Trace.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5cdfd30e210c410cb22555e998aeb985 +timeCreated: 1727558077 \ No newline at end of file diff --git a/Editor/PreviewSystem/Trace/TraceBuffer.cs b/Editor/PreviewSystem/Trace/TraceBuffer.cs new file mode 100644 index 00000000..d9773de9 --- /dev/null +++ b/Editor/PreviewSystem/Trace/TraceBuffer.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UnityEditor; + +namespace nadena.dev.ndmf.preview.trace +{ + /// + /// Records debugging traces in the NDMF preview system + /// + public static class TraceBuffer + { + [InitializeOnLoadMethod] + static void Init() + { + EditorApplication.update += () => + { + _editorFrame++; + }; + } + + private static long _editorFrame; + private static TraceEvent[] _traceEvents = new TraceEvent[256]; + private static long _totalTraceEvents; + + /// + /// Records a tracing event. + /// + /// Note that arguments that are ComputeContext will be converted to their description string to avoid keeping + /// those contexts alive inappropriately. + /// + /// An internal identifier for this event + /// A method that will format this event to a human-readable string + /// Arbitrary data usable by formatEvent + /// Arbitrary data usable by formatEvent + /// Arbitrary data usable by formatEvent + /// Optional filename usable by formatEvent + /// Optional line number usable by formatEvent + /// An event ID to associate as the parent of this event; if not provided, the + /// containing event scope will be used (see TraceEvent.Scope). If negative, no parent will be assigned. + /// If true, and the most recent event had the same eventType, overwrite that event + /// The tracing detail level (default is INFO) + /// + public static TraceEvent RecordTraceEvent( + string eventType, + Func formatEvent, + object arg0 = null, + object arg1 = null, + object arg2 = null, + string filename = null, + int? line = null, + long? parentEventId = null, + bool collapse = false, + TraceEventLevel? level = null + ) + { + level = level ?? TraceEventLevel.Info; + + lock (_traceEvents) + { + TraceEvent traceEvent = new TraceEvent + { + Timestamp = DateTime.Now.Ticks, + EditorFrame = _editorFrame, + EventType = eventType, + FormatEvent = formatEvent, + Arg0 = MapArg(arg0), + Arg1 = MapArg(arg1), + Arg2 = MapArg(arg2), + FilePath = filename, + Line = line, + ParentEventId = parentEventId ?? TraceScope.CurrentTraceEvent.Value, + Level = level.Value + }; + + if (traceEvent.ParentEventId < 0) + { + traceEvent.ParentEventId = null; + } + + if (collapse && _totalTraceEvents > 1) + { + TraceEvent lastEvent = _traceEvents[(int)(_totalTraceEvents - 1) % _traceEvents.Length]; + if (lastEvent.EventType == traceEvent.EventType) + { + _totalTraceEvents--; + } + } + + int index = (int)(_totalTraceEvents % _traceEvents.Length); + traceEvent.EventId = _totalTraceEvents; + _traceEvents[index] = traceEvent; + + _totalTraceEvents++; + + return traceEvent; + } + } + + private static object MapArg(object p0) + { + if (p0 is ComputeContext ctx) + { + return ctx.Description; + } + else + { + return p0; + } + } + + private static TraceEvent GetTraceEvent(long eventIndex) + { + if (eventIndex < 0 || eventIndex >= _totalTraceEvents) + { + return new TraceEvent() + { + Timestamp = 0, + EditorFrame = 0, + EventType = "???", + FormatEvent = (ev) => "???", + EventId = eventIndex, + ParentEventId = null + }; + } + + return _traceEvents[(int)(eventIndex % _traceEvents.Length)]; + } + + internal static List<(string, string)> FormatTraceBuffer(int maxEvents, TraceEventLevel minLevel = TraceEventLevel.Info) + { + lock (_traceEvents) + { + if (_totalTraceEvents == 0) return new(); + + long firstAvailableEvent = _totalTraceEvents - Math.Min(maxEvents, _traceEvents.Length); + firstAvailableEvent = Math.Max(0, firstAvailableEvent); + + SortedDictionary> frameToEvents = new(); + + for (long ev = firstAvailableEvent; ev < _totalTraceEvents; ev++) + { + var traceEvent = GetTraceEvent(ev); + + // Include lower level events only if a child event passes the filter + if (traceEvent.Level < minLevel) continue; + + if (!frameToEvents.TryGetValue(traceEvent.EditorFrame, out SortedSet events)) + { + events = new SortedSet(); + frameToEvents[traceEvent.EditorFrame] = events; + } + events.Add(ev); + + while (traceEvent.ParentEventId.HasValue) + { + traceEvent = GetTraceEvent(traceEvent.ParentEventId.Value); + events.Add(traceEvent.EventId); + } + } + + List<(string, string)> formattedEvents = new(); + StringBuilder buffer = new(); + + foreach (var (frame, events) in frameToEvents) + { + buffer.Clear(); + + string label = $"Editor frame {frame}"; + + FormatSingleFrameEvents(buffer, frame, events); + formattedEvents.Add((label, buffer.ToString())); + } + + return formattedEvents; + } + } + + private static void FormatSingleFrameEvents(StringBuilder builder, long editorFrame, SortedSet events) + { + // Generate parentage tree + Dictionary> parentToChildren = new(); + List rootEvents = new(); + + foreach (long eventId in events) + { + TraceEvent traceEvent = GetTraceEvent(eventId); + if (!traceEvent.ParentEventId.HasValue) + { + rootEvents.Add(eventId); + continue; + } + + if (!parentToChildren.TryGetValue(traceEvent.ParentEventId.Value, out SortedSet children)) + { + children = new SortedSet(); + parentToChildren[traceEvent.ParentEventId.Value] = children; + } + + children.Add(eventId); + } + + foreach (long rootEventId in rootEvents) + { + FormatTraceEvent(rootEventId, 0); + } + + + void FormatTraceEvent(long eventIndex, int indent) + { + TraceEvent traceEvent = GetTraceEvent(eventIndex); + + string continuationPrefix = ""; + if (editorFrame != traceEvent.EditorFrame) + { + continuationPrefix = "... "; + } + + string formattedEvent = traceEvent.FormatEvent(traceEvent); + builder.Append(' ', indent * 2); + builder.AppendFormat("[{1}{0}] ", traceEvent.EventId, continuationPrefix); + builder.Append(formattedEvent); + builder.AppendLine(); + + if (parentToChildren.TryGetValue(eventIndex, out var children)) + { + foreach (int childIndex in children) + { + FormatTraceEvent(childIndex, indent + 1); + } + } + } + } + + public static void Clear() + { + lock (_traceEvents) + { + _totalTraceEvents = 0; + } + } + } +} \ No newline at end of file diff --git a/Editor/PreviewSystem/Trace/TraceBuffer.cs.meta b/Editor/PreviewSystem/Trace/TraceBuffer.cs.meta new file mode 100644 index 00000000..b97df0d0 --- /dev/null +++ b/Editor/PreviewSystem/Trace/TraceBuffer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 949fb8f4e1ea4e388449c9b182f75c93 +timeCreated: 1727558404 \ No newline at end of file diff --git a/Editor/PreviewSystem/Trace/TraceEvent.cs b/Editor/PreviewSystem/Trace/TraceEvent.cs new file mode 100644 index 00000000..b58b50ca --- /dev/null +++ b/Editor/PreviewSystem/Trace/TraceEvent.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading; +using JetBrains.Annotations; + +namespace nadena.dev.ndmf.preview.trace +{ + public enum TraceEventLevel + { + /// + /// Events at TRACE will be hidden unless they have child events + /// + Trace, + Info, + } + + public struct TraceEvent + { + public long Timestamp; + public long EditorFrame; + + public string EventType; + public Func FormatEvent; + public object Arg0, Arg1, Arg2; + + [CanBeNull] public string FilePath; + + public string Filename + { + get + { + if (FilePath == null) return "???"; + + var lastSlash = FilePath.LastIndexOf('/'); + return lastSlash >= 0 ? FilePath.Substring(lastSlash + 1) : FilePath; + } + } + public int? Line; + + public long EventId; + public long? ParentEventId; + + public TraceEventLevel Level; + + /// + /// Enters a dynamic AsyncLocal scope in which this event is the parent event. The scope stack will be restored + /// to its original state on Dispose. + /// + /// + public TraceScope Scope() + { + return new TraceScope(EventId); + } + + /// + /// Enters a dynamic AsyncLocal scope in which the specified event ID is the parent event. The scope stack will + /// be restored to its original state on Dispose. + /// + /// + /// + public static TraceScope Scope(long eventId) + { + return new TraceScope(eventId); + } + + public long? CurrentEventId => TraceScope.CurrentTraceEvent.Value; + } + + public class TraceScope : IDisposable + { + internal static AsyncLocal CurrentTraceEvent = new(); + + private readonly long? _previousEventId; + + public TraceScope(long eventId) + { + _previousEventId = CurrentTraceEvent.Value; + CurrentTraceEvent.Value = eventId >= 0 ? eventId : null; + } + + public void Dispose() + { + CurrentTraceEvent.Value = _previousEventId; + } + } +} \ No newline at end of file diff --git a/Editor/PreviewSystem/Trace/TraceEvent.cs.meta b/Editor/PreviewSystem/Trace/TraceEvent.cs.meta new file mode 100644 index 00000000..e748c243 --- /dev/null +++ b/Editor/PreviewSystem/Trace/TraceEvent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bf0e734b737b49e280d1265a16247bea +timeCreated: 1727558087 \ No newline at end of file diff --git a/Editor/PreviewSystem/Trace/TraceWindow.cs b/Editor/PreviewSystem/Trace/TraceWindow.cs new file mode 100644 index 00000000..021b2b0e --- /dev/null +++ b/Editor/PreviewSystem/Trace/TraceWindow.cs @@ -0,0 +1,88 @@ +using System; +using System.Text; +using nadena.dev.ndmf.cs; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace nadena.dev.ndmf.preview.trace +{ + internal class TraceWindow : EditorWindow + { + [MenuItem("Tools/NDM Framework/Debug Tools/Preview trace")] + public static void ShowWindow() + { + GetWindow("Preview trace"); + } + + [SerializeField] StyleSheet uss; + [SerializeField] VisualTreeAsset uxml; + + private VisualElement _events; + private string _eventText; + + private void OnEnable() + { + EditorApplication.delayCall += LoadUI; + } + + private void OnDisable() + { + ObjectWatcher.Instance.PropertyMonitor.IsEnabled = true; + } + + private void LoadUI() + { + ObjectWatcher.Instance.PropertyMonitor.IsEnabled = false; + + var root = rootVisualElement; + root.Clear(); + + root.styleSheets.Add(uss); + uxml.CloneTree(root); + + _events = root.Q("events"); + + root.Q