Improved Memory Usage Display


In a previous post, I talked about how I added basic memory usage output to my frametime debug display. I also mentioned the major problem with that system being that the debug output I used allocated memory, causing garbage collections to occur. This constant change in memory usage naturally made the output hard to read. As I was writing that post, I had an idea for how I could easily solve that problem which I will share with you now.

The main issue with the memory output in my frametime debug display is that the constant memory allocations causes it to fluctuate between values several megabytes apart as temporary memory is allocated then periodically disposed by garbage collection. This means that the memory display ticks up constantly then drops down to a threshold value from which it starts ticking again. Naturally, this makes it hard to read the memory display and causes it to not accurately represent the memory being allocated by the game itself.

While I mentioned modifying the display logic to prevent memory allocations, I found an easier alternative I could implement without dramatically changing the existing code. Basically, I opted to only update the memory value being displayed just after a garbage collection. This means that the number displayed doesn’t change constantly as new memory is allocated, making it easier to read. Because it’s updated after a garbage collection run, the memory usage I log will exclude memory that was allocated but waiting to be released. Now, because I miss small allocations that don’t total up enough to trigger garbage collection and garbage collection itself is not guaranteed to necessarily clean up all potentially releasable memory, this approach to memory reporting is not entirely accurate. It’s pretty close however and the stability of the number gives me a much better reading of how much memory the various parts of my game are using relative to each other.

The code I use to only check memory usage after a garbage collection is below:

    //In class member declarations
    WeakReference GcDetectionReference;

...

    //In component Draw() function
    if (!GcDetectionReference.IsAlive)
    {
         GcDetectionReference = new WeakReference(new Object());
         TotalMemoryKB = GC.GetTotalMemory(false) / 1024;
    }

The trick here (which I picked up from this post) is to use a WeakReference around a dummy object as a canary for detecting garbage collections. WeakReferences aren’t considered to be holding memory for the purpose of garbage collections so they can have memory be deleted out from under them. In this scenario, the WeakReference GCDetectionReference is the only reference to the dummy object we allocate so it is very likely to for said object to be deleted during garbage collection. This allows us know when a garbage collection has just occurred and thus know when to update the memory usage report.

I hope this bit of code proves useful to my fellow XNA game devs. For convenience, the full code for the updated FrametimeCounterComponent follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;


namespace EstrellaLib.Debug
{
    /// <summary>
    /// This is a game component that implements IUpdateable.
    /// </summary>
    public class FrametimeCounterComponent : Microsoft.Xna.Framework.DrawableGameComponent
    {
        Stopwatch FrametimeStopwatch;
        
        SpriteFont FrametimeDisplayFont;
        SpriteBatch FrametimeDisplayBatch;

        public Vector2 FrametimeDisplayPosition;
        public Color FrametimeDisplayColor;

        const double FRAMETIME_DANGER_THRESHOLD = 1000.0f / 60.0f;      //If averaged frametime goes over this threshold...
        const float DANGER_COLOR_DISPLAY_SECONDS = 2.0f;                //... the frametime display will change color for this many seconds.
        
        float DangerTimer;
        public Color FrametimeDangerDisplayColor;
        float AveragedFrameTime;

        public bool ShowAveragedFrametime;

        WeakReference GcDetectionReference;
        long TotalMemoryKB;

        public FrametimeCounterComponent(Game game)
            : base(game)
        {
            ShowAveragedFrametime = true;
            FrametimeDisplayPosition = Vector2.One * 10.0f;
            FrametimeDisplayColor = Color.Green;
            FrametimeDangerDisplayColor = Color.Magenta;
            GcDetectionReference = new WeakReference(null);
        }

        /// <summary>
        /// Allows the game component to perform any initialization it needs to before starting
        /// to run.  This is where it can query for any required services and load content.
        /// </summary>
        public override void Initialize()
        {
            FrametimeStopwatch = new Stopwatch();
            FrametimeStopwatch.Start();
            base.Initialize();
        }

        protected override void LoadContent()
        {
            FrametimeDisplayBatch = new SpriteBatch(Game.GraphicsDevice);
            FrametimeDisplayFont = Game.Content.Load<SpriteFont>("Debug/Fonts/Default");
            base.LoadContent();
        }

        /// <summary>
        /// Allows the game component to update itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
        }

        public override void Draw(GameTime gameTime)
        {
            TimeSpan frameTime = FrametimeStopwatch.Elapsed;
            FrametimeStopwatch.Reset();
            FrametimeStopwatch.Start();

            if (!GcDetectionReference.IsAlive)
            {
                GcDetectionReference = new WeakReference(new Object());
                TotalMemoryKB = GC.GetTotalMemory(false) / 1024;
            }

            AveragedFrameTime = AveragedFrameTime * 0.9f + (float)frameTime.TotalMilliseconds * 0.1f;
            DangerTimer = MathHelper.Clamp(DangerTimer - (float)gameTime.ElapsedGameTime.TotalSeconds, 0.0f, DANGER_COLOR_DISPLAY_SECONDS);
            if (AveragedFrameTime > FRAMETIME_DANGER_THRESHOLD)
            {
                DangerTimer = DANGER_COLOR_DISPLAY_SECONDS;
            }

            FrametimeDisplayBatch.Begin();
            FrametimeDisplayBatch.DrawString(FrametimeDisplayFont, "Frame Time: " + (ShowAveragedFrametime ? AveragedFrameTime : frameTime.TotalMilliseconds) + "ms", FrametimeDisplayPosition, DangerTimer > 0.0f ? FrametimeDangerDisplayColor : FrametimeDisplayColor);
            FrametimeDisplayBatch.DrawString(FrametimeDisplayFont, "Total Memory: " + TotalMemoryKB + "KB", FrametimeDisplayPosition + Vector2.UnitY * FrametimeDisplayFont.LineSpacing, FrametimeDisplayColor);
            FrametimeDisplayBatch.End();

            base.Draw(gameTime);
        }
    }
}
Share this Article:
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Yahoo! Buzz
  • Twitter
  • Google Bookmarks
  • Print