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
Whenever a new feature in system is done, find somewhere in the header to insert the new scripts.
If it needs an entry point, put it somewhere in bootstrap.js
Leave it if the ordering of the module loaded does not cause error.
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
Find out the critical launch path - what is necessary to boot into homescreen or FTU or lockscreen?
Leave every module else as optional and find out a good timing to load them after entering homescreen.
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.
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.
// 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:
debugging method
settings observer
event publish/subscribe
start/stop mechanism
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
Register/un-register pre-defined events in start/stop
Observe/de-observe pre-defined settings db keys in start/stop
load+start/stop predefined child modules in start/stop
(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
Build time solution: define build flags and inject/generate modules according to the config.
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:
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
Making each module less dependent from others if possible.
Only parent is responsible to load/start/stop child in the timing being designed.
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.
Generic interface for UI-less module(BaseModule) and UI module(BaseUI).