From ed700cb0705495f8e76658b056688a84e1b3d547 Mon Sep 17 00:00:00 2001 From: Wojciech Figat Date: Fri, 9 Dec 2022 15:46:01 +0100 Subject: [PATCH] Add **Assets profiler** to Editor --- Source/Editor/Windows/Profiler/Assets.cs | 293 ++++++++++++++++++ .../Editor/Windows/Profiler/ProfilerWindow.cs | 3 + Source/Engine/Content/Asset.cpp | 11 + Source/Engine/Content/Asset.h | 9 +- Source/Engine/Core/Delegate.h | 8 + Source/Engine/Streaming/Streaming.cpp | 6 + 6 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 Source/Editor/Windows/Profiler/Assets.cs diff --git a/Source/Editor/Windows/Profiler/Assets.cs b/Source/Editor/Windows/Profiler/Assets.cs new file mode 100644 index 000000000..9ca0e844e --- /dev/null +++ b/Source/Editor/Windows/Profiler/Assets.cs @@ -0,0 +1,293 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text; +using FlaxEditor.GUI; +using FlaxEngine; +using FlaxEngine.Json; +using FlaxEngine.GUI; +using FlaxEditor.GUI.ContextMenu; + +namespace FlaxEditor.Windows.Profiler +{ + /// + /// The Assets profiling mode. + /// + /// + internal sealed class Assets : ProfilerMode + { + private struct Resource + { + public string Name; + public string TypeName; + public int ReferencesCount; + public ulong MemoryUsage; + public Guid AssetId; + } + + private readonly SingleChart _memoryUsageChart; + private readonly Table _table; + private SamplesBuffer _resources; + private List _tableRowsCache; + private Dictionary _resourceCache; + private StringBuilder _stringBuilder; + + public Assets() + : base("Assets") + { + // Layout + var panel = new Panel(ScrollBars.Vertical) + { + AnchorPreset = AnchorPresets.StretchAll, + Offsets = Margin.Zero, + Parent = this, + }; + var layout = new VerticalPanel + { + AnchorPreset = AnchorPresets.HorizontalStretchTop, + Offsets = Margin.Zero, + IsScrollable = true, + Parent = panel, + }; + + // Chart + _memoryUsageChart = new SingleChart + { + Title = "Assets Memory Usage (CPU)", + FormatSample = v => Utilities.Utils.FormatBytesCount((int)v), + Parent = layout, + }; + _memoryUsageChart.SelectedSampleChanged += OnSelectedSampleChanged; + + // Table + var headerColor = Style.Current.LightBackground; + _table = new Table + { + Columns = new[] + { + new ColumnDefinition + { + UseExpandCollapseMode = true, + CellAlignment = TextAlignment.Near, + Title = "Resource", + TitleBackgroundColor = headerColor, + }, + new ColumnDefinition + { + Title = "Type", + CellAlignment = TextAlignment.Center, + TitleBackgroundColor = headerColor, + }, + new ColumnDefinition + { + Title = "References", + TitleBackgroundColor = headerColor, + }, + new ColumnDefinition + { + Title = "Memory Usage", + TitleBackgroundColor = headerColor, + FormatValue = v => Utilities.Utils.FormatBytesCount((ulong)v), + }, + }, + Parent = layout, + }; + _table.Splits = new[] + { + 0.6f, + 0.2f, + 0.08f, + 0.12f, + }; + } + + /// + public override void Clear() + { + _memoryUsageChart.Clear(); + _resources?.Clear(); + _resourceCache?.Clear(); + } + + /// + public override void Update(ref SharedUpdateData sharedData) + { + if (_resourceCache == null) + _resourceCache = new Dictionary(); + if (_stringBuilder == null) + _stringBuilder = new StringBuilder(); + + // Capture current assets usage info + var assets = FlaxEngine.Content.Assets; + var sb = _stringBuilder; + var resources = new Resource[assets.Length]; + var contentDatabase = Editor.Instance.ContentDatabase; + ulong totalMemoryUsage = 0; + for (int i = 0; i < resources.Length; i++) + { + var asset = assets[i]; + if (!asset) + continue; + + // Try to reuse cached resource info + var assetId = asset.ID; + if (!_resourceCache.TryGetValue(assetId, out var resource)) + { + resource = new Resource + { + Name = asset.Path, + TypeName = asset.TypeName, + AssetId = assetId, + }; + var typeNameEnding = asset.TypeName.LastIndexOf('.'); + if (typeNameEnding != -1) + resource.TypeName = resource.TypeName.Substring(typeNameEnding + 1); + var assetItem = Editor.Instance.ContentDatabase.FindAsset(assetId); + if (assetItem != null) + { + resource.Name = assetItem.NamePath; + } + if (string.IsNullOrEmpty(resource.Name) && asset.IsVirtual) + resource.Name = ""; + _resourceCache.Add(assetId, resource); + } + + resource.MemoryUsage = asset.MemoryUsage; + totalMemoryUsage += resource.MemoryUsage; + resource.ReferencesCount = asset.ReferencesCount; + resources[i] = resource; + } + _memoryUsageChart.AddSample((float)totalMemoryUsage); + Array.Sort(resources, SortResources); + if (_resources == null) + _resources = new SamplesBuffer(); + _resources.Add(resources); + } + + /// + public override void UpdateView(int selectedFrame, bool showOnlyLastUpdateEvents) + { + _memoryUsageChart.SelectedSampleIndex = selectedFrame; + + if (_resources == null) + return; + if (_tableRowsCache == null) + _tableRowsCache = new List(); + UpdateTable(); + } + + /// + public override void OnDestroy() + { + _resources?.Clear(); + _resourceCache?.Clear(); + _tableRowsCache?.Clear(); + _stringBuilder?.Clear(); + + base.OnDestroy(); + } + + private static int SortResources(Resource a, Resource b) + { + return (int)(b.MemoryUsage - a.MemoryUsage); + } + + private void UpdateTable() + { + _table.IsLayoutLocked = true; + int idx = 0; + while (_table.Children.Count > idx) + { + var child = _table.Children[idx]; + if (child is ClickableRow row) + { + _tableRowsCache.Add(row); + child.Parent = null; + } + else + { + idx++; + } + } + _table.LockChildrenRecursive(); + + UpdateTableInner(); + + _table.UnlockChildrenRecursive(); + _table.PerformLayout(); + } + + private void UpdateTableInner() + { + if (_resources.Count == 0) + return; + var resources = _resources.Get(_memoryUsageChart.SelectedSampleIndex); + if (resources == null || resources.Length == 0) + return; + + // Add rows + var rowColor2 = Style.Current.Background * 1.4f; + for (int i = 0; i < resources.Length; i++) + { + ref var e = ref resources[i]; + + ClickableRow row; + if (_tableRowsCache.Count != 0) + { + // Reuse row + var last = _tableRowsCache.Count - 1; + row = _tableRowsCache[last]; + _tableRowsCache.RemoveAt(last); + } + else + { + // Allocate new row + row = new ClickableRow { Values = new object[4] }; + row.RowDoubleClick = OnRowDoubleClick; + row.RowRightClick = OnRowRightClick; + } + + // Setup row data + row.Tag = e.AssetId; + row.Values[0] = e.Name; + row.Values[1] = e.TypeName; + row.Values[2] = e.ReferencesCount; + row.Values[3] = e.MemoryUsage; + + // Add row to the table + row.Width = _table.Width; + row.BackgroundColor = i % 2 == 0 ? rowColor2 : Color.Transparent; + row.Parent = _table; + } + } + + private void OnRowDoubleClick(ClickableRow row) + { + var assetId = (Guid)row.Tag; + var assetItem = Editor.Instance.ContentDatabase.FindAsset(assetId); + if (assetItem != null) + Editor.Instance.ContentEditing.Open(assetItem); + } + + private void OnRowRightClick(ClickableRow row) + { + var assetId = (Guid)row.Tag; + var assetItem = Editor.Instance.ContentDatabase.FindAsset(assetId); + if (assetItem != null) + { + var cm = new ContextMenu(); + ContextMenuButton b; + b = cm.AddButton("Open", () => Editor.Instance.ContentEditing.Open(assetItem)); + cm.AddButton("Show in content window", () => Editor.Instance.Windows.ContentWin.Select(assetItem)); + cm.AddButton("Show in explorer", () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(assetItem.Path))); + cm.AddButton("Select actors using this asset", () => Editor.Instance.SceneEditing.SelectActorsUsingAsset(assetItem.ID)); + cm.AddButton("Show asset references graph", () => Editor.Instance.Windows.Open(new AssetReferencesGraphWindow(Editor.Instance, assetItem))); + cm.AddButton("Copy name", () => Clipboard.Text = assetItem.NamePath); + cm.AddButton("Copy path", () => Clipboard.Text = assetItem.Path); + cm.AddButton("Copy asset ID", () => Clipboard.Text = JsonSerializer.GetStringID(assetItem.ID)); + cm.Show(row, row.PointFromScreen(Input.MouseScreenPosition)); + } + } + } +} diff --git a/Source/Editor/Windows/Profiler/ProfilerWindow.cs b/Source/Editor/Windows/Profiler/ProfilerWindow.cs index 827cc674f..a40c38ea1 100644 --- a/Source/Editor/Windows/Profiler/ProfilerWindow.cs +++ b/Source/Editor/Windows/Profiler/ProfilerWindow.cs @@ -195,6 +195,7 @@ namespace FlaxEditor.Windows.Profiler AddMode(new GPU()); AddMode(new MemoryGPU()); AddMode(new Memory()); + AddMode(new Assets()); AddMode(new Network()); // Init view @@ -206,6 +207,8 @@ namespace FlaxEditor.Windows.Profiler } UpdateButtons(); + + ScriptsBuilder.ScriptsReloadEnd += Clear; // Prevent crashes if any of the profiler tabs has some scripting types cached (eg. asset type info) } /// diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index a7aeeab3d..e761dfd1b 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -313,6 +313,17 @@ bool Asset::ShouldDeleteFileOnUnload() const #endif +uint64 Asset::GetMemoryUsage() const +{ + uint64 result = sizeof(Asset); + Locker.Lock(); + if (_loadingTask) + result += sizeof(ContentLoadTask); + result += (OnLoaded.Capacity() + OnReloading.Capacity() + OnUnloaded.Capacity()) * sizeof(EventType::FunctionType); + Locker.Unlock(); + return result; +} + void Asset::Reload() { // Virtual assets are memory-only so reloading them makes no sense diff --git a/Source/Engine/Content/Asset.h b/Source/Engine/Content/Asset.h index 6039b6b3a..410dca8ab 100644 --- a/Source/Engine/Content/Asset.h +++ b/Source/Engine/Content/Asset.h @@ -128,14 +128,17 @@ public: } #if USE_EDITOR - /// /// Determines whether this asset was marked to be deleted on unload. /// API_PROPERTY() bool ShouldDeleteFileOnUnload() const; - #endif + /// + /// Gets amount of CPU memory used by this resource (in bytes). It's a rough estimation. Memory may be fragmented, compressed or sub-allocated so the actual memory pressure from this resource may vary. + /// + API_PROPERTY() virtual uint64 GetMemoryUsage() const; + public: /// /// Reloads the asset. @@ -160,7 +163,6 @@ public: virtual void CancelStreaming(); #if USE_EDITOR - /// /// Gets the asset references. Supported only in Editor. /// @@ -184,7 +186,6 @@ public: /// /// The collection of the asset ids referenced by this asset. API_FUNCTION() Array GetReferences() const; - #endif /// diff --git a/Source/Engine/Core/Delegate.h b/Source/Engine/Core/Delegate.h index 57d685031..d8ae9a5ea 100644 --- a/Source/Engine/Core/Delegate.h +++ b/Source/Engine/Core/Delegate.h @@ -557,6 +557,14 @@ public: return count; } + /// + /// Gets the current capacity of delegate table (amount of function to store before resizing). + /// + int32 Capacity() const + { + return (int32)Platform::AtomicRead((intptr volatile*)&_size); + } + /// /// Determines whether any function is binded. /// diff --git a/Source/Engine/Streaming/Streaming.cpp b/Source/Engine/Streaming/Streaming.cpp index f7ad33dc0..d11724fe8 100644 --- a/Source/Engine/Streaming/Streaming.cpp +++ b/Source/Engine/Streaming/Streaming.cpp @@ -304,6 +304,9 @@ GPUSampler* Streaming::GetTextureGroupSampler(int32 index) if (!sampler) { sampler = GPUSampler::New(); +#if GPU_ENABLE_RESOURCE_NAMING + sampler->SetName(group.Name); +#endif sampler->Init(desc); TextureGroupSamplers[index] = sampler; } @@ -316,6 +319,9 @@ GPUSampler* Streaming::GetTextureGroupSampler(int32 index) if (!FallbackSampler) { FallbackSampler = GPUSampler::New(); +#if GPU_ENABLE_RESOURCE_NAMING + sampler->SetName(TEXT("FallbackSampler")); +#endif FallbackSampler->Init(GPUSamplerDescription::New(GPUSamplerFilter::Trilinear)); } sampler = FallbackSampler;