Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Unity 5  Game Optimization

You're reading from   Unity 5 Game Optimization Master performance optimization for Unity3D applications with tips and techniques that cover every aspect of the Unity3D Engine

Arrow left icon
Product type Paperback
Published in Nov 2015
Publisher Packt
ISBN-13 9781785884580
Length 296 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Chris Dickinson Chris Dickinson
Author Profile Icon Chris Dickinson
Chris Dickinson
Arrow right icon
View More author details
Toc

Table of Contents (10) Chapters Close

Saving and loading Profiler data

The Unity Profiler currently has a few fairly significant pitfalls when it comes to saving and loading Profiler data:

  • Only 300 frames are visible in the Profiler window at once
  • There is no way to save Profiler data through the user interface
  • Profiler binary data can be saved into a file the Script code, but there is no built-in way to view this data

These issues make it very tricky to perform large-scale or long-term testing with the Unity Profiler. They have been raised in Unity's Issue Tracker tool for several years, and there doesn't appear to be any salvation in sight. So, we must rely on our own ingenuity to solve this problem.

Fortunately, the Profiler class exposes a few methods that we can use to control how the Profiler logs information:

  1. The Profiler.enabled method can be used to enable/disable the Profiler, which is the equivalent of clicking on the Record button in the Control View of the Profiler.

    Note

    Note that changing Profiler.enabled does not change the visible state of the Record button in the Profiler's Controls bar. This will cause some confusing conflicts if we're controlling the Profiler through both code and the user interface at the same time.

  2. The Profiler.logFile method sets the current path of the log file that the Profiler prints data out to. Be aware that this file only contains a printout of the application's frame rate over time, and none of the useful data we normally find in the Profiler's Timeline View. To save that kind of data as a binary file, we must use the options that follow.
  3. The Profiler.enableBinaryLog method will enable/disable logging of an additional file filled with binary data, which includes all of the important values we want to save from the Timeline and Breakdown Views. The file location and name will be the same as the value of Profiler.logFile, but with .data appended to the end.

With these methods, we can generate a simple data-saving tool that will generate large amounts of Profiler data separated into multiple files. With these files, we will be able to peruse them at a later date.

Saving Profiler data

In order to create a tool that can save our Profiler data, we can make use of a Coroutine. A typical method will be executed from beginning to end in one sitting. However, Coroutines are useful constructs that allow us write methods that can pause execution until a later time, or an event takes place. This is known as yielding, and is accomplished with the yield statement. The type of yield determines when execution will resume, which could be one of the following types (the object that must be passed into the yield statement is also given):

  • After a specific amount of time (WaitForSeconds)
  • After the next Update (WaitForEndOfFrame)
  • After the next Fixed Update (WaitForFixedUpdate)
  • Just prior to the next Late Update (null)
  • After a WWW object completes its current task, such as downloading a file (WWW)
  • After another Coroutine has finished (a reference to another Coroutine)

The Unity Documentation on Coroutines and Execution Order provides more information on how these useful tools function within the Unity Engine:

Tip

Coroutines should not be confused with threads, which execute independently of the main Unity thread. Coroutines always run on the main thread with the rest of our code, and simply pause and resume at certain moments, depending on the object passed into the yield statement.

Getting back to the task at hand, the following is the class definition for our ProfilerDataSaverComponent, which makes use of a Coroutine to repeat an action every 300 frames:

using UnityEngine;
using System.Text;
using System.Collections;

public class ProfilerDataSaverComponent : MonoBehaviour {

  int _count = 0;

  void Start() {
    Profiler.logFile = "";
  }

  void Update () {
    if (Input.GetKey (KeyCode.LeftControl) && Input.GetKeyDown (KeyCode.H)) {
      StopAllCoroutines();
      _count = 0;
      StartCoroutine(SaveProfilerData());
    }
  }

  IEnumerator SaveProfilerData() {
    // keep calling this method until Play Mode stops
    while (true) {

      // generate the file path
      string filepath = Application.persistentDataPath + "/profilerLog" + _count;

      // set the log file and enable the profiler
      Profiler.logFile = filepath;
      Profiler.enableBinaryLog = true;
      Profiler.enabled = true;

      // count 300 frames
      for(int i = 0; i < 300; ++i) {

        yield return new WaitForEndOfFrame();

        // workaround to keep the Profiler working
        if (!Profiler.enabled)
          Profiler.enabled = true;
      }

      // start again using the next file name
      _count++;
    }
  }
}

Try attaching this Component to any GameObject in the Scene, and press Ctrl + H (OSX users will want to replace the KeyCode.LeftControl code with something such as KeyCode.LeftCommand). The Profiler will start gathering information (whether or not the Profiler Window is open!) and, using a simple Coroutine, will pump the data out into a series of files under wherever Application.persistantDataPath is pointing to.

Tip

Note that the location of Application.persistantDataPath varies depending on the Operating System. Check the Unity Documentation for more details at http://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html.

It would be unwise to send the files to Application.dataPath, as it would put them within the Project Workspace. The Profiler does not release the most recent log file handle if we stop the Profiler or even when Play Mode is stopped. Consequently, as files are generated and placed into the Project workspace, there would be a conflict in file accessibility between the Unity Editor trying to read and generate complementary metadata files, and the Profiler keeping a file handle to the most recent log file. This would result in some nasty file access errors, which tend to crash the Unity Editor and lose any Scene changes we've made.

When this Component is recording data, there will be a small overhead in hard disk usage and the overhead cost of IEnumerator context switching every 300 frames, which will tend to appear at the start of every file and consume a few milliseconds of CPU (depending on hardware).

Each file pair should contain 300 frames worth of Profiler data, which skirts around the 300 frame limit in the Profiler window. All we need now is a way of presenting the data in the Profiler window.

Here is a screenshot of data files that have been generated by ProfilerDataSaverComponent:

Saving Profiler data

Note

Note that the first file may contain less than 300 frames if some frames were lost during Profiler warm up.

Loading Profiler data

The Profiler.AddFramesFromFile() method will load a given profiler log file pair (the text and binary files) and append it into the Profiler timeline, pushing existing data further back in time. Since each file will contain 300 frames, this is perfect for our needs, and we just need to create a simple EditorWindow class that can provide a list of buttons to load the files into the Profiler.

Tip

Note that AddFramesFromFile() only requires the name of the original log file. It will automatically find the complimentary binary .data file on its own.

The following is the class definition for our ProfilerDataLoaderWindow:

using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;

public class ProfilerDataLoaderWindow : EditorWindow {

  static List<string> s_cachedFilePaths;
  static int s_chosenIndex = -1;

  [MenuItem ("Window/ProfilerDataLoader")]
  static void Init() {
    ProfilerDataLoaderWindow window = (ProfilerDataLoaderWindow)EditorWindow.GetWindow (typeof(ProfilerDataLoaderWindow));
    window.Show ();

    ReadProfilerDataFiles ();
  }

  static void ReadProfilerDataFiles() {
    // make sure the profiler releases the file handle
    // to any of the files we're about to load in
    Profiler.logFile = "";

    string[] filePaths = Directory.GetFiles (Application.persistentDataPath, "profilerLog*");

    s_cachedFilePaths = new List<string> ();

    // we want to ignore all of the binary
    // files that end in .data. The Profiler
    // will figure that part out
    Regex test = new Regex (".data$");

    for (int i = 0; i < filePaths.Length; i++) {
      string thisPath = filePaths [i];

      Match match = test.Match (thisPath);

      if (!match.Success) {
        // not a binary file, add it to the list
        Debug.Log ("Found file: " + thisPath);
        s_cachedFilePaths.Add (thisPath);
      }
    }

    s_chosenIndex = -1;
  }

  void OnGUI () {
    if (GUILayout.Button ("Find Files")) {
      ReadProfilerDataFiles();
    }

    if (s_cachedFilePaths == null)
      return;

    EditorGUILayout.Space ();

    EditorGUILayout.LabelField ("Files");

    EditorGUILayout.BeginHorizontal ();

    // create some styles to organize the buttons, and show
    // the most recently-selected button with red text
    GUIStyle defaultStyle = new GUIStyle(GUI.skin.button);
    defaultStyle.fixedWidth = 40f;

    GUIStyle highlightedStyle = new GUIStyle (defaultStyle);
    highlightedStyle.normal.textColor = Color.red;

    for (int i = 0; i < s_cachedFilePaths.Count; ++i) {

      // list 5 items per row
      if (i % 5 == 0) {
        EditorGUILayout.EndHorizontal ();
        EditorGUILayout.BeginHorizontal ();
      }

      GUIStyle thisStyle = null;

      if (s_chosenIndex == i) {
        thisStyle = highlightedStyle;
      } else {
        thisStyle = defaultStyle;
      }

      if (GUILayout.Button("" + i, thisStyle)) {
        Profiler.AddFramesFromFile(s_cachedFilePaths[i]);

        s_chosenIndex = i;
      }
    }

    EditorGUILayout.EndHorizontal ();
  }
}

The first step in creating any custom EditorWindow is creating a menu entry point with a [MenuItem] attribute and then creating an instance of a Window object to control. Both of these occur within the Init() method.

We're also calling the ReadProfilerDataFiles() method during initialization. This method reads all files found within the Application.persistantDataPath folder (the same location our ProfilerDataSaverComponent saves data files to) and adds them to a cache of filenames to use later.

Finally, there is the OnGUI() method. This method does the bulk of the work. It provides a button to reload the files if needed, verifies that the cached filenames have been read, and provides a series of buttons to load each file into the Profiler. It also highlights the most recently clicked button with red text using a custom GUIStyle, making it easy to see which file's contents are visible in the Profiler at the current moment.

The ProfilerDataLoaderWindow can be accessed by navigating to Window | ProfilerDataLoader in the Editor interface, as show in the following screenshot:

Loading Profiler data

Here is a screenshot of the display with multiple files available to be loaded. Clicking on any of the numbered buttons will push the Profiler data contents of that file into the Profiler.

Loading Profiler data

The ProfilerDataSaverComponent and ProfilerDataLoaderWindow do not pretend to be exhaustive or feature-rich. They simply serve as a springboard to get us started if we wish to take the subject further. For most teams and projects, 300 frames worth of short-term data is enough for developers to acquire what they need to begin making code changes to fix the problem.

You have been reading a chapter from
Unity 5 Game Optimization
Published in: Nov 2015
Publisher: Packt
ISBN-13: 9781785884580
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime