Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a max memory usage monitor #410

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public enum PlayerType
readonly int tokenCount;
readonly int debugTokenCount;
readonly StringBuilder pgns;
long maxMemoryUsed = 0;

public ChallengeController()
{
Expand Down Expand Up @@ -233,6 +234,25 @@ void OnMoveChosen(Move chosenMove)
moveToPlay = chosenMove;
isWaitingToPlayMove = true;
playMoveTime = lastMoveMadeTime + MinMoveDelay;
if (MonitorMemoryUsage && PlayerToMove.Bot is MyBot)
{
isPlaying = false;
try
{
maxMemoryUsed = Math.Max(ObjectSizeHelper.GetSize(PlayerToMove.Bot), maxMemoryUsed);
}
catch (Exception e)
{
Log("An error occurred while determining used memory size.\n" + e.ToString(), true,
ConsoleColor.Red);
hasBotTaskException = true;
botExInfo = ExceptionDispatchInfo.Capture(e);
}
finally
{
isPlaying = true;
}
}
}
else
{
Expand Down Expand Up @@ -389,7 +409,8 @@ public void Draw()

public void DrawOverlay()
{
BotBrainCapacityUI.Draw(tokenCount, debugTokenCount, MaxTokenCount);
BotBrainCapacityUI.DrawTokenUsage(tokenCount, debugTokenCount, MaxTokenCount);
BotBrainCapacityUI.DrawMemoryUsage(maxMemoryUsed, MaxMemoryUsage);
MenuUI.DrawButtons(this);
MatchStatsUI.DrawMatchStats(this);
}
Expand Down
2 changes: 2 additions & 0 deletions Chess-Challenge/src/Framework/Application/Core/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public static class Settings
// Other settings
public const int MaxTokenCount = 1024;
public const LogType MessagesToLog = LogType.All;
public const int MaxMemoryUsage = 256 * 1024 * 1024;
public const bool MonitorMemoryUsage = true;

public enum LogType
{
Expand Down
162 changes: 162 additions & 0 deletions Chess-Challenge/src/Framework/Application/Helpers/ObjectSizeHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;

namespace ChessChallenge.Application
{
public static class ObjectSizeHelper
{
static readonly long ObjectSize = IntPtr.Size == 8 ? 24 : 12;
static readonly long PointerSize = IntPtr.Size;

public static long GetSize(object? obj) => GetSize(obj, new HashSet<object>());

static long GetSize(object? obj, HashSet<object> seenObjects)
{
if (obj is null)
{
return 0;
}

var type = obj.GetType();

// ignore references to the API board and the API Timer
if (typeof(API.Board) == type || typeof(API.Timer) == type)
{
return 0;
}

if (type.IsEnum)
{
if (!sizeOfTypeCache.TryGetValue(type, out var sizeOfType))
{
var underlyingType = Enum.GetUnderlyingType(type);
sizeOfTypeCache[type] = sizeOfType = Marshal.SizeOf(underlyingType);
}

return sizeOfType;
}

if (type.IsValueType)
{
// Marshal.SizeOf() does not work for structs with reference types
// Marshal.SizeOf() also does not work with generics, so we need to use Unsafe.SizeOf()
if (!fieldInfoCache.TryGetValue(type, out var fieldInfos))
{
fieldInfoCache[type] = fieldInfos = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
}
if (!typeContainsClassCache.TryGetValue(type, out var typeContainsClass))
{
typeContainsClassCache[type] = typeContainsClass = fieldInfos.Any(x => TypeIsClass(x.FieldType));
}
return typeContainsClass
? GetFieldsMemorySize(obj, fieldInfos, seenObjects)
: GetStructSize(type);
}

if (type == typeof(string))
{
var str = (string)obj;
return str.Length * 2 + 6 + ObjectSize;
}

if (obj is IList collection)
{
var totalSize = ObjectSize;

// structs are counted easy
var typeZero = collection.Count > 0 ? collection[0]?.GetType() : null;
if (typeZero is not null && sizeOfTypeCache.TryGetValue(typeZero, out var sizeOfType))
{
totalSize += collection.Count * sizeOfType;
return totalSize;
}

for (var index = 0; index < collection.Count; index++)
{
var item = collection[index];
totalSize += GetObjectSize(item, item?.GetType() ?? typeof(object), seenObjects);
}

return totalSize;
}

if (TypeIsClass(type))
{
if (!fieldInfoCache.TryGetValue(type, out var fieldInfos))
{
fieldInfoCache[type] = fieldInfos = type.GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
}

return ObjectSize + GetFieldsMemorySize(obj, fieldInfos, seenObjects);
}

throw new ArgumentException($"Unknown type {type.Name}", nameof(obj));
}

static readonly Dictionary<Type, FieldInfo[]> fieldInfoCache = new();
static readonly Dictionary<Type, bool> typeContainsClassCache = new();
static readonly Dictionary<Type, bool> typeIsClassCache = new();

static readonly Dictionary<Type, int> sizeOfTypeCache = new();
private static long GetStructSize(Type type)
{
if (!sizeOfTypeCache.TryGetValue(type, out var sizeOfType))
{
var genericSizeOf = typeof(System.Runtime.CompilerServices.Unsafe)
.GetMethod(nameof(System.Runtime.CompilerServices.Unsafe.SizeOf))
.MakeGenericMethod(type);

sizeOfTypeCache[type] = sizeOfType = (int)genericSizeOf.Invoke(null, null)!;
}

return sizeOfType;
}

private static long GetFieldsMemorySize(object obj, FieldInfo[] fieldInfos, HashSet<object> seenObjects)
{
var totalSize = 0L;

for (var index = 0; index < fieldInfos.Length; index++)
{
var fieldInfo = fieldInfos[index];
var fieldValue = fieldInfo.GetValue(obj);
totalSize += GetObjectSize(fieldValue, fieldInfo.FieldType, seenObjects);
}

return totalSize;
}

private static bool TypeIsClass(Type type)
{
if (!typeIsClassCache.TryGetValue(type, out var typeIsClass))
{
typeIsClassCache[type] = typeIsClass = type.IsClass;
}

return typeIsClass;
}

private static long GetObjectSize(object? obj, Type type, HashSet<object> seenObjects)
{
var typeIsClass = TypeIsClass(type);

if (obj is not null && typeIsClass && seenObjects.Contains(obj))
{
return 0;
}

var size = GetSize(obj, seenObjects) + (type.IsClass ? PointerSize : 0);

if (obj is not null && typeIsClass)
{
seenObjects.Add(obj);
}

return size;
}
}
}
66 changes: 51 additions & 15 deletions Chess-Challenge/src/Framework/Application/UI/BotBrainCapacityUI.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Raylib_cs;
using System;
using Raylib_cs;

namespace ChessChallenge.Application
{
Expand All @@ -10,19 +11,45 @@ public static class BotBrainCapacityUI
static readonly Color red = new(219, 9, 9, 255);
static readonly Color background = new Color(40, 40, 40, 255);

public static void Draw(int totalTokenCount, int debugTokenCount, int tokenLimit)
public static void DrawTokenUsage(int totalTokenCount, int debugTokenCount, int tokenLimit)
{
int activeTokenCount = totalTokenCount - debugTokenCount;

int screenWidth = Raylib.GetScreenWidth();
int startPos = Raylib.GetScreenWidth() / 2;
double t = (double)activeTokenCount / tokenLimit;
string text = $"Bot Brain Capacity: {activeTokenCount}/{tokenLimit}";
if (activeTokenCount > tokenLimit)
{
text += " [LIMIT EXCEEDED]";
}
else if (debugTokenCount != 0)
{
text += $" ({totalTokenCount} with Debugs included)";
}
Draw(text, t, startPos);
}

public static void DrawMemoryUsage(long memorySize, long memoryLimit)
{
int startPos = 0;
double t = (double)memorySize / memoryLimit;
string text = $"Bot Memory Capacity: {FriendlySize(memorySize)} / {FriendlySize(memoryLimit)}";
if (memorySize > memoryLimit)
{
text += " [LIMIT EXCEEDED]";
}
Draw(text, t, startPos);
}

static void Draw(string text, double t, int startPos)
{
int barWidth = Raylib.GetScreenWidth() / 2;
int screenHeight = Raylib.GetScreenHeight();
int height = UIHelper.ScaleInt(48);
int barHeight = UIHelper.ScaleInt(48);
int fontSize = UIHelper.ScaleInt(35);
// Bg
Raylib.DrawRectangle(0, screenHeight - height, screenWidth, height, background);
Raylib.DrawRectangle(startPos, screenHeight - barHeight, startPos + barWidth, barHeight, background);
// Bar
double t = (double)activeTokenCount / tokenLimit;

Color col;
if (t <= 0.7)
col = green;
Expand All @@ -32,19 +59,28 @@ public static void Draw(int totalTokenCount, int debugTokenCount, int tokenLimit
col = orange;
else
col = red;
Raylib.DrawRectangle(0, screenHeight - height, (int)(screenWidth * t), height, col);

Raylib.DrawRectangle(startPos, screenHeight - barHeight, startPos + (int)(barWidth * Math.Min(t, 1)), barHeight, col);

var textPos = new System.Numerics.Vector2(screenWidth / 2, screenHeight - height / 2);
string text = $"Bot Brain Capacity: {activeTokenCount}/{tokenLimit}";
if (activeTokenCount > tokenLimit)
var textPos = new System.Numerics.Vector2(startPos + barWidth / 2, screenHeight - barHeight / 2);
UIHelper.DrawText(text, textPos, fontSize, 1, Color.WHITE, UIHelper.AlignH.Centre);
}

static readonly string[] sizes = { "B", "KB", "MB" };
static string FriendlySize(long size)
{
if (size == 0)
{
text += " [LIMIT EXCEEDED]";
return "--";
}
else if (debugTokenCount != 0)
double friendlySize = size;
int order = 0;
while (friendlySize >= 1024 && order < sizes.Length - 1)
{
text += $" ({totalTokenCount} with Debugs included)";
order++;
friendlySize /= 1024;
}
UIHelper.DrawText(text, textPos, fontSize, 1, Color.WHITE, UIHelper.AlignH.Centre);
return $"{friendlySize:0.##} {sizes[order]}";
}
}
}