I am working on a non-trivial patch to refine the system app launch time(See https://bugzilla.mozilla.org/show_bug.cgi?id=1094759) now; according to raptor, it's improving launch time by 8~10secs.

Demo

https://www.youtube.com/watch?v=4ZiqaA-VKjM
(Left: with patch; Right: current master.
From the progress bar, you could see the left one is about 12sec faster than the right one.)

Let's see what we did in the past

  1. Whenever a new feature in system is done, find somewhere in the header to insert the new scripts.
  2. If it needs an entry point, put it somewhere in bootstrap.js
  3. Leave it if the ordering of the module loaded does not cause error.
  4. If there is an error? Re-arrange the scripts until the error disappears.

The result is the scripts being added grows unbelievably long and the device boot time is slower and slower.

To improve this

  1. Find out the critical launch path - what is necessary to boot into homescreen or FTU or lockscreen?
  2. Leave every module else as optional and find out a good timing to load them after entering homescreen.
  3. Build dependency tree to make sure dynamic load won't cause race.

Critical launch path

The first app to be launched on a phone is either 'First Time Usage' or 'Homescreen'. To decide which to launch, we need to read several asynchronous values:

  • deviceinfo.os
  • deviceinfo.previous_os
  • ftu.manifestURL
  • homescreen.manifestURL from settings db
  • ftu.enabled from async storage

In the past most of the value fetching is done in FtuLauncher module; however, it's running at the same time as other non-critical scripts are busy running.


To refine this, we removed most scripts from the header, and only let one module named for launcher to get these values at the startup, to make sure all of the boot preference are done before the real bootup procedure.

Dependency Tree Architecture

Core

After launcher decides what to do, it would launch the system core.

The task of system core is simple: it will launch every subsystem which is responsible to certain hardware API. Every subsystem has a core, too, to load all its dependency modules to make sure we control the hardware well.

Hardware subsystems

The hardware subsystem is expected to be independent running from all other concurrent subsystems. Each subsystem has a core as well, which plays as a "smart" loader and decides the priority to load modules, also sometimes cooperate the child modules if necessary.

Example: MobileConnection Subsystem

Window Management Subsystem

This subsystem is playing the most important role of the whole operation system; as you could imagine, it's including most modules and expected to be most complex.
For detail of window management development, see slides here
https://docs.google.com/presentation/d/1nx9k5Gdetnt7dqoX2iehfDXeM7H3be3dMnMYAwZjzRY/edit?usp=sharing

Cooperate subsystems

Service

Each subsystem is expected to be independent from other subsystems.
However, sometimes we still need to query or request other modules across subsystems;
'Service' is created to serve as the mediator between all modules who needs other's help.

Service.query('isHeadsetConnected'); // undefined

Service.registerState('isHeadsetConnected', SoundManager);
SoundManager.isHeadsetConnected = true;
Service.query('isHeadsetConnected'); // true
// Launcher

Service.request('FtuLauncher.launch', ftuManifest);
// FtuLauncher

Service.register('launch', this);
// When being registrated, the queue request would be procceeded right away.
Hierarchy management

Hierarchy Manager is designed to resolve these problems:

  • Some UI events are "top most only"
  • When topmost UI are changed, we need to inform other UI for that.
  • Some UI events are "radiolucent" Before we could lazily load every modules, the biggest problem is several UI components are listening to some UI events by the ordering of DOM event registration.
    // certain_ui.js who needs home button event
    
    start: function() {
      window.addEventListener('home', function(evt) {
        evt.stopImmediatePropagation();
      });
    }
    
    The problem is this kind of event priorization implementation needs to start the module event registration in certain ordering. This makes lazy load difficult because the ordering of loading scripts are not guranteed.

The proper fix is to have a mediator to deal with all UI events priority according to a predefined hierarchy map. With that, an UI module don't need to know the existence of other UI modules, but instead it would be noticed once there is a higher priority one activated/deactivated.

// Lazy loaded module A

Service.request('registerHierarchy'); // join hierarchy competition

this.publish('activated');
// Lazy loaded module B

Service.request('registerHierarchy');
this.publish('activated');

// Assume UI priority B > A:

// A.setHierarchy(false); will be called by HierarchyManager to notify it to do the

// blur() set DOM element's aria-hidden to true because it loses the top most view.

One key point is, without knowing each other, an UI module could be safely started/stopped to join the hierarchy competition, which also means it's safe to remove unwanted UI module or add new UI module somedays, or for certain device type.

BaseModule

During the design of the new architecure, I found that we almost have the same pattern of the behavior of a module:

  1. debugging method
  2. settings observer
  3. event publish/subscribe
  4. start/stop mechanism
  5. rendering if it's an UI module

To group all these patterns together, BaseModule is designed to support them by configuration on the constructor. Each part could be swapped out if necessary. For example, event publish/subscribe is ready to be switched to pubsub library or any other event registration library if necessary.

Settings Observer

SettingsCore is implemented to replace all navigator.mozSettings libraries in the current system to provide a more generic interface to observe the settings. Moreover, an settings should be only observed by a certain settings manager module, and it should notify others when it observes the settings is changed instead of letting every module to observe the same settings at the same time. It's similar to put a cache between settings db and the observer to make sure the getter is getting the value without doing the asynchorous operation each time it needs the value.

Event pub/sub

Event is an important part to decouple modules; currently we are using DOM events to dispatch the event and mostly on the window object. With a centralized event interface it's possible to switch the DOM event into any other 3rd party event library. This is especially important if we are migrating the UI-less modules into ServiceWorker because we cannot access DOM there.

Start/stop mechanism

The ideal world is everything could be started automatically and don't need to care others; however, in real world we need to make sure something is ready before we really init a module.
The paring start/stop's work is to

  1. Register/un-register pre-defined events in start/stop
  2. Observe/de-observe pre-defined settings db keys in start/stop
  3. load+start/stop predefined child modules in start/stop
  4. (optional) Render DOM elements if necessary

Future work: supprting multi-device

To achieve the goal: 'one system to serve multiple devices', there are two solutions

  1. Build time solution: define build flags and inject/generate modules according to the config.
    make DEVICE=phone
    make DEVICE=tv
    
  2. Run time solution:
    if (Service.query('deviceConfig') === 'tv') {
      LazyLoader.load('tv/WindowManager');
    }
    
    However, it might be insufficient to only use 1. for the second display case
    To support the remote display which might has different dimension, we still need to have the ability to load a different UI set at run time.

Ideally, the goal is to split UI-less modules and UI modules; each core in the subsystem is expected to load the UI-less part then the UI part per device type or the request from the remote display; it's still vague at this point, but IMO we have two ways to deal with multiple devices:

  1. Configurable module WindowManager might be a good example. In the current design, it's possible to change only LayoutManager to adaptor multiple windows. (There was an old side project of mine to implement multiple windows, see https://github.com/alivedise/gaia/commit/beee95717b5af037ca60100352e38b0889b2ced8)
  2. Centralized render method per configs If everyone is using BaseUI/BaseModule, it's possible to have the render method and the view method being managed by a ViewManager will will load view for phone or view for tablet accordingly.

Conclusion

  1. Making each module less dependent from others if possible.
  2. Only parent is responsible to load/start/stop child in the timing being designed.
  3. The launching timing of non-blocking modules should not affect each other. A.k.a., the ordering does not matter and everything will work if any new module is started/stopped. Requests before an module which provides the service would be queued until it is started.
  4. Generic interface for UI-less module(BaseModule) and UI module(BaseUI).

Comments

comments powered by Disqus