Bangle.js Fast Loading

Fast Loading is when an app can remove all its code from memory so that a new app (like the Launcher) can be loaded without having to completely reset Bangle.js's state.

Normally an app can do whatever it wants because everything gets totally reset afterwards and reloaded - but this process can take 200ms or more, so if it can be avoided your Bangle will run faster.

Some clocks (like Anton) implement this, and you can implement it in your own apps too, as long as your app uses widgets.

We're recommending you only use this on Clocks and Launchers right now - the potential improvement for apps is small (as it only affects loading the next app after yours), and the potential for things to break is much higher!

BE CAREFUL! - because the environment persists, anything you don't free or reset to the correct state can cause a memory leak or interfere with the next app.

There are a few things you need to get right with your app. As an example we'll use this code (from here)

// timeout used to update every minute
var drawTimeout;

// schedule a draw for the next minute
function queueDraw() {
  if (drawTimeout) clearTimeout(drawTimeout);
  drawTimeout = setTimeout(function() {
    drawTimeout = undefined;
    draw();
  }, 60000 - (Date.now() % 60000));
}

function draw() {
  var x = g.getWidth()/2;
  var y = g.getHeight()/2;
  g.reset();
  // work out locale-friendly date/time
  var date = new Date();
  var timeStr = require("locale").time(date,1);
  var dateStr = require("locale").date(date);
  // draw time
  g.setFontAlign(0,0).setFont("Vector",48);
  g.clearRect(0,y-15,g.getWidth(),y+25); // clear the background
  g.drawString(timeStr,x,y);
  // draw date
  y += 35;
  g.setFontAlign(0,0).setFont("6x8");
  g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background
  g.drawString(dateStr,x,y);
  // queue draw in one minute
  queueDraw();
}

// Clear the screen once, at startup
g.clear();
// draw immediately at first, queue update
draw();
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();

Testing

The first thing we need to do is to be able to test how much memory our clock uses.

  • Copy the above file into the IDE, and set it to save to s Storage File called myclock.app.js by clicking the down-arrow below the upload icon in the IDE.
  • Now type reset() in the left-hand side - this will reset Bangle.js without loading any app
  • Now type Bangle.loadWidgets();Bangle.drawWidgets(); - this will load the widgets (because we want to include these in our 'before' memory measurement)
  • Now type process.memory().usage - this shows us how much memory is used by just the widgets. In my case it's 465 - yours will be different.
  • Now, we'll load our app with eval(require("Storage").read("myclock.app.js"))
  • We can now run process.memory().usage again and see how much memory is being used now the app is loaded - it's 609 in my case

When unloading is working, you should be able to type Bangle.setUI() in the left-hand side, and then process.memory().usage, and you should see the *same number that you had from just after you called Bangle.loadWidgets(); (in my case 465)

Note: this may not be entirely true (see below)

Ok, so now it's time to get started making sure memory is freed.

1. Bangle.setUI({ ..., remove })

The first step is to add a remove handler to setUI - this shows that your app supports unloading, and this handler should remove anything that got allocated or any timers that got started.

First, replace Bangle.setUI("clock"); with:

Bangle.setUI({mode:"clock", remove:function() {
  if (drawTimeout) clearTimeout(drawTimeout);
}});

This code removes the timeout added by queueDraw which makes your clock redraw. Your clock will have something similar.

2. Scoping

Right now, everything that is defined is in the global scope, which is great for debugging. However, it means that drawTimeout, queueDraw, etc will all stay allocated.

We want to define them in their own scope, so that when drawTimeout is removed everything is de-allcoated.

The classic way to do this is to wrap everything in a function (function() { ... })() but this has the downside that when loading your app the function has to be parsed once to allocate it and again to execute - which will slow down your app's loading.

Instead, we'll put the code in a block ({...}) and instead of using var we'll use let and const, and instead of function foo() { ... } we'll use let foo = function() { ... };

So now our code looks like this:

{
  // timeout used to update every minute
  let drawTimeout;

  // schedule a draw for the next minute
  let queueDraw = function() {
    if (drawTimeout) clearTimeout(drawTimeout);
    drawTimeout = setTimeout(function() {
      drawTimeout = undefined;
      draw();
    }, 60000 - (Date.now() % 60000));
  };

  let draw = function() {
    var x = g.getWidth()/2;
    var y = g.getHeight()/2;
    g.reset();
    // work out locale-friendly date/time
    var date = new Date();
    var timeStr = require("locale").time(date,1);
    var dateStr = require("locale").date(date);
    // draw time
    g.setFontAlign(0,0).setFont("Vector",48);
    g.clearRect(0,y-15,g.getWidth(),y+25); // clear the background
    g.drawString(timeStr,x,y);
    // draw date
    y += 35;
    g.setFontAlign(0,0).setFont("6x8");
    g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background
    g.drawString(dateStr,x,y);
    // queue draw in one minute
    queueDraw();
  };

  // Clear the screen once, at startup
  g.clear();
  // draw immediately at first, queue update
  draw();
  // Show launcher when middle button pressed
  Bangle.setUI({mode:"clock", remove:function() {
    if (drawTimeout) clearTimeout(drawTimeout);
  }});
  // Load widgets
  Bangle.loadWidgets();
  Bangle.drawWidgets();
}

Event Handlers

For some apps/clock faces, you may add event handlers - usually these will take the form Bangle.on('event', handler - for example in the Clock examples we use Bangle.on('lcdPower' to detect if the LCD is turned on for Bangle.js 1:

Bangle.on('lcdPower',on=>{
  if (on) {
    draw(); // draw immediately, queue redraw
  } else { // stop draw timer
    if (drawTimeout) clearTimeout(drawTimeout);
    drawTimeout = undefined;
  }
});

We need to remove these handlers. To do this (and not remove other handlers) you need the function you use to be in a variable. So you'll need to change your code to:

let onLCDPower = on => {
  if (on) {
    draw(); // draw immediately, queue redraw
  } else { // stop draw timer
    if (drawTimeout) clearTimeout(drawTimeout);
    drawTimeout = undefined;
  }
};
Bangle.on('lcdPower',onLCDPower);

// then in Bangle.setUI({mode:"clock", remove:function() {
Bangle.removeListener('lcdPower',onLCDPower);

Your clock may also use setWatch on a button - but in this case we'd recommend you use Bangle.setUI({mode : "custom", ...} to handle this - see the reference

Other libraries

You might find that other libraries you've used are registering for Bangle.js input as well. If so, you'll need to call a function provided by the library that will remove that.

If the library registers for input and can't be unloaded then it may not be suitable for 'fast load'.

As an example, the clock_info.js module allows you to add custom information to your clock face:

let clockInfoItems = require("clock_info").load();
let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { ... });

In this case, clockInfoMenu contains a function called remove which you'll need to call in your remove function:

// in Bangle.setUI({mode:"clock", remove:function() {
clockInfoMenu.remove();

Or if your app used the widget_utils.js module to hide widgets (or make them swipeable) it must use .show() to show them again.

Bangle.js state

You may also have configured something in Bangle.js - for example:

  • Turning hardware on (like the heart rate monitor)
  • Setting the Theme to something other than the system theme

And you'll have to reset these to what they were when your app was loaded.

Changes to global variables

Your app might also have added to a global variable. For example if your clock face uses a custom font, it may have code like this:

Graphics.prototype.setFontAnton = function(scale) {
  ...
};

In that case you'll have to add:

// in Bangle.setUI({mode:"clock", remove:function() {
delete Graphics.prototype.setFontAnton;

Testing Afterwards

Ok, now it's time to test:

Type reset() to reset the Bangle's state.

Now paste the following into the IDE's left hand side:

Bangle.loadWidgets();Bangle.drawWidgets();
process.memory().usage
eval(require("Storage").read("myclock.app.js"))
process.memory().usage
Bangle.setUI()
process.memory().usage

You should get something along the lines of:

=465
=undefined
=619
=undefined
=565
  • 465 : before the app was loaded
  • 619 : after the app was loaded
  • 565 : when the app was unloaded

They're not the same! So what's wrong?

Well, if modules are loaded into memory then they will stay loaded, and we're using require("locale"), which is using memory. We don't actually care about 'losing' this memory, but we just need to check we're not leaking memory anywhere else.

We can get an idea of how much memory modules are using with E.getSizeOf(global["\xff"].modules) (although this may not be accurate if the modules reference user code).

But the best way to check is just to re-load and free your app again:

eval(require("Storage").read("myclock.app.js"))
process.memory().usage
Bangle.setUI()
process.memory().usage

And this gives us:

=undefined
=619
=undefined
=565

The same figures as before - showing that we're not leaking memory!

Now that's done, your clock is good to publish. You'll notice that now, when you press the button to enter the Launcher, there is no Loading... popup and the Launcher will be loaded much more quickly!

Taking Advantage of Fast Loading

Now that your clock uses fast loading, anything loaded from your app can be loaded quickly as long as that app uses widgets too

Using Bangle.setUI({mode:"clock",...}) means that he button-press will load the launcher, as will Bangle.showLauncher() - and these commands are smart enough to do a fast load.

You can also use Bangle.showClock() (if your app is not a clock - eg a launcher). Or, you can use Bangle.load("load_me.app.js") - but again, the app you load must use widgets, since your Clock would have loaded with widgets and they cannot be unloaded.

This page is auto-generated from GitHub. If you see any mistakes or have suggestions, please let us know.