MSBuild performance tips

A while back, I posted about a Powershell script I wrote to help analyze build performance. Today I spent a good amount of time looking at the output and tweaking things. I came across a couple BIG performance enhancements that I thought I would share.

Tip #1 - Don’t start external processes

Starting another process is a time consuming activity, and if you are doing it on a post/pre build event, that is going to slow your build down tremendously. I was looking through my project, and found this post build event.

<propertygroup>
  <postbuildevent>"$(MSBuildBinPath)\msbuild.exe" "$(SolutionDir)ApplyConfiguration.msbuild" /p:Configuration=$(ConfigurationName)</postbuildevent>
</propertygroup>

In the diagnostic output, I saw the _Exec _ call took 649ms.

Improving performance is simple, just import the MSBuild file and call the target directly. The refactored build looks like the following.

  <import project="$(SolutionDir)ApplyConfiguration.msbuild"></import>
  <target dependsontargets="InstallConfiguration" name="AfterBuild"></target>

And the MSBuild timing is pretty amazing, 5ms. That’s half a second in savings.

Tip #2 - Understand Inputs/Outputs

In MSBuild, targets can have a list of input and output files. Before building the target, it checks to make sure there is a need to build based on the timestamps of the inputs and outputs, also known as building incrementally.

Lets look at an example target for compressing Javascript.

<target dependsontargets="_CreateScriptFolder;MergeJavascript" name="CompressJavascript">
  <exec command="..\Bin\jsmin.exe < $(MergedJavascript) > $(CompressedJavascript)"></exec>
</target>

This executes in about 104 ms. Not a long time, but if you are doing it over and over, it could end up adding up quickly. Refatoring this to execute quickly is extremely simple.

<target dependsontargets="_CreateScriptFolder;MergeJavascript" inputs="$(MergedJavascript)" name="CompressJavascript" outputs="$(CompressedJavascript)">
  <exec command="..\Bin\jsmin.exe < $(MergedJavascript) > $(CompressedJavascript)"></exec>
</target>

The first build, it takes the same time. But all subsequent builds execute in 0ms if the $(MergedJavascript) file hasn’t changed.

Tip #3 - Reduce dependencies

After you have optimized everything else, you will see a couple targets that take the majority of your build time, ResolveAssemblyReferences and ResolveProjectReferences. Unfortunately, there isn’t a quick fix for this one. It comes back to your application architecture and dependency management.

Even compiling isn’t that big a deal, since compiling is done incrementally if files have changed. Dependencies have to be resolved each build though.

       78 ms  CoreCompile                                1 calls
      149 ms  ResolveProjectReferences                   1 calls
      188 ms  ResolveAssemblyReferences                  1 calls