Improve TransformTree efficiency

- Added batching to the update method so that a maximum of 2000 GameObjects are traversed each frame.
- Changed from OrderedDictionary.Remove to OrderedDictionary.RemoveAt when pruning entries as the former needs to iterate through all entries to find the index of the key, whereas the latter is constant time.
This commit is contained in:
Sinai 2022-03-10 04:35:06 +11:00
parent 0e37e8030c
commit 0afccadc64
3 changed files with 128 additions and 76 deletions

View File

@ -82,7 +82,7 @@ namespace UnityExplorer.Inspectors
this.Target = newTarget; this.Target = newTarget;
GOControls.UpdateGameObjectInfo(true, true); GOControls.UpdateGameObjectInfo(true, true);
GOControls.UpdateTransformControlValues(true); GOControls.UpdateTransformControlValues(true);
TransformTree.RefreshData(true, false); TransformTree.RefreshData(true, false, true);
UpdateComponents(); UpdateComponents();
} }
@ -109,7 +109,7 @@ namespace UnityExplorer.Inspectors
GOControls.UpdateGameObjectInfo(false, false); GOControls.UpdateGameObjectInfo(false, false);
TransformTree.RefreshData(true, false); TransformTree.RefreshData(true, false, false);
UpdateComponents(); UpdateComponents();
} }
} }
@ -220,7 +220,7 @@ namespace UnityExplorer.Inspectors
var newObject = new GameObject(input); var newObject = new GameObject(input);
newObject.transform.parent = GOTarget.transform; newObject.transform.parent = GOTarget.transform;
TransformTree.RefreshData(true, false); TransformTree.RefreshData(true, false, true);
} }
private void OnAddComponentClicked(string input) private void OnAddComponentClicked(string input)

View File

@ -64,7 +64,7 @@ namespace UnityExplorer.ObjectExplorer
public void UpdateTree() public void UpdateTree()
{ {
SceneHandler.Update(); SceneHandler.Update();
Tree.RefreshData(true); Tree.RefreshData(true, false, false);
} }
public void JumpToTransform(Transform transform) public void JumpToTransform(Transform transform)
@ -94,7 +94,7 @@ namespace UnityExplorer.ObjectExplorer
SceneHandler.SelectedScene = SceneHandler.LoadedScenes[value]; SceneHandler.SelectedScene = SceneHandler.LoadedScenes[value];
SceneHandler.Update(); SceneHandler.Update();
Tree.RefreshData(true); Tree.RefreshData(true, true, true);
OnSelectedSceneChanged(SceneHandler.SelectedScene.Value); OnSelectedSceneChanged(SceneHandler.SelectedScene.Value);
} }
@ -158,7 +158,7 @@ namespace UnityExplorer.ObjectExplorer
} }
Tree.CurrentFilter = input; Tree.CurrentFilter = input;
Tree.RefreshData(true, true); Tree.RefreshData(true, false, true);
} }
private void TryLoadScene(LoadSceneMode mode, Dropdown allSceneDrop) private void TryLoadScene(LoadSceneMode mode, Dropdown allSceneDrop)
@ -239,6 +239,17 @@ namespace UnityExplorer.ObjectExplorer
refreshRow.SetActive(false); refreshRow.SetActive(false);
// tree labels row
var labelsRow = UIFactory.CreateHorizontalGroup(toolbar, "LabelsRow", true, true, true, true, 2, new Vector4(2, 2, 2, 2));
UIFactory.SetLayoutElement(labelsRow, minHeight: 30, flexibleHeight: 0);
var nameLabel = UIFactory.CreateLabel(labelsRow, "NameLabel", "Name", TextAnchor.MiddleLeft, color: Color.grey);
UIFactory.SetLayoutElement(nameLabel.gameObject, flexibleWidth: 9999, minHeight: 25);
var indexLabel = UIFactory.CreateLabel(labelsRow, "IndexLabel", "Sibling Index", TextAnchor.MiddleLeft, fontSize: 12, color: Color.grey);
UIFactory.SetLayoutElement(indexLabel.gameObject, minWidth: 100, flexibleWidth: 0, minHeight: 25);
// Transform Tree // Transform Tree
var scrollPool = UIFactory.CreateScrollPool<TransformCell>(uiRoot, "TransformTree", out GameObject scrollObj, var scrollPool = UIFactory.CreateScrollPool<TransformCell>(uiRoot, "TransformTree", out GameObject scrollObj,
@ -248,7 +259,7 @@ namespace UnityExplorer.ObjectExplorer
Tree = new TransformTree(scrollPool, GetRootEntries); Tree = new TransformTree(scrollPool, GetRootEntries);
Tree.Init(); Tree.Init();
Tree.RefreshData(true, true); Tree.RefreshData(true, true, true);
//scrollPool.Viewport.GetComponent<Mask>().enabled = false; //scrollPool.Viewport.GetComponent<Mask>().enabled = false;
//UIRoot.GetComponent<Mask>().enabled = false; //UIRoot.GetComponent<Mask>().enabled = false;

View File

@ -2,12 +2,9 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Diagnostics;
using System.Text;
using UnityEngine; using UnityEngine;
using UnityEngine.UI;
using UniverseLib; using UniverseLib;
using UniverseLib.UI.Widgets;
using UniverseLib.UI.Widgets.ScrollView; using UniverseLib.UI.Widgets.ScrollView;
using UniverseLib.Utility; using UniverseLib.Utility;
@ -24,17 +21,18 @@ namespace UnityExplorer.UI.Widgets
/// Key: UnityEngine.Transform instance ID<br/> /// Key: UnityEngine.Transform instance ID<br/>
/// Value: CachedTransform /// Value: CachedTransform
/// </summary> /// </summary>
internal readonly OrderedDictionary cachedTransforms = new OrderedDictionary(); internal readonly OrderedDictionary cachedTransforms = new();
// for keeping track of which actual transforms are expanded or not, outside of the cache data. // for keeping track of which actual transforms are expanded or not, outside of the cache data.
private readonly HashSet<int> expandedInstanceIDs = new HashSet<int>(); private readonly HashSet<int> expandedInstanceIDs = new();
private readonly HashSet<int> autoExpandedIDs = new HashSet<int>(); private readonly HashSet<int> autoExpandedIDs = new();
private readonly HashSet<int> visited = new HashSet<int>(); private readonly HashSet<int> visited = new();
private bool needRefresh; private bool needRefresh;
private int displayIndex; private int displayIndex;
int prevDisplayIndex;
public int ItemCount => cachedTransforms.Count; public int ItemCount => prevDisplayIndex;
public bool Filtering => !string.IsNullOrEmpty(currentFilter); public bool Filtering => !string.IsNullOrEmpty(currentFilter);
private bool wasFiltering; private bool wasFiltering;
@ -56,45 +54,15 @@ namespace UnityExplorer.UI.Widgets
} }
private string currentFilter; private string currentFilter;
private Coroutine refreshCoroutine;
private readonly Stopwatch traversedThisFrame = new();
public TransformTree(ScrollPool<TransformCell> scrollPool, Func<IEnumerable<GameObject>> getRootEntriesMethod) public TransformTree(ScrollPool<TransformCell> scrollPool, Func<IEnumerable<GameObject>> getRootEntriesMethod)
{ {
ScrollPool = scrollPool; ScrollPool = scrollPool;
GetRootEntriesMethod = getRootEntriesMethod; GetRootEntriesMethod = getRootEntriesMethod;
} }
public void OnCellBorrowed(TransformCell cell)
{
cell.OnExpandToggled += OnCellExpandToggled;
cell.OnGameObjectClicked += OnGameObjectClicked;
cell.OnEnableToggled += OnCellEnableToggled;
}
private void OnGameObjectClicked(GameObject obj)
{
if (OnClickOverrideHandler != null)
OnClickOverrideHandler.Invoke(obj);
else
InspectorManager.Inspect(obj);
}
public void OnCellExpandToggled(CachedTransform cache)
{
var instanceID = cache.InstanceID;
if (expandedInstanceIDs.Contains(instanceID))
expandedInstanceIDs.Remove(instanceID);
else
expandedInstanceIDs.Add(instanceID);
RefreshData(true);
}
public void OnCellEnableToggled(CachedTransform cache)
{
cache.Value.gameObject.SetActive(!cache.Value.gameObject.activeSelf);
RefreshData(true);
}
public void Init() public void Init()
{ {
ScrollPool.Initialize(this); ScrollPool.Initialize(this);
@ -129,7 +97,7 @@ namespace UnityExplorer.UI.Widgets
} }
// Refresh cached transforms (no UI rebuild yet) // Refresh cached transforms (no UI rebuild yet)
RefreshData(false); RefreshData(false, false, false);
int transformID = transform.GetInstanceID(); int transformID = transform.GetInstanceID();
@ -167,57 +135,82 @@ namespace UnityExplorer.UI.Widgets
autoExpandedIDs.Clear(); autoExpandedIDs.Clear();
expandedInstanceIDs.Clear(); expandedInstanceIDs.Clear();
RefreshData(true, true); RefreshData(true, true, true);
} }
public void RefreshData(bool andReload = false, bool jumpToTop = false) public void RefreshData(bool andRefreshUI, bool jumpToTop, bool stopExistingCoroutine)
{ {
if (refreshCoroutine != null)
{
if (stopExistingCoroutine)
{
RuntimeHelper.StopCoroutine(refreshCoroutine);
refreshCoroutine = null;
}
else
return;
}
visited.Clear(); visited.Clear();
displayIndex = 0; displayIndex = 0;
needRefresh = false; needRefresh = false;
traversedThisFrame.Reset();
traversedThisFrame.Start();
var rootObjects = GetRootEntriesMethod.Invoke(); IEnumerable<GameObject> rootObjects = GetRootEntriesMethod.Invoke();
//int displayIndex = 0; refreshCoroutine = RuntimeHelper.StartCoroutine(RefreshCoroutine(rootObjects, andRefreshUI, jumpToTop));
foreach (var obj in rootObjects) }
if (obj) Traverse(obj.transform);
private IEnumerator RefreshCoroutine(IEnumerable<GameObject> rootObjects, bool andRefreshUI, bool jumpToTop)
{
var thisCoro = refreshCoroutine;
foreach (var gameObj in rootObjects)
{
if (gameObj)
{
var enumerator = Traverse(gameObj.transform);
while (enumerator.MoveNext())
yield return enumerator.Current;
}
}
// Prune displayed transforms that we didnt visit in that traverse // Prune displayed transforms that we didnt visit in that traverse
for (int i = cachedTransforms.Count - 1; i >= 0; i--) for (int i = cachedTransforms.Count - 1; i >= 0; i--)
{ {
var obj = (CachedTransform)cachedTransforms[i]; var cached = (CachedTransform)cachedTransforms[i];
if (!visited.Contains(obj.InstanceID)) if (!visited.Contains(cached.InstanceID))
{ {
cachedTransforms.Remove(obj.InstanceID); cachedTransforms.RemoveAt(i);
needRefresh = true; needRefresh = true;
} }
} }
if (!needRefresh) if (andRefreshUI && needRefresh)
return; ScrollPool.Refresh(true, jumpToTop);
//displayedObjects.Clear(); prevDisplayIndex = displayIndex;
if (andReload)
{
if (!jumpToTop)
ScrollPool.Refresh(true);
else
ScrollPool.Refresh(true, true);
}
} }
private void Traverse(Transform transform, CachedTransform parent = null, int depth = 0) private IEnumerator Traverse(Transform transform, CachedTransform parent = null, int depth = 0)
{ {
// Let's only tank 2ms of each frame (60->53fps)
if (traversedThisFrame.ElapsedMilliseconds > 2)
{
yield return null;
traversedThisFrame.Reset();
traversedThisFrame.Start();
}
int instanceID = transform.GetInstanceID(); int instanceID = transform.GetInstanceID();
if (visited.Contains(instanceID)) if (visited.Contains(instanceID))
return; yield break;
if (Filtering) if (Filtering)
{ {
if (!FilterHierarchy(transform)) if (!FilterHierarchy(transform))
return; yield break;
visited.Add(instanceID); visited.Add(instanceID);
@ -231,8 +224,21 @@ namespace UnityExplorer.UI.Widgets
if (cachedTransforms.Contains(instanceID)) if (cachedTransforms.Contains(instanceID))
{ {
cached = (CachedTransform)cachedTransforms[(object)instanceID]; cached = (CachedTransform)cachedTransforms[(object)instanceID];
int prevSiblingIdx = cached.SiblingIndex;
if (cached.Update(transform, depth)) if (cached.Update(transform, depth))
{
needRefresh = true; needRefresh = true;
// If the sibling index changed, we need to shuffle it in our cached transforms list.
if (prevSiblingIdx != cached.SiblingIndex)
{
cachedTransforms.Remove(instanceID);
if (cachedTransforms.Count <= displayIndex)
cachedTransforms.Add(instanceID, cached);
else
cachedTransforms.Insert(displayIndex, instanceID, cached);
}
}
} }
else else
{ {
@ -249,7 +255,11 @@ namespace UnityExplorer.UI.Widgets
if (IsCellExpanded(instanceID) && cached.Value.childCount > 0) if (IsCellExpanded(instanceID) && cached.Value.childCount > 0)
{ {
for (int i = 0; i < transform.childCount; i++) for (int i = 0; i < transform.childCount; i++)
Traverse(transform.GetChild(i), cached, depth + 1); {
var enumerator = Traverse(transform.GetChild(i), cached, depth + 1);
while (enumerator.MoveNext())
yield return enumerator.Current;
}
} }
} }
@ -276,13 +286,44 @@ namespace UnityExplorer.UI.Widgets
if (Filtering) if (Filtering)
{ {
if (cell.cachedTransform.Name.ContainsIgnoreCase(currentFilter)) if (cell.cachedTransform.Name.ContainsIgnoreCase(currentFilter))
{
cell.NameButton.ButtonText.color = Color.green; cell.NameButton.ButtonText.color = Color.green;
} }
} }
}
else else
cell.Disable(); cell.Disable();
} }
public void OnCellBorrowed(TransformCell cell)
{
cell.OnExpandToggled += OnCellExpandToggled;
cell.OnGameObjectClicked += OnGameObjectClicked;
cell.OnEnableToggled += OnCellEnableToggled;
}
private void OnGameObjectClicked(GameObject obj)
{
if (OnClickOverrideHandler != null)
OnClickOverrideHandler.Invoke(obj);
else
InspectorManager.Inspect(obj);
}
public void OnCellExpandToggled(CachedTransform cache)
{
var instanceID = cache.InstanceID;
if (expandedInstanceIDs.Contains(instanceID))
expandedInstanceIDs.Remove(instanceID);
else
expandedInstanceIDs.Add(instanceID);
RefreshData(true, false, true);
}
public void OnCellEnableToggled(CachedTransform cache)
{
cache.Value.gameObject.SetActive(!cache.Value.gameObject.activeSelf);
RefreshData(true, false, true);
}
} }
} }