Measuring script recompilation times

Hi !
As a quite common scenario, i’m working on a project with an increasing number of scripts.
After the last few days, I noticed a sensible increase in script recompilation times: it takes almost 20 seconds instead of what used to be, like 3.

I’ve been reading here and there about how C# or building my code into a DLL compiles much faster, but, rather than speeding it up, i’m trying to understand why this happens, ,so I’d like to narrow down what could just be some awful code.

I’d like to write an helper script, to measure individual scripts recompiling times and find the bad guy.

I’m reading AssetDatabase docs, but doesn’t seam the right place for what i’m trying to do.

//something like this

function OnRecompilation()
{
var time_break = Time.realtimeSinceStartup;

var measured_script = every script triggered to be compiled ? how ? :)
AssetDatabase.Refresh();  //obviously not, but you get the idea. do you ? :)


print(measured_script + " Loaded in " + (Time.realtimeSinceStartup-time_break));
}

Of course, given the growing number of scripts, including a “self compilation time measuring” function to all of them, is not an option.

Also, Is there anything else beyond scripts, (Substances in Resources folder) affecting what i may wrongly call “recompilation times” (the small lower right circle) ? Moving substances away from that folder didn’t help, but at the moment i’m clueless.

Thanks for your time and attention :slight_smile:

Alright so if you really want to speed your compilation you need to forget about UnityScript/JS/Boo and use C#, it’s tons of times faster.

Now here’s how you can manual compile stuff so you could measure things. First let’s start with C#, there’s two ways, first via a static method, second is via the StartCompiler instance method. In both cases you have to provide:

  1. Absolute paths to your source file(s) (With their extensions like .cs .js etc)
  2. (Optional) Absolute paths to any non .NET reference that you used (if any) (see later) (msdn)
  3. (Optional) Any defines that you used (msdn)
  4. Absolute output file path (with the .dll extension as well)

Now create an editor file (put it in your Editor folder) called “ManualCompile.cs” (or anything)

using UnityEngine;
using UnityEditor;
using System.Linq;
using System;
using System.Reflection;
using System.Collections.Generic;

public static class ManualCompile
{
	[MenuItem("Tools/Compile Via Static Method")]
	public static void CompileCSStatic()
	{
	}

	[MenuItem("Tools/Compile Via MonoIsland")]
	public static void CompileCSViaIsland()
	{
	}
}

Now let’s write these methods:

public static void CompileCSStatic()
{
	// Get a reference to the UnityEditor.dll - could be done via any UnityEditor type, not just Editor. So typeof(SerializedProperty).Assembly also works...
	var editorAsm = typeof(Editor).Assembly;

	// Get the type object for our mono cs compiler
	// For some reason doing a editorAsm.GetType("Whatever") always returns null so I get all the types and filter the type I'm interested in
	var compilerType = editorAsm.GetTypes().FirstOrDefault(t => t.Name == "MonoCSharpCompiler");

	// Get a MethodInfo reference to our static Compile method
	var compileMethod = compilerType.GetMethod("Compile", BindingFlags.Static | BindingFlags.Public);

	// Now we set the sources, refs, defs and output
	var sources = new[] { @"absolute path to a source file, ex: C:\Users\vexe\Desktop\Test\Assets\Editor\ManualCompile.cs" };
	var references = new string[]
	{
			typeof(GameObject).Assembly.Location, // or C:\Program Files (x86)\Unity\Editor\Data\Managed\UnityEngine.dll
			editorAsm.Location
			//, add more references if needed - no need to add .NET references like System, etc
	};
	var defines = new string[] { };
	var output = @"C:\Users\vexe\Desktop\Test\Assets\output.dll";

	// Invoking the compile method will return a string[] containing all the compilation errors (if any)
	var errors = compileMethod.Invoke(null, new object[] { sources, references, defines, output });

	foreach (var e in (errors as IEnumerable<string>))
		Debug.LogError(e);

	// We import and refresh
	AssetDatabase.ImportAsset("Assets/output.dll");
	AssetDatabase.Refresh();
}

And that’s it!

Now I have to mention the other way (via MonoIsland) because it seems that the UnityScript compiler doesn’t have a static Compile method so we can’t just change the compiler type and use the same code above, we have to do it the island way, so let’s see…

First, I should mention that the static compile method above, creates an island internally so it’s just a wrapper for the method that we’ll write next. Now what is an island? It’s just a struct that holds references to the source files, references, defines, output file, build target and a library profile:

internal struct MonoIsland
{
	public readonly BuildTarget _target;
	public readonly string _classlib_profile;
	public readonly string[] _files;
	public readonly string[] _references;
	public readonly string[] _defines;
	public readonly string _output;

	public MonoIsland(BuildTarget target, string classlib_profile, string[] files, string[] references, string[] defines, string output)
	{
		this._target = target;
		this._classlib_profile = classlib_profile;
		this._files = files;
		this._references = references;
		this._defines = defines;
		this._output = output;
	}
}

Now what is BuildTarget? - It’s just an enum in the UnityEditor namespace.
What is _classlib_prfile? - I’m not a 100% sure, but reading Unity’s code (specifically the GetProfileDirectory method in the internal class MonoInstallationFinder in the UnityEditor.Utils namespace) I found out that all the profiles are located in EditorApplication.applicationPath + @"\Data\Mono\lib\mono" (EditorApplication.applicationPath == (in my computer) @“C:\Program Files (x86)\Unity\Editor”) - So I used “2.0” - Judging from the number, this could be the equivalent .NET framework that Unity’s Mono uses (somebody correct me if I’m wrong)

So now basically the only difference, is that we have to create a MonoIsland, and pass it to the StartCompile method - StartCompile returns a Program reference (an internal class in UnityEditor.Utils) from which we can take the output messages (errors, etc). So here we go:

public static void CompileCSViaIsland()
{
	var editorAsm = typeof(Editor).Assembly;
	var editorTypes = editorAsm.GetTypes();

	var target = BuildTarget.StandaloneWindows64;
	var profile = "2.0";
	var sources = new[]
	{
		@"C:\Users\vexe\Desktop\Test\Assets\Editor\ManualCompile.cs"
		//, add more source files...
	};
	var references = new string[]
	{
			typeof(GameObject).Assembly.Location,
			editorAsm.Location
	};
	var defines = new string[] { };
	var output = @"C:\Users\vexe\Desktop\Test\Assets\output.dll";

	var islandType = editorTypes.FirstOrDefault(t => t.Name == "MonoIsland");
	var islandInstance = Activator.CreateInstance(islandType, target, profile, sources, references, defines, output);
	var compilerType = editorTypes.FirstOrDefault(t => t.Name == "MonoCSharpCompiler");
	var compilerInstance = Activator.CreateInstance(compilerType, islandInstance);

	// For some reason I got an AmbiguousMatchException thrown when I used compilerType.GetMethod("StartCompiler, BF.Instance | BF.NonPublic);
	// I don't know why, since there's only one StartCompiler method so there should be no ambiguity...
	// So I used a more explicit overload of GetMethod and specified that I want the StartCompiler that takes no arguments...
	var startCompiler = compilerType.GetMethod("StartCompiler", BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);

	// Like we said earlier, StartCompiler returns a Program instance
	var program = startCompiler.Invoke(compilerInstance, null);
	var errors = program.GetType().GetMethod("GetErrorOutput", BindingFlags.Instance | BindingFlags.Public).Invoke(program, null);

	foreach (var e in (errors as IEnumerable<string>))
		Debug.LogError(e);

	AssetDatabase.ImportAsset("Assets/output.dll");
	AssetDatabase.Refresh();
}

Now I haven’t tried it, but I’m willing to bet that all you have to do to compile UnityScripts, is just change the compiler type name, so instead of “MonoCSharpCompiler” you use “UnityScriptCompiler”

I will write a more nicified/friendly/easy-to-use/portable versions of the two methods above later so you can pass a whole directory to compile, much better.

Now you can measure your stuff, just write the source files paths, use System.Diagnostics.Stopwatch and compile. - Let me know how it goes.


EDIT:

Actually you can do your compilation from the command line, reading from MonoScriptCompilerBase (in UnityEditor.Scripting.Compilers) StartCompiler method you can see that the compiler is actually executed from the profile folders I mentioned above, so if you go to the “Editor/Data/Mono/lib/mono/2.0” the compilers executables are there, somewhere…


EDIT:

Here’s a better-written version - Notice I’m not detecting the references when I compile a directory - So you have to manually add the references your self. Remember, you don’t need to add .NET references like System, etc but only custom/user references, like Unity dlls or MyRuntimeExtensions.dll or whatever… You might want to put the stopwatch in a dif place, or put more than one, up to you.


EDIT:

Forgot to mention something, the second answer here covers it.