From 2dc404cbd36a2f4af8f5bb0a7a2723eefe7b800e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 22 May 2025 04:40:32 +0200 Subject: [PATCH] Add new memory profiler --- Source/Editor/Windows/Profiler/Memory.cs | 186 ++++++++- Source/Engine/Core/Types/StringView.h | 8 + Source/Engine/Engine/Engine.cpp | 10 + Source/Engine/Platform/Base/PlatformBase.cpp | 15 + Source/Engine/Platform/Base/PlatformBase.h | 2 +- Source/Engine/Profiler/Profiler.h | 1 + Source/Engine/Profiler/ProfilerMemory.cpp | 413 +++++++++++++++++++ Source/Engine/Profiler/ProfilerMemory.h | 258 ++++++++++++ 8 files changed, 891 insertions(+), 2 deletions(-) create mode 100644 Source/Engine/Profiler/ProfilerMemory.cpp create mode 100644 Source/Engine/Profiler/ProfilerMemory.h diff --git a/Source/Editor/Windows/Profiler/Memory.cs b/Source/Editor/Windows/Profiler/Memory.cs index d7bdc43af..f33bec4cc 100644 --- a/Source/Editor/Windows/Profiler/Memory.cs +++ b/Source/Editor/Windows/Profiler/Memory.cs @@ -2,6 +2,8 @@ #if USE_PROFILER using System; +using System.Collections.Generic; +using FlaxEditor.GUI; using FlaxEngine; using FlaxEngine.GUI; @@ -13,9 +15,21 @@ namespace FlaxEditor.Windows.Profiler /// internal sealed class Memory : ProfilerMode { + private struct FrameData + { + public ProfilerMemory.GroupsArray Usage; + public ProfilerMemory.GroupsArray Peek; + public ProfilerMemory.GroupsArray Count; + } + private readonly SingleChart _nativeAllocationsChart; private readonly SingleChart _managedAllocationsChart; - + private readonly Table _table; + private SamplesBuffer _frames; + private List _tableRowsCache; + private string[] _groupNames; + private int[] _groupOrder; + public Memory() : base("Memory") { @@ -50,6 +64,58 @@ namespace FlaxEditor.Windows.Profiler Parent = layout, }; _managedAllocationsChart.SelectedSampleChanged += OnSelectedSampleChanged; + + // Table + var style = Style.Current; + var headerColor = style.LightBackground; + var textColor = style.Foreground; + _table = new Table + { + Columns = new[] + { + new ColumnDefinition + { + UseExpandCollapseMode = true, + CellAlignment = TextAlignment.Near, + Title = "Group", + TitleBackgroundColor = headerColor, + TitleColor = textColor, + }, + new ColumnDefinition + { + Title = "Usage", + TitleBackgroundColor = headerColor, + FormatValue = FormatCellBytes, + TitleColor = textColor, + }, + new ColumnDefinition + { + Title = "Peek", + TitleBackgroundColor = headerColor, + FormatValue = FormatCellBytes, + TitleColor = textColor, + }, + new ColumnDefinition + { + Title = "Count", + TitleBackgroundColor = headerColor, + TitleColor = textColor, + }, + }, + Parent = layout, + }; + _table.Splits = new[] + { + 0.5f, + 0.2f, + 0.2f, + 0.1f, + }; + } + + private string FormatCellBytes(object x) + { + return Utilities.Utils.FormatBytesCount(Convert.ToUInt64(x)); } /// @@ -57,6 +123,7 @@ namespace FlaxEditor.Windows.Profiler { _nativeAllocationsChart.Clear(); _managedAllocationsChart.Clear(); + _frames?.Clear(); } /// @@ -84,6 +151,19 @@ namespace FlaxEditor.Windows.Profiler _nativeAllocationsChart.AddSample(nativeMemoryAllocation); _managedAllocationsChart.AddSample(managedMemoryAllocation); + + // Gather memory profiler stats for groups + var frame = new FrameData + { + Usage = ProfilerMemory.GetGroups(0), + Peek = ProfilerMemory.GetGroups(1), + Count = ProfilerMemory.GetGroups(2), + }; + if (_frames == null) + _frames = new SamplesBuffer(); + if (_groupNames == null) + _groupNames = ProfilerMemory.GetGroupNames(); + _frames.Add(frame); } /// @@ -91,6 +171,110 @@ namespace FlaxEditor.Windows.Profiler { _nativeAllocationsChart.SelectedSampleIndex = selectedFrame; _managedAllocationsChart.SelectedSampleIndex = selectedFrame; + + UpdateTable(selectedFrame); + } + + /// + public override void OnDestroy() + { + _tableRowsCache?.Clear(); + _groupNames = null; + _groupOrder = null; + + base.OnDestroy(); + } + + private void UpdateTable(int selectedFrame) + { + if (_frames == null) + return; + if (_tableRowsCache == null) + _tableRowsCache = new List(); + _table.IsLayoutLocked = true; + + RecycleTableRows(_table, _tableRowsCache); + UpdateTableInner(selectedFrame); + + _table.UnlockChildrenRecursive(); + _table.PerformLayout(); + } + + private unsafe void UpdateTableInner(int selectedFrame) + { + if (_frames.Count == 0) + return; + var frame = _frames.Get(selectedFrame); + var totalUage = frame.Usage.Values0[(int)ProfilerMemory.Groups.TotalTracked]; + var totalPeek = frame.Peek.Values0[(int)ProfilerMemory.Groups.TotalTracked]; + var totalCount = frame.Count.Values0[(int)ProfilerMemory.Groups.TotalTracked]; + + // Sort by memory size + if (_groupOrder == null) + _groupOrder = new int[(int)ProfilerMemory.Groups.MAX]; + for (int i = 0; i < (int)ProfilerMemory.Groups.MAX; i++) + _groupOrder[i] = i; + Array.Sort(_groupOrder, (x, y) => + { + var tmp = _frames.Get(selectedFrame); + return (int)(tmp.Usage.Values0[y] - tmp.Usage.Values0[x]); + }); + + // Add rows + var rowColor2 = Style.Current.Background * 1.4f; + for (int i = 0; i < (int)ProfilerMemory.Groups.MAX; i++) + { + var group = _groupOrder[i]; + var groupUsage = frame.Usage.Values0[group]; + if (groupUsage <= 0) + continue; + var groupPeek = frame.Peek.Values0[group]; + var groupCount = frame.Count.Values0[group]; + + Row row; + if (_tableRowsCache.Count != 0) + { + var last = _tableRowsCache.Count - 1; + row = _tableRowsCache[last]; + _tableRowsCache.RemoveAt(last); + } + else + { + row = new Row + { + Values = new object[4], + BackgroundColors = new Color[4], + }; + } + { + // Group + row.Values[0] = _groupNames[group]; + + // Usage + row.Values[1] = groupUsage; + row.BackgroundColors[1] = Color.Red.AlphaMultiplied(Mathf.Min(1, (float)groupUsage / totalUage) * 0.5f); + + // Peek + row.Values[2] = groupPeek; + row.BackgroundColors[2] = Color.Red.AlphaMultiplied(Mathf.Min(1, (float)groupPeek / totalPeek) * 0.5f); + + // Count + row.Values[3] = groupCount; + row.BackgroundColors[3] = Color.Red.AlphaMultiplied(Mathf.Min(1, (float)groupCount / totalCount) * 0.5f); + } + row.Width = _table.Width; + row.BackgroundColor = i % 2 == 1 ? rowColor2 : Color.Transparent; + row.Parent = _table; + + var useBackground = group != (int)ProfilerMemory.Groups.Total && + group != (int)ProfilerMemory.Groups.TotalTracked && + group != (int)ProfilerMemory.Groups.Malloc; + if (!useBackground) + { + for (int k = 1; k < row.BackgroundColors.Length; k++) + row.BackgroundColors[k] = Color.Transparent; + } + } } } } diff --git a/Source/Engine/Core/Types/StringView.h b/Source/Engine/Core/Types/StringView.h index 25d156c64..cbc221e7b 100644 --- a/Source/Engine/Core/Types/StringView.h +++ b/Source/Engine/Core/Types/StringView.h @@ -208,6 +208,14 @@ public: return StringUtils::CompareIgnoreCase(&(*this)[Length() - suffix.Length()], *suffix) == 0; return StringUtils::Compare(&(*this)[Length() - suffix.Length()], *suffix) == 0; } + + bool Contains(const T* subStr, StringSearchCase searchCase = StringSearchCase::CaseSensitive) const + { + const int32 length = Length(); + if (subStr == nullptr || length == 0) + return false; + return (searchCase == StringSearchCase::IgnoreCase ? StringUtils::FindIgnoreCase(_data, subStr) : StringUtils::Find(_data, subStr)) != nullptr; + } }; /// diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp index 385a05554..288e0da11 100644 --- a/Source/Engine/Engine/Engine.cpp +++ b/Source/Engine/Engine/Engine.cpp @@ -79,6 +79,11 @@ Window* Engine::MainWindow = nullptr; int32 Engine::Main(const Char* cmdLine) { +#if COMPILE_WITH_PROFILER + extern void InitProfilerMemory(const Char*); + InitProfilerMemory(cmdLine); +#endif + PROFILE_MEM_BEGIN(Engine); EngineImpl::CommandLine = cmdLine; Globals::MainThreadID = Platform::GetCurrentThreadID(); StartupTime = DateTime::Now(); @@ -164,6 +169,7 @@ int32 Engine::Main(const Char* cmdLine) LOG_FLUSH(); Time::Synchronize(); EngineImpl::IsReady = true; + PROFILE_MEM_END(); // Main engine loop const bool useSleep = true; // TODO: this should probably be a platform setting @@ -204,6 +210,10 @@ int32 Engine::Main(const Char* cmdLine) { PROFILE_CPU_NAMED("Platform.Tick"); Platform::Tick(); +#if COMPILE_WITH_PROFILER + extern void TickProfilerMemory(); + TickProfilerMemory(); +#endif } // Update game logic diff --git a/Source/Engine/Platform/Base/PlatformBase.cpp b/Source/Engine/Platform/Base/PlatformBase.cpp index 65f995967..9ba6b7bd6 100644 --- a/Source/Engine/Platform/Base/PlatformBase.cpp +++ b/Source/Engine/Platform/Base/PlatformBase.cpp @@ -17,6 +17,7 @@ #include "Engine/Core/Utilities.h" #if COMPILE_WITH_PROFILER #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Profiler/ProfilerMemory.h" #endif #include "Engine/Threading/Threading.h" #include "Engine/Engine/CommandLine.h" @@ -218,6 +219,10 @@ void PlatformBase::OnMemoryAlloc(void* ptr, uint64 size) tracy::Profiler::MemAllocCallstack(ptr, (size_t)size, 12, false); #endif + // Register in memory profiler + if (ProfilerMemory::Enabled) + ProfilerMemory::OnMemoryAlloc(ptr, size); + // Register allocation during the current CPU event auto thread = ProfilerCPU::GetCurrentThread(); if (thread != nullptr && thread->Buffer.GetCount() != 0) @@ -235,6 +240,10 @@ void PlatformBase::OnMemoryFree(void* ptr) if (!ptr) return; + // Register in memory profiler + if (ProfilerMemory::Enabled) + ProfilerMemory::OnMemoryFree(ptr); + #if TRACY_ENABLE_MEMORY // Track memory allocation in Tracy tracy::Profiler::MemFree(ptr, false); @@ -372,6 +381,12 @@ void PlatformBase::Fatal(const StringView& msg, void* context, FatalErrorType er LOG(Error, "External Used Physical Memory: {0} ({1}%)", Utilities::BytesToText(externalUsedPhysical), (int32)(100 * externalUsedPhysical / memoryStats.TotalPhysicalMemory)); LOG(Error, "External Used Virtual Memory: {0} ({1}%)", Utilities::BytesToText(externalUsedVirtual), (int32)(100 * externalUsedVirtual / memoryStats.TotalVirtualMemory)); } +#if COMPILE_WITH_PROFILER + if (error == FatalErrorType::OutOfMemory || error == FatalErrorType::GPUOutOfMemory) + { + ProfilerMemory::Dump(); + } +#endif } if (Log::Logger::LogFilePath.HasChars()) { diff --git a/Source/Engine/Platform/Base/PlatformBase.h b/Source/Engine/Platform/Base/PlatformBase.h index fb2c68099..ff47e77a3 100644 --- a/Source/Engine/Platform/Base/PlatformBase.h +++ b/Source/Engine/Platform/Base/PlatformBase.h @@ -286,7 +286,7 @@ public: /// /// A pointer to the first operand. This value will be replaced with the result of the operation. /// The second operand. - /// The result value of the operation. + /// The original value of the dst parameter. static int64 InterlockedAdd(int64 volatile* dst, int64 value) = delete; /// diff --git a/Source/Engine/Profiler/Profiler.h b/Source/Engine/Profiler/Profiler.h index f0f7aad05..b80541719 100644 --- a/Source/Engine/Profiler/Profiler.h +++ b/Source/Engine/Profiler/Profiler.h @@ -4,6 +4,7 @@ #include "ProfilerCPU.h" #include "ProfilerGPU.h" +#include "ProfilerMemory.h" #if COMPILE_WITH_PROFILER diff --git a/Source/Engine/Profiler/ProfilerMemory.cpp b/Source/Engine/Profiler/ProfilerMemory.cpp new file mode 100644 index 000000000..e617d712c --- /dev/null +++ b/Source/Engine/Profiler/ProfilerMemory.cpp @@ -0,0 +1,413 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#if COMPILE_WITH_PROFILER + +#include "ProfilerMemory.h" +#include "Engine/Core/Log.h" +#include "Engine/Core/Utilities.h" +#include "Engine/Core/Math/Math.h" +#include "Engine/Core/Types/StringBuilder.h" +#include "Engine/Core/Collections/Sorting.h" +#include "Engine/Core/Collections/Dictionary.h" +#include "Engine/Platform/MemoryStats.h" +#include "Engine/Platform/File.h" +#include "Engine/Scripting/Enums.h" +#include "Engine/Threading/ThreadLocal.h" +#include "Engine/Utilities/StringConverter.h" + +#define GROUPS_COUNT (int32)ProfilerMemory::Groups::MAX + +static_assert(GROUPS_COUNT <= MAX_uint8, "Fix memory profiler groups to fit a single byte."); + +// Compact name storage. +struct GroupNameBuffer +{ + Char Buffer[30]; + + template + void Set(const T* str) + { + int32 max = StringUtils::Length(str), dst = 0; + char prev = 0; + for (int32 i = 0; i < max && dst < ARRAY_COUNT(Buffer) - 2; i++) + { + char cur = str[i]; + if (StringUtils::IsUpper(cur) && StringUtils::IsLower(prev)) + Buffer[dst++] = '/'; + Buffer[dst++] = cur; + prev = cur; + } + Buffer[dst] = 0; + } +}; + +// Compact groups stack container. +struct GroupStackData +{ + uint8 Count : 7; + uint8 SkipRecursion : 1; + uint8 Stack[15]; + + FORCE_INLINE void Push(ProfilerMemory::Groups group) + { + if (Count < ARRAY_COUNT(Stack)) + Count++; + else + { + int a= 10; + } + Stack[Count - 1] = (uint8)group; + } + + FORCE_INLINE void Pop() + { + if (Count > 0) + Count--; + } + + FORCE_INLINE ProfilerMemory::Groups Peek() const + { + return Count > 0 ? (ProfilerMemory::Groups)Stack[Count - 1] : ProfilerMemory::Groups::Unknown; + } +}; + +template<> +struct TIsPODType +{ + enum { Value = true }; +}; + +// Memory allocation data for a specific pointer. +struct PointerData +{ + uint32 Size; + uint8 Group; +}; + +template<> +struct TIsPODType +{ + enum { Value = true }; +}; + +#define UPDATE_PEEK(group) Platform::AtomicStore(&GroupMemoryPeek[(int32)group], Math::Max(Platform::AtomicRead(&GroupMemory[(int32)group]), Platform::AtomicRead(&GroupMemoryPeek[(int32)group]))) + +namespace +{ + alignas(16) volatile int64 GroupMemory[GROUPS_COUNT] = {}; + alignas(16) volatile int64 GroupMemoryPeek[GROUPS_COUNT] = {}; + alignas(16) volatile int64 GroupMemoryCount[GROUPS_COUNT] = {}; + uint8 GroupParents[GROUPS_COUNT] = {}; + ThreadLocal GroupStack; + GroupNameBuffer GroupNames[GROUPS_COUNT]; + bool InitedNames = false; + CriticalSection PointersLocker; + Dictionary Pointers; + + void InitNames() + { + if (InitedNames) + return; + InitedNames = true; + for (int32 i = 0; i < GROUPS_COUNT; i++) + { + const char* name = ScriptingEnum::GetName((ProfilerMemory::Groups)i); + GroupNames[i].Set(name); + } + + // Init constant memory + PROFILE_MEM_INC(ProgramSize, Platform::GetMemoryStats().ProgramSizeMemory); + UPDATE_PEEK(ProfilerMemory::Groups::ProgramSize); + } + + void Dump(StringBuilder& output, const int32 maxCount) + { + InitNames(); + + // Sort groups + struct GroupInfo + { + ProfilerMemory::Groups Group; + int64 Size; + int64 Peek; + uint32 Count; + + bool operator<(const GroupInfo& other) const + { + return Size > other.Size; + } + }; + GroupInfo groups[GROUPS_COUNT]; + for (int32 i = 0; i < GROUPS_COUNT; i++) + { + GroupInfo& group = groups[i]; + group.Group = (ProfilerMemory::Groups)i; + group.Size = Platform::AtomicRead(&GroupMemory[i]); + group.Peek = Platform::AtomicRead(&GroupMemoryPeek[i]); + group.Count = (uint32)Platform::AtomicRead(&GroupMemoryCount[i]); + } + Sorting::QuickSort(groups, GROUPS_COUNT); + + // Print groups + output.Append(TEXT("Memory profiler summary:")).AppendLine(); + for (int32 i = 0; i < maxCount; i++) + { + const GroupInfo& group = groups[i]; + if (group.Size == 0) + break; + const Char* name = GroupNames[(int32)group.Group].Buffer; + const String size = Utilities::BytesToText(group.Size); + const String peek = Utilities::BytesToText(group.Peek); + output.AppendFormat(TEXT("{:>30}: {:>11} (peek: {}, count: {})"), name, size.Get(), peek.Get(), group.Count); + output.AppendLine(); + } + +#if 0 + // Print count of memory allocs count per group + for (int32 i = 0; i < GROUPS_COUNT; i++) + { + GroupInfo& group = groups[i]; + group.Group = (ProfilerMemory::Groups)i; + group.Size = 0; + } + PointersLocker.Lock(); + for (auto& e : Pointers) + groups[e.Value.Group].Size++; + PointersLocker.Unlock(); + Sorting::QuickSort(groups, GROUPS_COUNT); + output.Append(TEXT("Memory allocations count summary:")).AppendLine(); + for (int32 i = 0; i < maxCount; i++) + { + const GroupInfo& group = groups[i]; + if (group.Size == 0) + break; + const Char* name = GroupName[(int32)group.Group].Buffer; + output.AppendFormat(TEXT("{:>30}: {:>11}"), name, group.Size); + output.AppendLine(); + } +#endif + } + + FORCE_INLINE void AddGroupMemory(ProfilerMemory::Groups group, int64 add) + { + // Group itself + Platform::InterlockedAdd(&GroupMemory[(int32)group], add); + Platform::InterlockedIncrement(&GroupMemoryCount[(int32)group]); + UPDATE_PEEK(group); + + // Total memory + Platform::InterlockedAdd(&GroupMemory[(int32)ProfilerMemory::Groups::TotalTracked], add); + Platform::InterlockedIncrement(&GroupMemoryCount[(int32)ProfilerMemory::Groups::TotalTracked]); + UPDATE_PEEK(ProfilerMemory::Groups::TotalTracked); + + // Group hierarchy parents + uint8 parent = GroupParents[(int32)group]; + while (parent != 0) + { + Platform::InterlockedAdd(&GroupMemory[parent], add); + Platform::InterlockedIncrement(&GroupMemoryCount[parent]); + UPDATE_PEEK(parent); + parent = GroupParents[parent]; + } + } + + FORCE_INLINE void SubGroupMemory(ProfilerMemory::Groups group, int64 add) + { + // Group itself + int64 value = Platform::InterlockedAdd(&GroupMemory[(int32)group], add); + Platform::InterlockedDecrement(&GroupMemoryCount[(int32)group]); + + // Total memory + value = Platform::InterlockedAdd(&GroupMemory[(int32)ProfilerMemory::Groups::TotalTracked], add); + Platform::InterlockedDecrement(&GroupMemoryCount[(int32)ProfilerMemory::Groups::TotalTracked]); + + // Group hierarchy parents + uint8 parent = GroupParents[(int32)group]; + while (parent != 0) + { + value = Platform::InterlockedAdd(&GroupMemory[parent], add); + Platform::InterlockedDecrement(&GroupMemoryCount[parent]); + parent = GroupParents[parent]; + } + } +} + +void InitProfilerMemory(const Char* cmdLine) +{ + // Check for command line option (memory profiling affects performance thus not active by default) + ProfilerMemory::Enabled = StringUtils::FindIgnoreCase(cmdLine, TEXT("-mem")); + + // Init hierarchy +#define INIT_PARENT(parent, child) GroupParents[(int32)ProfilerMemory::Groups::child] = (uint8)ProfilerMemory::Groups::parent + INIT_PARENT(Graphics, GraphicsTextures); + INIT_PARENT(Graphics, GraphicsBuffers); + INIT_PARENT(Graphics, GraphicsMeshes); + INIT_PARENT(Graphics, GraphicsShaders); + INIT_PARENT(Graphics, GraphicsMaterials); + INIT_PARENT(Graphics, GraphicsCommands); + INIT_PARENT(Animations, AnimationsData); + INIT_PARENT(Content, ContentAssets); + INIT_PARENT(Content, ContentFiles); +#undef INIT_PARENT +} + +void TickProfilerMemory() +{ + // Update profiler memory + PointersLocker.Lock(); + GroupMemory[(int32)ProfilerMemory::Groups::Profiler] = + sizeof(GroupMemory) + sizeof(GroupNames) + sizeof(GroupStack) + + Pointers.Capacity() * sizeof(Dictionary::Bucket); + PointersLocker.Unlock(); + + // Get total system memory and update untracked amount + auto memory = Platform::GetProcessMemoryStats(); + memory.UsedPhysicalMemory -= GroupMemory[(int32)ProfilerMemory::Groups::Profiler]; + GroupMemory[(int32)ProfilerMemory::Groups::Total] = memory.UsedPhysicalMemory; + GroupMemory[(int32)ProfilerMemory::Groups::TotalUntracked] = Math::Max(memory.UsedPhysicalMemory - GroupMemory[(int32)ProfilerMemory::Groups::TotalTracked], 0); + + // Update peeks + UPDATE_PEEK(ProfilerMemory::Groups::Profiler); + UPDATE_PEEK(ProfilerMemory::Groups::Total); + UPDATE_PEEK(ProfilerMemory::Groups::TotalUntracked); + GroupMemoryPeek[(int32)ProfilerMemory::Groups::Total] = Math::Max(GroupMemoryPeek[(int32)ProfilerMemory::Groups::Total], GroupMemoryPeek[(int32)ProfilerMemory::Groups::TotalTracked]); +} + +bool ProfilerMemory::Enabled = false; + +void ProfilerMemory::IncrementGroup(Groups group, uint64 size) +{ + AddGroupMemory(group, (int64)size); +} + +void ProfilerMemory::DecrementGroup(Groups group, uint64 size) +{ + SubGroupMemory(group, -(int64)size); +} + +void ProfilerMemory::BeginGroup(Groups group) +{ + auto& stack = GroupStack.Get(); + stack.Push(group); +} + +void ProfilerMemory::EndGroup() +{ + auto& stack = GroupStack.Get(); + stack.Pop(); +} + +void ProfilerMemory::RenameGroup(Groups group, const StringView& name) +{ + GroupNames[(int32)group].Set(name.Get()); +} + +Array ProfilerMemory::GetGroupNames() +{ + Array result; + result.Resize((int32)Groups::MAX); + InitNames(); + for (int32 i = 0; i < (int32)Groups::MAX; i++) + result[i] = GroupNames[i].Buffer; + return result; +} + +ProfilerMemory::GroupsArray ProfilerMemory::GetGroups(int32 mode) +{ + GroupsArray result; + Platform::MemoryClear(&result, sizeof(result)); + static_assert(ARRAY_COUNT(result.Values) >= (int32)Groups::MAX, "Update group array size."); + InitNames(); + if (mode == 0) + { + for (int32 i = 0; i < (int32)Groups::MAX; i++) + result.Values[i] = Platform::AtomicRead(&GroupMemory[i]); + } + else if (mode == 1) + { + for (int32 i = 0; i < (int32)Groups::MAX; i++) + result.Values[i] = Platform::AtomicRead(&GroupMemoryPeek[i]); + } + else if (mode == 2) + { + for (int32 i = 0; i < (int32)Groups::MAX; i++) + result.Values[i] = Platform::AtomicRead(&GroupMemoryCount[i]); + } + return result; +} + +void ProfilerMemory::Dump(const StringView& options) +{ + bool file = options.Contains(TEXT("file")); + StringBuilder output; + int32 maxCount = 20; + if (file || options.Contains(TEXT("all"))) + maxCount = MAX_int32; + ::Dump(output, maxCount); + if (file) + { + String path = String(StringUtils::GetDirectoryName(Log::Logger::LogFilePath)) / TEXT("Memory_") + DateTime::Now().ToFileNameString() + TEXT(".txt"); + File::WriteAllText(path, output, Encoding::ANSI); + LOG(Info, "Saved to {}", path); + return; + } + LOG_STR(Info, output.ToStringView()); +} + +void ProfilerMemory::OnMemoryAlloc(void* ptr, uint64 size) +{ + ASSERT_LOW_LAYER(Enabled && ptr); + auto& stack = GroupStack.Get(); + if (stack.SkipRecursion) + return; + stack.SkipRecursion = true; + + // Register pointer + PointerData ptrData; + ptrData.Size = size; + ptrData.Group = (uint8)stack.Peek(); + PointersLocker.Lock(); + Pointers[ptr] = ptrData; + PointersLocker.Unlock(); + + // Update group memory + const int64 add = (int64)size; + AddGroupMemory((Groups)ptrData.Group, add); + Platform::InterlockedAdd(&GroupMemory[(int32)ProfilerMemory::Groups::Malloc], add); + Platform::InterlockedIncrement(&GroupMemoryCount[(int32)ProfilerMemory::Groups::Malloc]); + UPDATE_PEEK(ProfilerMemory::Groups::Malloc); + + stack.SkipRecursion = false; +} + +void ProfilerMemory::OnMemoryFree(void* ptr) +{ + ASSERT_LOW_LAYER(Enabled && ptr); + auto& stack = GroupStack.Get(); + if (stack.SkipRecursion) + return; + stack.SkipRecursion = true; + + // Find and remove pointer + PointerData ptrData; + PointersLocker.Lock(); + auto it = Pointers.Find(ptr); + bool found = it.IsNotEnd(); + if (found) + ptrData = it->Value; + Pointers.Remove(it); + PointersLocker.Unlock(); + + if (found) + { + // Update group memory + const int64 add = -(int64)ptrData.Size; + SubGroupMemory((Groups)ptrData.Group, add); + Platform::InterlockedAdd(&GroupMemory[(int32)ProfilerMemory::Groups::Malloc], add); + Platform::InterlockedDecrement(&GroupMemoryCount[(int32)ProfilerMemory::Groups::Malloc]); + } + + stack.SkipRecursion = false; +} + +#endif diff --git a/Source/Engine/Profiler/ProfilerMemory.h b/Source/Engine/Profiler/ProfilerMemory.h new file mode 100644 index 000000000..65ed5d9ab --- /dev/null +++ b/Source/Engine/Profiler/ProfilerMemory.h @@ -0,0 +1,258 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Platform/Platform.h" + +#if COMPILE_WITH_PROFILER + +#include "Engine/Core/Types/StringView.h" + +/// +/// Provides memory allocations collecting utilities. +/// +API_CLASS(Static) class FLAXENGINE_API ProfilerMemory +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(ProfilerMemory); +public: + /// + /// List of different memory categories used to track and analyze memory allocations specific to a certain engine system. + /// + API_ENUM() enum class Groups : uint8 + { + // Not categorized. + Unknown, + // Total used system memory (reported by platform). + Total, + // Total amount of tracked memory (by ProfilerMemory tool). + TotalTracked, + // Total amount of untracked memory (gap between total system memory usage and tracked memory size). + TotalUntracked, + // Initial memory used by program upon startup (eg. executable size, static variables). + ProgramSize, + // Total memory allocated via malloc. + Malloc, + // General purpose engine memory. + Engine, + // Profiling tool memory overhead. + Profiler, + + // Total graphics memory usage. + Graphics, + // Total textures memory usage. + GraphicsTextures, + // Total buffers memory usage. + GraphicsBuffers, + // Total meshes memory usage (vertex and idnex buffers allocated by models). + GraphicsMeshes, + // Totoal shaders memory usage (shaders bytecode, PSOs data). + GraphicsShaders, + // Totoal materials memory usage (constant buffers, parameters data). + GraphicsMaterials, + // Totoal command buffers memory usage (draw lists, constants uploads, ring buffer allocators). + GraphicsCommands, + + // Total Artificial Intelligence systems memory usage (eg. Behavior Trees). + AI, + + // Total animations system memory usage. + Animations, + // Total animation data memory usage (curves, events, keyframes, graphs, etc.). + AnimationsData, + + // Total autio system memory. + Audio, + + // Total content system memory usage. + Content, + // Total general purpose memory allocated by assets. + ContentAssets, + // Total memory used by content files buffers (file reading and streaming buffers). + ContentFiles, + // Total memory used by content streaming system (internals). + ContentStreaming, + + // Total memory allocated by input system. + Input, + + // Total memory allocated by scene objects. + Level, + + // Total localization system memory. + Localization, + + // Total navigation system memory. + Navigation, + + // Total networking system memory. + Networking, + + // Total particles memory (loaded assets, particles buffers and instance parameters). + Particles, + + // Total physics memory. + Physics, + + // Total scripting memory allocated by game. + Scripting, + + // Total User Interface components memory. + UI, + + // Total video system memory (video file data, frame buffers, GPU images and any audio buffers used by video playback). + Video, + + // Custom game-specific memory tracking. + CustomGame0, + // Custom game-specific memory tracking. + CustomGame1, + // Custom game-specific memory tracking. + CustomGame2, + // Custom game-specific memory tracking. + CustomGame3, + // Custom game-specific memory tracking. + CustomGame4, + // Custom game-specific memory tracking. + CustomGame5, + // Custom game-specific memory tracking. + CustomGame6, + // Custom game-specific memory tracking. + CustomGame7, + // Custom game-specific memory tracking. + CustomGame8, + // Custom game-specific memory tracking. + CustomGame9, + + // Custom plugin-specific memory tracking. + CustomPlugin0, + // Custom plugin-specific memory tracking. + CustomPlugin1, + // Custom plugin-specific memory tracking. + CustomPlugin2, + // Custom plugin-specific memory tracking. + CustomPlugin3, + // Custom plugin-specific memory tracking. + CustomPlugin4, + // Custom plugin-specific memory tracking. + CustomPlugin5, + // Custom plugin-specific memory tracking. + CustomPlugin6, + // Custom plugin-specific memory tracking. + CustomPlugin7, + // Custom plugin-specific memory tracking. + CustomPlugin8, + // Custom plugin-specific memory tracking. + CustomPlugin9, + + // Total editor-specific memory. + Editor, + + MAX + }; + + /// + /// The memory groups array wraper to avoid dynamic memory allocation. + /// + API_STRUCT(NoDefault) struct GroupsArray + { + DECLARE_SCRIPTING_TYPE_MINIMAL(GroupsArray); + + // Values for each group + API_FIELD(NoArray) int64 Values[100]; + }; + +public: + /// + /// Increments memory usage by a specific group. + /// + /// The group to update. + /// The amount of memory allocated (in bytes). + API_FUNCTION() static void IncrementGroup(Groups group, uint64 size); + + /// + /// Decrements memory usage by a specific group. + /// + /// The group to update. + /// The amount of memory freed (in bytes). + API_FUNCTION() static void DecrementGroup(Groups group, uint64 size); + + /// + /// Enters a new group context scope (by the current thread). Informs the profiler about context of any memory allocations within. + /// + /// The group to enter. + API_FUNCTION() static void BeginGroup(Groups group); + + /// + /// Leaves the last group context scope (by the current thread). + /// + API_FUNCTION() static void EndGroup(); + + /// + /// Renames the group. Can be used for custom game/plugin groups naming. + /// + /// The group to update. + /// The new name to set. + API_FUNCTION() static void RenameGroup(Groups group, const StringView& name); + + /// + /// Gets the names of all groups (array matches Groups enums). + /// + API_FUNCTION() static Array GetGroupNames(); + + /// + /// Gets the memory stats for all groups (array matches Groups enums). + /// + /// 0 to get current memory, 1 to get peek memory, 2 to get current count. + API_FUNCTION() static GroupsArray GetGroups(int32 mode = 0); + + /// + /// Dumps the memory allocations stats (groupped). + /// + /// 'all' to dump all groups, 'file' to dump info to a file (in Logs folder) + API_FUNCTION(Attributes="DebugCommand") static void Dump(const StringView& options = StringView::Empty); + +public: + /// + /// The profiling tools usage flag. Can be used to disable profiler. Run engine with '-mem' command line to activate it from start. + /// + static bool Enabled; + + static void OnMemoryAlloc(void* ptr, uint64 size); + static void OnMemoryFree(void* ptr); + +public: + /// + /// Helper structure used to call begin/end on group within single code block. + /// + struct GroupScope + { + FORCE_INLINE GroupScope(Groups group) + { + if (ProfilerMemory::Enabled) + ProfilerMemory::BeginGroup(group); + } + + FORCE_INLINE ~GroupScope() + { + if (ProfilerMemory::Enabled) + ProfilerMemory::EndGroup(); + } + }; +}; + +#define PROFILE_MEM_INC(group, size) ProfilerMemory::IncrementGroup(ProfilerMemory::Groups::group, size) +#define PROFILE_MEM_DEC(group, size) ProfilerMemory::DecrementGroup(ProfilerMemory::Groups::group, size) +#define PROFILE_MEM(group) ProfilerMemory::GroupScope ProfileMem(ProfilerMemory::Groups::group) +#define PROFILE_MEM_BEGIN(group) if (ProfilerMemory::Enabled) ProfilerMemory::BeginGroup(ProfilerMemory::Groups::group) +#define PROFILE_MEM_END() if (ProfilerMemory::Enabled) ProfilerMemory::EndGroup() + +#else + +// Empty macros for disabled profiler +#define PROFILE_MEM_INC(group, size) +#define PROFILE_MEM_DEC(group, size) +#define PROFILE_MEM(group) +#define PROFILE_MEM_BEGIN(group) +#define PROFILE_MEM_END() + +#endif