Performance

From PowerUI
Revision as of 01:07, 27 March 2017 by 151.229.136.200 (talk) (Only redraw your canvas when PowerUI does)
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.

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.

Table Chaos

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 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;
  • 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 your 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.

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.

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. 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();
        
    }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;
    
}

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);
    // ...
    
}