Performance

From PowerUI
Revision as of 18:49, 21 March 2018 by Bablakeluke (talk | contribs)
Jump to: navigation, search

PowerUI puts performance first. A user interface has very different requirements from a web browser - compare the UI of your favourite games with just how stationary this wiki is, for example. A web browser focuses on making a page show up quickly where as a game will spend time loading to make sure it runs quickly. PowerUI has been built from scratch to fit the requirements of a high performance UI framework.

In general, we only add support for something when we can get it working without affecting PowerUI's overall speed. It's working too - PowerUI can handle large volumes of HTML with little to no performance issues, whilst now simultaneously having very broad coverage of web technologies.

However, PowerUI is vulnerable to being exposed to some very performance draining web techniques (and there are many of them!) so this is intended to be a short guide to understanding how web engines work and what you can do to avoid some common pitfalls.

Avoid searching for constant elements in Update

This one is a common mistake but can also make great performance savings if you make some small changes. Here's an example of what it looks like:

void Update(){
 
    // Don't do this!
    var element = UI.document.getElementById("my-nav-menu");

}

PowerUI will try and index your IDs, but in general, this will result in scanning your whole DOM every single frame. You should just cache the element instead:


Element MyNavMenu;

void Start(){
    // Much better!
    MyNavMenu = UI.document.getElementById("my-nav-menu");
}

void Update(){
    // Use MyNavMenu in here
}

Go event based - avoid spamming your UI in Update

Although it's ok to update your UI in the Update loop, for example for a timer, try being more event based for the greatest performance. For example, let's say you pop up some info when the user clicks on an NPC. Rather than spamming some 'SelectedNpc' field, update the UI when they're actually clicked on:

A common Unity anti-pattern - don't do this!:

void Update() {
    // Bad!
    var headsUp = UI.document.getElementById("npc-heads-up-info");
    
    if (SelectedNpc != null) {
        // Spamspamspamspamspam
        headsUp.innerHTML = SelectedNpc.HtmlDetails;
    } else {
        headsUp.innerHTML = "";
    }
}

Instead, set that innerHTML when you set SelectedNpc:

void SelectedAnNpc(Npc npc) {
    // Much better!
    SelectedNpc = npc;

    // Update the UI:
    var headsUp = UI.document.getElementById("npc-heads-up-info");
    
    if (SelectedNpc != null) {
        headsUp.innerHTML = SelectedNpc.HtmlDetails;
    } else {
        headsUp.innerHTML = "";
    }
}

PowerUI ships with a very powerful and extendible [Event Flow|standards compliant event system] - use this to your advantage. Selecting an NPC could fire off an NPC selection event, which some other class controlling your UI responds to:

void SelectedAnNpc(Npc npc) {
    // Much better!
    SelectedNpc = npc;
    
    // Fire off an event which anything can subscribe to with a standard event listener on the document:
    NpcEvent e = new NpcEvent("selected");
    e.npc = npc;
    UI.document.dispatchEvent(e);
}

// .. somewhere else ..

UI.document.addEventListener("npcselected", delegate(NpcEvent e){
    // Update the UI here, using e.npc instead.
});


Reflow

Reflow is the name given when a web engine resolves CSS values and figures out where everything is on the screen. PowerUI also performs reflow, so following standard good practice for reducing reflow in a web browser applies to PowerUI too, so here's a guide that will help with just that. You may have cases where your UI generates lots of reflows very fast - PowerUI internally meters this using UI.SetRate; it won't reflow any more often than the rate you give.

  • Power tip #1: Use UI.SetRate and get it as low as you reasonably can. The default is 30fps with super smooth at 60fps.

Following similar lines, pulling properties such as contentHeight from an element will force a reflow to happen if the element is known to require one. Accessing the computed style (element.style.Computed.ContentHeight for example) does not force reflows, so if you know e.g. the height didn't change then using computed styles directly can be a little quicker, but this only really applies if you're doing a style change and then grabbing the contentHeight rapidly in a loop.

  • Power tip #2: Read from ComputedStyle where possible.

Skipping Reflow

It's possible to pull off a complex UI which only performs reflow a handful of times by focusing on post-process CSS properties. These are CSS properties which don't affect the flow or structure of an element - the main examples are color, color-overlay, transform and the special case that is scroll. Generally try to animate these ones when you can!

  • Power tip #3: Animate transforms (scale, rotate, translate) rather than positions (top, left, right etc) if possible.

Limiting reflow scope

Reflow applies to the nearest flow root element. For example, let's say you update the innerHTML of a fixed/absolute/sticky positioned element then only that element will actually reflow. The same applies to any of the nested kids of that positioned element - it'll bubble up the DOM to the nearest flow root, and request to reflow that element. In short, making an element become a flow root is an easy way to massively limit how many elements reflow actually affects.

Table Chaos

PowerUI doesn't support tables with no defined widths. Tables cause multiple passes over potentially thousands of elements. The modern reason for using a table today is for vertical alignment - most other layouts can be easily done with other far less intense techniques. If it's vertical align that you're really after then use the custom vertical-align values on any element:

/* Middle vertical alignment without the overhead of table-cell/ tables */
vertical-align:table-middle;

On a similar line, tables with no defined column widths are outright evil. The number of elements scanned to guess how wide a column probably should be can go exponential very quickly and, after all of that, it often doesn't look great anyway. As we're making UIs which reflow very frequently, PowerUI requires you to give all your table columns and the table itself some kind of width value. This way PowerUI has unusually fast tables - making them suitable for heavy reflow - but they're still slow compared to almost anything else.

  • Power tip #4: Use techniques other than table whenever appropriate, and make use of table-middle.

Memory Usage

PowerUI is conservative about its memory usage, but if you're targeting really low memory devices, turning off image atlasing may be wise. Turning it off exchanges GPU/rendering time for lower memory and CPU use, which can be vital for pulling off a stunning UI on a device with little memory available.

  • Power tip #5: Turn off image atlasing for very low memory devices. See UI.RenderMode for doing that.

Multithreading

Virtually all of PowerUI is multithreading friendly - anything that directly calls a Unity method like the geolocation web API isn't. Presently the cookie cache (loading or setting cookies) is the only known PowerUI API which avoidably causes threading issues. Use PowerUI API's directly from network threads, or spin up threads specifically for doing heavier UI work to make significant performance gains on multi-core architectures.

Canvas

If you're drawing to a canvas, there's some nice savings you can be making. These savings are particularly noticeable if you're drawing repeatedly, such as in Update.

Only redraw your canvas when PowerUI does

PowerUI uploads the canvas image to the GPU no faster than your UI rate. Drawing your canvas any faster than that interval will essentially just waste cycles. We call that upload process a 'refresh' - whenever you call stroke() or fill(), a refresh request is made (and then at some point in the near future, a refresh happens).

So, if PowerUI already has an image which is ready but hasn't been uploaded yet, there will be a pending refresh. Checking for that is like this:

private CanvasContext2D Context;

void Update(){

    if(Context.ImageData.RefreshRequired){
        // This means we've already rendered something to the image data
        // and it's just waiting to be uploaded. Rendering again would be a waste of time.
        // So, do nothing!
        return;
    }
    
    // Nothing to upload - draw it now!
    Context.beginPath();
    Context.moveTo(10,10);
    // ...
    
}

Multithreading canvas

Like most PowerUI API's, canvas is multithreading friendly. You could build those paths on some separate timer thread if you wanted to, for example.

Cache your paths

Whenever you call context.arcTo, context.lineTo etc, PowerUI internally builds up a representation of your path. When you then next call context.beginPath or context.clear, that representation is destroyed. If you're drawing the same path over and over, it would be a great idea to save this internal representation rather than constantly rebuilding it to take some pressure off garbage collection.

If you do this, it's up to you to safely bound your points. Normally the canvas API automatically takes care of points that are excessively out of range for you - things like infinity, NaN, or generally huge numbers. If you build a VectorPath and set it to a canvas, it's important that you also guarantee your path doesn't have any of those things. Internally, the canvas API runs VectorPath.Clip on all paths constructed via lineTo, arcTo etc. Otherwise, if you have an extremely long path then the canvas API will freeze whilst it outputs billions of pixels.

To cache your paths you could either simply never call context.clear or context.beginPath and access Context.Path to move the nodes around like this:

using PowerUI; // For CanvasContext and UI.

/// <summary>The canvas context.</summary>
private CanvasContext Context;


void Start(){

    var document = UI.document;
    
    var canvas = document.getElementById("my-canvas") as HtmlElement;

    Context = canvas.getContext("2d");

}

void Update(){
    
    // To clear the image but not the path, use ImageData:
    Context.ImageData.Clear();

    // The path is totally empty if there's no first node:
    if(Context.Path.FirstPathNode == null){
        
        // No path yet! Build it now:
        Context.moveTo(10,10);
        Context.lineTo(10,100);
        Context.lineTo(100,100);
        Context.lineTo(100,10);
        Context.closePath();
        
        // Note if any of the points in this first time call
        // go far out of range of the canvas, they may be 
        // sliced for performance purposes.
        // For example, if your entire path is off canvas,
        // the whole thing may be dumped when you call stroke().
        
        // One way to avoid that uncertainty is to build the VectorPath directly (like below)
        // and then call VectorPath.Clip when it's appropriate for your use case.
        
    }else{
        
        // E.g. update the nodes in the cachedPath:
        var point = CachedPath.FirstPathNode;
        
        // Our path has 5 points (alternatively loop until point is null).
        for(int i=0;i<5;i++){ // while(point!=null){
            
            // Move the point by the frame time:
            point.X += Time.deltaTime;
            
            // Go to the next point:
            point = point.Next;
          
        }
        
    }
    
    // Stroke or fill etc down here:
    Context.stroke();

}

If you're drawing multiple paths and you want to cache them all, then the simpler route would be to directly build one or more VectorPath instances first:

using PowerUI; // For CanvasContext and UI
using Blaze; // For VectorPath

/// <summary>The canvas context.</summary>
private CanvasContext Context;
/// <summary>A single cached path.</summary>
private VectorPath CachedPath;

void Start(){
    
    var document = UI.document;
    
    var canvas = document.getElementById("my-canvas") as HtmlElement;

    Context = canvas.getContext("2d");
    
    // build the vector path(s):
    CachedPath = new VectorPath();

    // The API is very similar - it's capitalized as it's part of the internal public API:
    CachedPath.MoveTo(10,10);
    CachedPath.LineTo(10,100);
    CachedPath.LineTo(100,100);
    CachedPath.LineTo(100,10);
    CachedPath.ClosePath();

}

void Update(){
    
    // Calling normal clear doesn't matter here (as we're not using e.g. Context.lineTo)
    Context.clear();
    
    // E.g. update current path here:
    var point = CachedPath.FirstPathNode;
    
    // note that closed paths never make infinite loops here.
    // A close node simply overlaps the node it closes and has IsClose set to true.
    while(point!=null){
        point.X+=Time.deltaTime;
        point=point.Next;
    }
    
    // Temporarily retain the path object in the canvas:
    VectorPath activePath = Canvas.Path;
    
    // Set ours:
    Canvas.Path = CachedPath;
    
    // Stroke it now:
    Canvas.stroke();

    // Restore the other path:
    Canvas.Path = activePath;
    
}

Precompile PowerUI

Performance applies to your development process too. Unless you've got a solid state hard drive, PowerUI can take some 15 seconds to compile. Every time you change any of your C# files. Precompile PowerUI by going to Window > PowerUI > Precompile then just tick the box - that will entirely eliminate this delay for you.

If you change platform, or update Unity, you must precompile it again! That's because things like UNITY_ANDROID matter a lot.

Note that precompiling does not affect custom tags etc, but it does affect any partial class extensions.

Set textContent rather than innerHTML

If you know your text contains no HTML then setting to textContent will cause virtually everything to get recycled. Setting innerHTML is fast anyway but textContent does almost nothing.

You can also get another small boost if the new text you're setting is the same length as the old text - this takes advantage of a health counter optimisation:

myElement.textContent = "1";
myElement.textContent = "14"; // 1 character longer

// Using 01 would make a (tiny) saving:
myElement.textContent = "01";
myElement.textContent = "14";

Disabling Unicode Bidirectionality

If you have no plans to use right-to-left or bidirectional text (like Arabic mixed with English), disable it by deleting PowerUI/Resources/BidirectionalData.bytes. This will give you a small memory saving (about 10kb) and slightly boost text performance.

You can also go further by specifying NoBIDI as a "scripting define symbols" as that will effectively remove all of the code which does the BIDI testing.

Avoid late loading CSS

This is a massive performance drainer - avoid it! Put style as high up your document as you can (e.g. in head). That's because a newly loaded CSS selector must check your entire DOM to see if they match any of the elements already in there. If your DOM is basically empty then this operation is much faster.

Meanwhile, a newly loading element uses a range of indices to very rapidly figure out which selectors actually apply to it.