Some performance tips for using 0.9.0.1

Topics: General
Jul 16, 2013 at 6:21 PM
Edited Jul 16, 2013 at 10:23 PM
If you're compelled to use 0.9.0.1 like I am, there's a couple of things you can do to make it tolerable:

<del>Despite what I've read, Web Essentials is OK to use - in fact, the JS preview window is essential to use as a means to detect when either the VS language service or the compiler is running. The real issue is that</del>

(codeplex doesn't seem to support strikethough -- ignore the above paragraph -- web essentials is at fault)

When using Web Essentials coupled with preview window and compile on save, the TSC compiler does not batch - it runs in parallel with other concurrent instances of itself! if you save a TS file twice in quick succession, two tsc.exe instances will be launched. If you make three quick changes with subsequent saves, there will be three compilers melting your computer, and so on. If this is also run in parallel with VS language services figuring out what squiggley lines to put where, it's going to entirely melt your CPU.

This is easy to validate. Open Task Manager, sort by CPU then go into any random .ts file and hit ctrl+s twice. Return to Task Manager. Laugh, curse then read the following points:

Summary

  • Disable Resharper's typescript support completely (if you're running 8.x)
  • If you've made changes to a TS file, let VS language services catch up before you save.
  • Never save more than one TS file at a time. You must wait for the compiler to complete.
  • Watch VS's memory usage - if it reaches 2GB or so, restart VS (sucks, yes.)
UPDATE

Monitor compilation via VS Status Bar

Here's a powershell script to paste into the NuGet package manager console that will update the VS status bar with the number of concurrent TSC compilers running. It might be easier to use a sound cue, or just writing out text to the PM console. Hell, if you wanted to get fancy, you could have the script disable saving until the compiler is done.
$q = "Select * From __InstanceCreationEvent within 1 Where TargetInstance ISA 'Win32_Process' and TargetInstance.Name = 'tsc.exe'"
register-wmievent -sourceidentifier tsc.started -query $q
register-wmievent -sourceidentifier tsc.ended -query $q.replace("Creation","Deletion")
$tsc = 0 # counter for concurrent TSC compilers
Register-EngineEvent -SourceIdentifier tsc.started -Action { $global:tsc++; $dte.statusbar.text = "TSC compiling [$tsc instance(s)]" }
Register-EngineEvent -SourceIdentifier tsc.ended -Action { $global:tsc--; if ($tsc -gt 0) { $dte.statusbar.text = "TSC compiling [$tsc instance(s)]" } else { $dte.statusbar.text = "Ready" } }
Hope this helps.
Jul 17, 2013 at 9:55 AM
Edited Jul 17, 2013 at 10:02 AM
Hi,

We started using TypeScript a couple of month ago and as a whole it does improve quality of the produced Javascript. The last week has been spend implementing the new 0.9.0.1 version and it has been problematic to say the least. One time I decided to turn on the 'Compile typescript on build' and started a build. It totally locked up my complete system for about 4 minutes (and I mean my entire PC became unresponsive).

Performance was so poor that we almost decide to stop using TypeScript all together. We use TypeScript in combination with T4 code generation templates. We generate .ts files for our DataModels and DataSources which are used in Kendo UI (Telerik). The problem here was that TypeScript does not automatically compile when it is generated via T4. So I had to cook up a solution that would start the Compile of TypeScript files after generation of the .ts files.

Another problem was that we would also like to minify the generated JavaScript. This had to be done in a separate loop calling the CodePlex Ajax Minifier (there is no commandline version of web essentials available, is there?). So we ended up writing some C# code for the templates which use the System.Diagnostics.Process class to start the 'tsc.exe' and 'AjaxMin.exe'. Below you'll see the script that we created to start the processes (including a progress status bar).

In the routines a Metadata container is used that has all the metadata of our datamodel. It is a class that uses reflection to obtain all the entities and properties from our datamodel which then can be enumerated in the T4 script. So people who want to use our solution will have to come up with something to replace that but further there should not be a problem to run the TypeScript Compiler and Minifier this way.

With this solution in place the performance is bareable.

Regards
Paul
<#+ 

DTE2 m_DTE2;

DTE2 DTE2
{
    get
    {
        if(m_DTE2 == null)
        {
            m_DTE2 = (EnvDTE80.DTE2)System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.11.0");
        }

        return m_DTE2;
    }
}

EnvDTE.StatusBar StatusBar
{
    get
    {
        return DTE2.StatusBar;
    }
}

void ProcessTypeScripts(string fileNameExtension)
{
    List<System.Diagnostics.Process> processes = new List<System.Diagnostics.Process>();

    foreach (EntityMetadata entity in Metadata.MetadataContainer.Entities)
    {
        Project project = null;

        try
        {
            string outputFileName = Path.Combine(Path.GetDirectoryName(this.Host.TemplateFile), entity.Name + fileNameExtension);

            FileInfo fileInfo = new FileInfo(outputFileName);

            processes.Add(CompileTypeScriptFile(fileInfo.FullName));
        }
        catch(Exception ex)
        {
            MessageBox.Show(project.FullName);
        }
    }

    WaitForAllToFinish(processes, "Running typescript compiler");
}

void ProcessMinifiers(string fileNameExtension)
{
    List<System.Diagnostics.Process> processes = new List<System.Diagnostics.Process>();

    foreach (EntityMetadata entity in Metadata.MetadataContainer.Entities)
    {
        Project project = null;

        try
        {
            string outputFileName = Path.Combine(Path.GetDirectoryName(this.Host.TemplateFile), entity.Name + fileNameExtension);

            FileInfo fileInfo = new FileInfo(outputFileName);

            processes.Add(MinifyJavascripts(fileInfo.FullName));
        }
        catch(Exception ex)
        {
            MessageBox.Show(project.FullName);
        }
    }

    WaitForAllToFinish(processes, "Minify javascripts");
}

 void WaitForAllToFinish(List<System.Diagnostics.Process> processes, string reason)
{
    Queue queue = new Queue();
    int maxToRun = 8; // We hebben 8 processoren in ons systeem.

    foreach (System.Diagnostics.Process queueItem in processes)
    {
        queue.Enqueue(queueItem);
    }

    int totalProcesses = queue.Count;

    List<System.Diagnostics.Process> runningProcesses = new List<System.Diagnostics.Process>();

    while(queue.Count > 0 || runningProcesses.Count > 0)
    {
        StatusBar.Progress(true, reason, totalProcesses - queue.Count - runningProcesses.Count, totalProcesses);

        if(queue.Count > 0 && runningProcesses.Count < maxToRun)
        {
            System.Diagnostics.Process processToStart = (System.Diagnostics.Process)queue.Dequeue();
            if(processToStart.Start())
            {
                runningProcesses.Add(processToStart);
            }
        }

        foreach (System.Diagnostics.Process process in runningProcesses.ToArray())
        {
            if(process.HasExited) // process finished
            {
                runningProcesses.Remove(process);
            }
        }

        System.Threading.Thread.Sleep(100);
    }

    StatusBar.Progress(false);
}

System.Diagnostics.Process CompileTypeScriptFile(string fullFileName)
{
    var process = new System.Diagnostics.Process
    {
        StartInfo = new ProcessStartInfo
        {
            FileName = "tsc.exe",
            Arguments = string.Format("--target ES5 \"{0}\"", fullFileName)
        }
    };

    // process.StartInfo.CreateNoWindow = true;
    process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
    return process;
}

System.Diagnostics.Process MinifyJavascripts(string fullFileName)
{
    string solutionPath = Path.GetDirectoryName(DTE2.Solution.FullName);
    string fullMinFileName = Path.Combine(Path.GetDirectoryName(fullFileName), Path.GetFileNameWithoutExtension(fullFileName) + ".min.js");

    string programFilesPath = Environment.GetEnvironmentVariable("ProgramFiles(x86)");
    string minifierPath = System.IO.Path.Combine(programFilesPath, "Microsoft", "Microsoft Ajax Minifier", "AjaxMin.exe");

    System.Diagnostics.Process process = null;

    if(System.IO.File.Exists(minifierPath))
    {
        process = new System.Diagnostics.Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = minifierPath,
                Arguments = string.Format("-JS {0} -out {1}", fullFileName, fullMinFileName)
            }
        };

        process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
    }
    else
    {
        MessageBox.Show("Ajax minifier not installed. goto to: https://ajaxmin.codeplex.com/");
    }

    return process;
}

#>
Jul 17, 2013 at 10:00 AM
Edited Jul 17, 2013 at 10:12 AM
Here's an example of how we call this in our DataSources.tt:
<#@ output extension=".txt" #>
<#@ Include file="$(SolutionDir)Common.T4\Assemblies.ttinclude" #><#   
    try
    {   // START main try
        // Initialization Output Manager
        Manager outputManager = Manager.Create(Host, GenerationEnvironment);

        // Start Costruction CodeModelTree
        IServiceProvider hostServiceProvider = (IServiceProvider)Host;

        string directoryPath = Host.ResolvePath(@"..\..\Projecten.Model");
        string assemblyName = directoryPath + @"\bin\Model\Projecten.Model.dll";
        string fileTypescriptNameExtension = "DataSource.generated.ts";
        string fileMinifiedNameExtension = "DataSource.generated.min.js";
        string fileTypescriptOutputExtension = "DataSource.generated.js";

        Assembly assembly = Assembly.LoadFrom(assemblyName);

        Metadata.MetadataContainer.AddAssembly(assembly);

        try
        {
            foreach (EntityMetadata entity in Metadata.MetadataContainer.Entities)
            {
                if(entity != null)
                {
                    string outputJavascriptFileName = entity.Name + fileTypescriptOutputExtension;
                    string outputMinifiedFileName = entity.Name + fileMinifiedNameExtension;
                    string outputTypeScriptFileName = entity.Name + fileTypescriptNameExtension;

                    outputManager.StartNewFile(outputJavascriptFileName); // Creeer een lege javascript file.
                    outputManager.StartNewFile(outputMinifiedFileName); // Creeer een lege minified file.
                    outputManager.StartNewFile(outputTypeScriptFileName);

#>
// <reference path="../Scripts/contractors/externals.ts" />
/// <reference path="../DataModels/<#= entity.Name #>DataModel.generated.ts" />

module <#= entity.Name #>DataSources
{
    ///<summary>
    /// Returns the datasource for <#= entity.Name #>
    /// When id = undefined a datasource for a complete list is returned
    /// When id != undefined a datasource for the id is returned
    ///</summary>
    export function <#= entity.Name #>DataSource(id: number) {
        return new kendo.data.DataSource
        ({
            transport: {
                read: {
                    cache: false,
                    dataType: 'jsonp',
                    url: function() {
                        if(id == undefined) {
                            return contractors.pathname + "<#= entity.Name #>/Get";
                        }
                        else {
                            return contractors.pathname + "<#= entity.Name #>/Get/" + id;
                        }
                    }
                },
                update: {
                    url: function(item) {
                        return contractors.pathname + "<#= entity.Name #>/Update/" + item.Id;
                    },
                    dataType: 'jsonp',
                    type: "POST"
                },
                destroy: {
                    url: function(item) {
                        return contractors.pathname + "<#= entity.Name #>/Delete/" + item.Id;
                    },
                    dataType: 'jsonp',
                    type: "POST"
                },
                create: {
                    url: contractors.pathname + "<#= entity.Name #>/Create",
                    dataType: 'jsonp',
                    type: "POST"
                },
                parameterMap: function (data, operation) {
                    if (operation === "create") {
                        data.TypeName = '<#= entity.Name #>';
                    }
                    return { data: kendo.stringify(data) };
                }
            },
            pageSize: 25,
            serverPaging: true,
            serverSorting: true,
            serverFiltering: true,
            batch: false,
            schema: {
                data: "Data.Data",
                total: "Data.Size",
                errors: "Errors",
                model: <#= entity.Name #>Models.<#= entity.Name #>Model()
            }
        });
    }

<#
                    foreach (var property in entity.AllProperties)
                    {
                        if(property != null)
                        {
                            if(property.IsEntitySet)
                            {
#>
    ///<summary>
    /// Returns the grid datasource for subset <#= property.Name #> of entity <#= entity.Name #>
    /// Serverside paging and sorting are supported.
    ///</summary>
    export function <#= entity.Name #><#= property.Name #>GridDataSource(parameters) {
        return new kendo.data.DataSource
        ({
            transport: {
                read: {
                    cache: false,
                    dataType: 'jsonp',
                    url: contractors.pathname + "<#= entity.Name #>/Get<#= property.Name #>",
                },
                update: {
                    url: contractors.pathname + "<#= entity.Name #>/Update<#= property.Name #>",
                    dataType: 'jsonp'
                },
                destroy: {
                    url: contractors.pathname + "<#= entity.Name #>/Delete<#= property.Name #>",
                    dataType: 'jsonp'
                },
                create: {
                    url: contractors.pathname + "<#= entity.Name #>/Add<#= property.Name #>",
                    dataType: 'jsonp'
                },
                parameterMap: function (data, operation) {
                    if (operation === "read") {
                        data.parentId = parameters.parentId;
                    }

<#
                                if(property.IsPaired)
                                {
#>
                    if (operation === "create") {
                        // Vul het id van de foreign entity
                        data.<#= property.PairedTo #>Id = parameters.parentId;
                        data.TypeName = '<#= property.FirstGenericTypeString #>';
                    }

<#
                                }
#>
                    if(parameters.load != undefined) {
                        $.extend(data, { load: parameters.load } );
                    }

                    return { data: kendo.stringify(data) };
                }
            },
            batch: false,
            pageSize: 25,
            serverPaging: true,
            serverSorting: true,
            serverFiltering: true,
            batch: true,
            schema: {
                data: "Data.Data",
                total: "Data.Size",
                model: parameters.model
                //<#= property.FirstGenericTypeString #>Model()
            }
        });
    }
<#
                        }
                    }
                }
#>
}
<#
            }
        }
    }
    catch (Exception ex)
    {
#>
            // ERROR <#= ex.Message #> 
            // STACK <#= ex.StackTrace #>
<#
        }

        outputManager.Process(true); //write files to disk

        ProcessTypeScripts(fileTypescriptNameExtension);
        ProcessMinifiers(fileTypescriptOutputExtension);
    } // END main try
    catch(Exception ex)
    {
        MessageBox.Show("Error in DataSources.tt: " + Environment.NewLine + Environment.NewLine + ex.ToString(), "Error in Transformation");
    }
#>

<#+ 

 #>
Jul 17, 2013 at 2:14 PM
This is why my TypeScript building and minifying is all done outside of Visual Studio, with Node.js and Grunt.

Mark Rendle
Founder & CEO
Oort Corporation
Makers of Zudio


Jul 17, 2013 at 3:46 PM
This is why I'm avoiding 0.9 entirely until they resolve it! 0.8 works perfectly for me and compiles really fast. Not screwing-up my workflow just for a few extra features.
Jul 17, 2013 at 4:14 PM
Edited Jul 17, 2013 at 4:15 PM
For what it's worth, Web Essentials 2012 has the same problem: Hitting ctrl+s spawns a new compiler process, irrespective of whether it's already compiling. It sounds like it's a WE problem, but it must be more complex to fix than it appears.
Jul 17, 2013 at 5:42 PM
I've never had a good experience with Web Essentials. It has always locked up VS for me, or done something I didn't want/expect sadly. I'm sure it's not a WE issue directly, but I can live without its features so it's no great loss.
Jul 29, 2013 at 1:53 PM
I was running into the same thing, before I came to the same conclusion: Web Essentials is responsible for stalling VS with TypeScript 0.9. The fix for this was basically what's in the first post (disable Web Essentials compile on save, compile all on build, and preview window), however, I also had to make sure that TypeScript's built-in compile on save was enabled AND I had to follow the instructions at the following link to make sure that my .csproj had the appropriate MsBuild targets: https://typescript.codeplex.com/wikipage?title=Compile-on-save&referringTitle=Documentation