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