Getting Cute with Performance Metrics (and Reducing Memory Usage too!)


I’ve lamented before about how unfortunate it was that my frametime display allocated memory and would cause garbage collections to occur. Well, I found a simple solution for my memory allocation problem and, if I say so myself, it’s kind of… cute.

Okay, so enough being coy, what did I actually do? Basically, I replaced my frametime and memory displays with icons. I retain the original text-based display as an option when I need more detail, but most of the time, I can use the icon-based display to keep me aware of my basic performance profile. You can see the new frametime display in action in this screenshot from work-in-progress Robot Legions (upper-left corner):

There are 2 major elements to this new, memory-allocation-free frametime display. One is the stopwatch whose neighboring smiley face changes color and expression depending on how high the frametime is. If my frametime grows to a level where I would start dropping frames, the face icon becomes an angry red. The other element is the row of garbage cans (actually recycling bins because we’re eco-friendly around here) that show the number of recent garbage collections. It’s not a lot of information but it’s just enough to let me know when I should start using my heavier-duty profiling tools to dig into a problem.

In case you want to use this sort of display in your own project, you can grab the icon spritesheet here.

The icons are originally from the public domain Tango Icon Library. You can go there to find icons suitable for a variety of purposes.

The latest code for my frametime display code is below:

#if DEBUG
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
    {
        enum PerfIconType
        {
            Stopwatch,
            GarbageBin,
            HappyFace,
            NeutralFace,
            SadFace
        }

        Stopwatch FrametimeStopwatch;

        const int PERF_ICON_WIDTH = 32;
        Texture2D PerfIconSheet;
        SpriteFont FrametimeDisplayFont;
        SpriteBatch FrametimeDisplayBatch;

        bool ShowFrametimeText;

        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;

        float RecentGarbageCollections;

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

        /// <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);
            PerfIconSheet = Game.Content.Load<Texture2D>("Textures/PerfIconSheet");
            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)
        {
            float deltaSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds;
            TimeSpan frameTime = FrametimeStopwatch.Elapsed;
            FrametimeStopwatch.Reset();
            FrametimeStopwatch.Start();

            RecentGarbageCollections = MathHelper.Max(0.0f, RecentGarbageCollections - deltaSeconds * 0.1f);        //Subtract 1 RecentGC every 10 seconds
            if (!GcDetectionReference.IsAlive)
            {
                GcDetectionReference = new WeakReference(new Object());
                TotalMemoryKB = GC.GetTotalMemory(false) / 1024;
                RecentGarbageCollections+=1.0f;
            }
           

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

            int displayItemCount = 0;
            FrametimeDisplayBatch.Begin();
           
            if (ShowFrametimeText)
            {
                FrametimeDisplayBatch.DrawString(FrametimeDisplayFont, "Frame Time: " + (ShowAveragedFrametime ? AveragedFrameTime : frameTime.TotalMilliseconds) + "ms", FrametimeDisplayPosition + Vector2.UnitY * (FrametimeDisplayFont.LineSpacing * displayItemCount++), DangerTimer > 0.0f ? FrametimeDangerDisplayColor : FrametimeDisplayColor);
                FrametimeDisplayBatch.DrawString(FrametimeDisplayFont, "Total Memory: " + TotalMemoryKB + "KB", FrametimeDisplayPosition + Vector2.UnitY * (FrametimeDisplayFont.LineSpacing * displayItemCount++), FrametimeDisplayColor);
                FrametimeDisplayBatch.DrawString(FrametimeDisplayFont, "Recent GCs: " + (int)RecentGarbageCollections, FrametimeDisplayPosition + Vector2.UnitY * (FrametimeDisplayFont.LineSpacing * displayItemCount++), FrametimeDisplayColor);
            }

            DrawIcon(PerfIconType.Stopwatch, FrametimeDisplayPosition + Vector2.UnitY * (FrametimeDisplayFont.LineSpacing * displayItemCount));
            DrawIcon(FrametimeToPerfIcon(ShowAveragedFrametime ? AveragedFrameTime : (float)frameTime.TotalMilliseconds), FrametimeDisplayPosition + Vector2.UnitX * PERF_ICON_WIDTH + Vector2.UnitY * (FrametimeDisplayFont.LineSpacing * displayItemCount));
            displayItemCount++;

            int garbageX = 0;
            int garbageY = 0;
            for (int i = 0; i < (int)RecentGarbageCollections; ++i)
            {
                DrawIcon(PerfIconType.GarbageBin, FrametimeDisplayPosition + Vector2.UnitX * PERF_ICON_WIDTH * garbageX + Vector2.UnitY * (FrametimeDisplayFont.LineSpacing * displayItemCount) + Vector2.UnitY * (FrametimeDisplayFont.LineSpacing * garbageY));
                garbageX++;
                if (garbageX > 10)
                {
                    garbageY++;
                    garbageX = 0;
                }
            }
            displayItemCount++;

            FrametimeDisplayBatch.End();

            base.Draw(gameTime);
        }

        private void DrawIcon(PerfIconType iconType, Vector2 position)
        {
            FrametimeDisplayBatch.Draw(PerfIconSheet, position, new Rectangle((int)iconType * PERF_ICON_WIDTH, 0, PERF_ICON_WIDTH, PERF_ICON_WIDTH), Color.White);
        }

        private PerfIconType FrametimeToPerfIcon(float frameTime)
        {
            if (frameTime < FRAMETIME_DANGER_THRESHOLD * 0.75f )
            {
                return PerfIconType.HappyFace;
            }
            else if (frameTime < FRAMETIME_DANGER_THRESHOLD * 1.5f)
            {
                return PerfIconType.NeutralFace;
            }

            return PerfIconType.SadFace;
        }
    }
}
#endif
Share this Article:
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Yahoo! Buzz
  • Twitter
  • Google Bookmarks
  • Print

One thought on “Getting Cute with Performance Metrics (and Reducing Memory Usage too!)

  1. Pingback: Using CLRProfiler to Take Out the Trash, Part 1 | Game Dev Without a Cause

Comments are closed.