├── .gitignore
├── LICENSE
├── README.md
├── app
├── index.js
├── screen1.js
├── screen2.js
└── view.js
├── docs
├── number.png
├── object-properties.png
└── optimization-guidelines.md
├── package.json
└── resources
├── index.gui
├── screen1.gui
├── screen2.gui
├── styles.css
└── widgets.gui
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.fba
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Vlad Balin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ionic Views
2 |
3 | An application for the Fitbit Ionic can quickly become a mess. This micro-framework provides basic support for View and subview patterns in about 100 LOCs, providing the necessary structure to keep the view layer code manageable when your application grows.
4 |
5 | > Read [FitbitOS JavaScript Optimization Guidlines](/docs/optimization-guidelines.md) to make sure you understand the limitations of the platform you're working with.
6 |
7 | ## Features
8 |
9 | - jQuery-style `$` DOM selectors.
10 | - `View` base class:
11 | - views can be dynamically inserted and removed;
12 | - `onMount()`/`onUnmount()` lifecycle hooks;
13 | - hierarchical subviews support;
14 | - supressed render when device screen is off to reduce energy consumption.
15 | - `Application` base class with screen switching support.
16 | - Multi-screen application boilerplate.
17 |
18 | ## Installation
19 |
20 | This repository contains the starting boilerplate for the multi-screen project. You could copy all the files as they are,
21 | or just copy the `/app/view.js` file to your project.
22 |
23 | ## API
24 |
25 | ### DOM selectors
26 |
27 | #### `function` $( query, [ element ] )
28 |
29 | jQuery-style `$` query to access SVG DOM elements. No wrapping is performed, the raw element or elements array is returned.
30 | If an `element` argument is provided, the element's subtree will be searched; otherwise the search will be global.
31 |
32 | The `query` is the space-separated sequence of the following simple selectors:
33 |
34 | - `#id-of-an-element` - will call `element.getElementById( 'id-of-an-element' )` and return en element.
35 | - `.class-name` - will call `element.getElementsByClassName( 'class-name' )` and return elements array.
36 | - `element-type` - will call `element.getElementsByTypeName( 'element-type' )` and return elements array.
37 |
38 | If all of the selectors in the query are `#id` selectors, the single element will be returned. Otherwise, an array of elements will be returned.
39 |
40 | ```javascript
41 | import { $ } from './view'
42 |
43 | // Will search for #element globally
44 | $( '#element' ).style.display = 'inline';
45 |
46 | // Find the root element of the screen, then show all underlying elements having "hidden" class.
47 | $( '#my-screen .hidden' ).forEach( ({ style }) => style.display = 'inline' );
48 |
49 | // The same sequence made in two steps. See `$at()` function.
50 | const screen = $( '#my-screen' );
51 | $( '.hidden', screen ).forEach( ({ style }) => style.display = 'inline' );
52 |
53 | ```
54 |
55 | > Avoid repeated ad-hoc $-queries. Cache found elements when possible. See Elements Group pattern.
56 |
57 | #### `function` $at( id-selector )
58 |
59 | Create the $-function to search in the given DOM subtree. Used to enforce DOM elements isolation for different views.
60 |
61 | When called without arguments, returns the root element.
62 |
63 | ```javascript
64 | import { $at } from './view'
65 |
66 | const $ = $at( '#my-screen' );
67 |
68 | // Make #myscreen visible
69 | $().style.display = 'inline';
70 |
71 | // Will search descendants of #myscreen only
72 | $( '#element' ).style.display = 'inline';
73 | ```
74 |
75 | #### `function` $wrap( element )
76 |
77 | Create the $-function to search in the given DOM subtree wrapping the given element.
78 |
79 | ```javascript
80 | const $at = selector => $wrap( $( selector ) );
81 | ```
82 |
83 | ### `pattern` Elements Group
84 |
85 | An obvious and most memory-efficient way to encapsulate UI update logic is to define an update function. The function can be called directly to update encapsulated elements. It should be preferred for small and simple widgets.
86 |
87 | ```javascript
88 | function time(){
89 | // Preallocate SVG DOM elements. Ad-hoc DOM lookups should be avoided.
90 | const minutes = $( '#minutes' ),
91 | seconds = $( '#seconds' );
92 |
93 | // Return update function.
94 | return seconds => {
95 | minutes.text = Math.floor( seconds / 60 );
96 | seconds.text = ( seconds % 60 ).toFixed( 2 );
97 | }
98 | }
99 |
100 | class Timer extends View {
101 | // Create time widget.
102 | time = time();
103 | ...
104 |
105 | onRender(){
106 | ...
107 | this.time( this.seconds );
108 | }
109 | }
110 | ```
111 |
112 | SVG DOM element reference takes 32 bytes, and it's highly beneficial if it will be cached. Ad-hoc element lookups should be avoided.
113 |
114 | ### `class` View
115 |
116 | View is the stateful group of elements. The difference from the elements group is that views can me contained in each other and they have `onMount`/`onUnmount` lifecycle hooks. API:
117 |
118 | - `view.el` - optional root view element. Used to show and hide the view when its mounted and unmounted.
119 | - `view.onMount( options )` - place to insert subviews and register events listeners. `options` is the first parameter passed the view's constructor.
120 | - `view.render()` - render the view and all of its subviews if the display is on. No-op otherwise.
121 | - `view.onRender()` - place actual UI update code here.
122 | - `view.onUnmount()` - place to unregister events listeners.
123 | - `view.insert( subview )` - insert and mount the subview.
124 | - `view.remove( subview )` - remove and unmount the subview.
125 |
126 | Example:
127 |
128 | ```javascript
129 | import { $at } from './view'
130 | const $ = $at( '#timer' );
131 |
132 | class Timer extends View {
133 | el = $();
134 |
135 | onMount(){
136 | clock.granularity = "seconds";
137 | clock.ontick = () => {
138 | this.ticks++;
139 | this.render();
140 | }
141 | }
142 |
143 | ticks = 0;
144 |
145 | minutes = $( '#minutes' );
146 | seconds = $( '#seconds' );
147 |
148 | onRender(){
149 | const { ticks } = this;
150 | this.minutes.text = Math.floor( ticks / 60 );
151 | this.seconds.text = ( ticks % 60 ).toFixed( 2 );
152 | }
153 |
154 | onUnmount(){
155 | clock.granularity = "off";
156 | clock.ontick = null;
157 | }
158 | }
159 | ```
160 |
161 | ### `class` Application
162 |
163 | Application is the main view having the single `screen` subview.
164 | It's the singleton which is globally accessible through the `Application.instance` variable.
165 |
166 | - `MyApp.screens = { View1, View2, ... }` - all screens must be registered here.
167 | - `MyApp.start( 'View1' )` - instantiate and mount the application, display the screen with a goven name.
168 | - `Application.instance` - access an application instance.
169 | - `Application.switchTo( 'screenName' )` - switch to the screen which is the member of an application.
170 | - `app.screen` - property used to retrieve the current screen view.
171 | - `app.render()` - render everything, _if display is on_. It's called automaticaly when display goes on.
172 |
173 | ```javascript
174 | class MyApp extends Application {
175 | screens = { Screen1View, Screen2View, LoadingView }
176 | }
177 |
178 | MyApp.start( 'LoadingView' );
179 |
180 | ...
181 | // To switch the screen, use:
182 | Application.switchTo( 'Screen2View' );
183 | ```
184 |
185 | ## Project structure
186 |
187 | Application may consist of several screens. The following project structure is recommended for this case:
188 |
189 | - `app/` <- standard app folder
190 | - `index.js` <- `Application` subclass class, which will switch the screens
191 | - `view.js` <- copy this file to your project
192 | - `screen1.js` <- each screen is defined as `View` subclass
193 | - `screen2/` <- if there are many modules related to the single screen, group them to the folder
194 | - ...
195 | - `resources/` <- standard resources folder
196 | - `index.gui` <- include screens SVG files with ``
197 | - `widgets.gui` <- include screens CSS files with ``
198 | - `screen1.gui` <- put SVG for your screens to different files
199 | - `screen2/` <- group SVG, CSS, and images used by a screen to a folder
200 |
--------------------------------------------------------------------------------
/app/index.js:
--------------------------------------------------------------------------------
1 | import document from "document";
2 | import { Application } from './view'
3 | import { Screen1 } from './screen1'
4 | import { Screen2 } from './screen2'
5 |
6 | class MultiScreenApp extends Application {
7 | // List all screens
8 | screens = { Screen1, Screen2 }
9 |
10 | // "down" key handler
11 | onKeyDown(){
12 | // Switch between two screens we have.
13 | Application.switchTo( this.screen.constructor === Screen1 ? 'Screen2' : 'Screen1' );
14 | }
15 | }
16 |
17 | // Start the application with Screen1.
18 | MultiScreenApp.start( 'Screen1' );
--------------------------------------------------------------------------------
/app/screen1.js:
--------------------------------------------------------------------------------
1 | import { View, $at } from './view'
2 | import clock from 'clock'
3 |
4 | const $ = $at( '#screen-1' );
5 |
6 | export class Screen1 extends View {
7 | // Root view element used to show/hide the view.
8 | el = $(); // Extract #screen-1 element.
9 |
10 | // Element group.
11 | time = time();
12 |
13 | // The view state.
14 | seconds = 0;
15 |
16 | onMount(){
17 | // Subscribe for the clock...
18 | clock.granularity = 'seconds';
19 |
20 | clock.ontick = () => {
21 | // Update the state and force render.
22 | this.seconds++;
23 | this.render();
24 | }
25 | }
26 |
27 | onRender(){
28 | // Render the elements group.
29 | this.time( this.seconds );
30 | }
31 |
32 | onUnmount(){
33 | // Unsubscribe from the clock
34 | clock.granularity = 'off';
35 | clock.ontick = null;
36 | }
37 |
38 | // Screens may have they own key handlers.
39 | onKeyUp(){
40 | console.log( 'Key Up!');
41 | }
42 | }
43 |
44 |
45 | // Elements group
46 | function time(){
47 | const minutes = $( '#minutes' ),
48 | seconds = $( '#seconds' );
49 |
50 | return secs => {
51 | minutes.text = ( secs / 60 ) | 0;
52 | seconds.text = secs % 60;
53 | }
54 | }
--------------------------------------------------------------------------------
/app/screen2.js:
--------------------------------------------------------------------------------
1 | import { View, $at } from './view'
2 |
3 | // Create the root selector for the view...
4 | const $ = $at( '#screen-2' );
5 |
6 | export class Screen2 extends View {
7 | // Specify the root view element.
8 | // When set, it will be used to show/hide the view on mount and unmount.
9 | el = $();
10 |
11 | // Ad-hoc $-queries must be avoided.
12 | // You've got dumb 120MHz MCU with no JIT in VM, thus everything you do is expensive.
13 | // Put all of your elements here, like this:
14 |
15 | // otherEl = $( '#other-el-id' );
16 | // elementsArray = $( '.other-el-class' );
17 |
18 | // Lifecycle hook executed on `view.mount()`.
19 | onMount(){
20 | // TODO: insert subviews...
21 | // TODO: subscribe for events...
22 | }
23 |
24 | // Lifecycle hook executed on `view.unmount()`.
25 | onUnmount(){
26 | // TODO: unsubscribe from events...
27 | this.trash = null;
28 | }
29 |
30 | // Custom UI update logic, executed on `view.render()`.
31 | onRender(){
32 | // TODO: put DOM manipulations here...
33 | // Call this.render() to update UI.
34 | }
35 | }
--------------------------------------------------------------------------------
/app/view.js:
--------------------------------------------------------------------------------
1 | import document from "document";
2 | import { display } from "display";
3 |
4 | // Main DOM search method.
5 | export function $(query, el) {
6 | const selectors = query.match(/\.|#|\S+/g);
7 | let root = el || document;
8 |
9 | for (let i = 0; root && i < selectors.length; i++) {
10 | const s = selectors[i];
11 | root =
12 | s === "#"
13 | ? $id(selectors[++i], root)
14 | : s === "."
15 | ? $classAndType("getElementsByClassName", selectors[++i], root)
16 | : $classAndType("getElementsByTypeName", s, root);
17 | }
18 |
19 | return root;
20 | }
21 |
22 | // Search subtrees by id...
23 | function $id(id, arr) {
24 | if (Array.isArray(arr)) {
25 | const res = [];
26 |
27 | for (let i = arr.length; i--; ) {
28 | const x = arr[i].getElementById(id);
29 | if (x) res.push(x);
30 | }
31 |
32 | return res;
33 | }
34 |
35 | return arr.getElementById(id);
36 | }
37 |
38 | // Search subtrees by class or type...
39 | function $classAndType(method, arg, arr) {
40 | if (Array.isArray(arr)) {
41 | const res = [];
42 |
43 | for (let i = arr.length; i--; ) {
44 | const el = arr[i][method](arg);
45 | for (let j = el.length; j--; ) {
46 | res.push(el[j]);
47 | }
48 | }
49 |
50 | return res;
51 | }
52 |
53 | return arr[method](arg);
54 | }
55 |
56 | export function $wrap(element) {
57 | return selector => (selector ? $(selector, element) : element);
58 | }
59 |
60 | export function $at(selector) {
61 | return $wrap($(selector));
62 | }
63 |
64 | function show(view, yes) {
65 | const { el } = view;
66 | if (el) el.style.display = yes ? "inline" : "none";
67 | }
68 |
69 | function mount(view) {
70 | show(view, true);
71 | view.onMount(view.options);
72 | }
73 |
74 | function unmount(view) {
75 | const { _subviews } = view;
76 | if (_subviews) {
77 | let i = _subviews.length;
78 | while (i--) unmount(_subviews[i]);
79 |
80 | delete view._subviews;
81 | }
82 | view.onUnmount();
83 | show(view, false);
84 | }
85 |
86 | export class View {
87 | constructor(options) {
88 | if (options) this.options = options;
89 | }
90 |
91 | insert(subview) {
92 | const subviews = this._subviews || (this._subviews = []);
93 | subviews.push(subview);
94 | mount(subview);
95 | return this;
96 | }
97 |
98 | remove(subview) {
99 | const { _subviews } = this;
100 | _subviews.splice(_subviews.indexOf(subview), 1);
101 | unmount(subview);
102 | }
103 |
104 | render() {
105 | if (display.on) {
106 | const { _subviews } = this;
107 | if (_subviews)
108 | for (let i = _subviews.length; i--; ) _subviews[i].render();
109 |
110 | this.onRender();
111 | }
112 | }
113 | }
114 |
115 | const ViewProto = View.prototype;
116 | ViewProto.onKeyBack = ViewProto.onKeyDown = ViewProto.onKeyUp = ViewProto.onMount = ViewProto.onUnmount = ViewProto.onRender = function() {};
117 |
118 | export class Application extends View {
119 | setScreen(s) {
120 | if (this.screen) this.remove(this.screen);
121 |
122 | // Poke the display so it will be on after the screen switch...
123 | display.poke();
124 |
125 | this.insert((this.screen = s)).render();
126 | }
127 |
128 | // Switch the screen
129 | static switchTo(screenName) {
130 | const { instance } = Application;
131 | instance.setScreen(new instance.screens[screenName]());
132 | }
133 |
134 | static start(screen) {
135 | // Instantiate and mount an application.
136 | const app = (Application.instance = new this());
137 | Application.switchTo(screen);
138 | mount(app);
139 |
140 | // Refresh UI when the screen in on.
141 | display.onchange = () => {
142 | app.render();
143 | };
144 |
145 | document.onkeypress = (evt) => {
146 | if (evt.key === "back") app.onKeyBack(evt);
147 | else if (evt.key === "down") app.onKeyDown(evt);
148 | else if (evt.key === "up") app.onKeyUp(evt);
149 | };
150 | }
151 |
152 | onKeyBack(evt) {
153 | this.screen.onKeyBack(evt);
154 | }
155 |
156 | onKeyDown(evt) {
157 | this.screen.onKeyDown(evt);
158 | }
159 |
160 | onKeyUp(evt) {
161 | this.screen.onKeyUp(evt);
162 | }
163 |
164 | }
165 |
--------------------------------------------------------------------------------
/docs/number.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaperton/ionic-views/6b4a6ba07bb71cf2bc4203ddc49e0f1f4813e52f/docs/number.png
--------------------------------------------------------------------------------
/docs/object-properties.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaperton/ionic-views/6b4a6ba07bb71cf2bc4203ddc49e0f1f4813e52f/docs/object-properties.png
--------------------------------------------------------------------------------
/docs/optimization-guidelines.md:
--------------------------------------------------------------------------------
1 | # FitbitOS JavaScript Optimization Guidelines
2 |
3 | FitbitOS relies on [JerryScript](http://jerryscript.net/) virtual machine. JerryScript is a full-featured JS engine fully compatible with ECMA-262 edition 5.1. It is designed for microcontrollers having restricted RAM and is primarily optimized for the low memory consumption. It can operate with less than 64KB RAM, it doesn't have a JIT, and it is in general much slower than popular JS engines.
4 | Here's [the document](https://wiki.tizen.org/images/5/52/04-JerryScript_ECMA_Internal_and_Memory_Management.pdf) describing JerryScript internals and its memory architecture.
5 |
6 | Fitbit doesn't publish the detailed hardware specs for their devices, however, [it's known](https://toshiba.semicon-storage.com/eu/product/assp/applite/tz1200.html) that Fitbit Ionic:
7 | - uses ARM Cortex-M4F core running at 120 MHz.
8 | - has pretty decent hardware 2D accellerator supporting vector graphics and bitmap rotation.
9 | - has 64KB limit of JS memory for the code and the heap combined.
10 |
11 | ## There's no JIT, everything is slow
12 |
13 | Keep in mind that in contrast to the desktop JS engines JerryScript is a pure interpreter, so optimization techniques you're probably familiar with doesn't work. Don't expect any smart optimization from the runtime, it's not smart. An every extra operation costs you performance. It might seem scary at first, but it's not that bad because the rendering pipeline is hardware accelerated and JS is used for the reaction on events from sensors and user input only.
14 |
15 | Take this loop as an example:
16 |
17 | let i = 1000, x = 0;
18 | while( i-- ) x++;
19 |
20 | It is about 15% faster, than this loop:
21 |
22 | let x = 0;
23 | for( let j = 0; j < 1000; j++ ) x++;
24 |
25 | It happens because the comparison with 1000 is more expensive than the simple check that the value is truthy.
26 |
27 | The bottom line is that nothing is free in JerryScript, but the performance is easy to predict. Carefuly review your code, remove extra operations, cache intermediate results in valiables, and do the rest of stuff people did on their Commodore 64 in 80th.
28 |
29 | ## Floating point numbers are expensive
30 |
31 | JS Number type doesn't make a difference between integers and floats, and ECMA-262 requires the floating point to implement [64-bit IEEE math](https://en.wikipedia.org/wiki/IEEE_754). ARM Cortex-M4F has not hardware accellerated 64-bit floating point math, thus it's implemented in software and is quite slow. You can expect an execution speed to be about 4-5K of arithmetic operations per second for a Fitbit Ionic. Integer math is slightly (about 25%) faster.
32 |
33 | There's an important difference between integers and floating point numbers in the memory consumption, however.
34 |
35 | 
36 |
37 | JerryScript operates with 32-bit values internally, and small integers which fits 29 bit (< 268,435,456) are being packed in the value directly. In contrast, the floating point number or large integer will be allocated in the heap as 64-bit float, with a value holding a pointer to it. Therefore, the floating point number takes 12 bytes, while the small integer will fit in 4.
38 |
39 | let a = 1; // 4 bytes
40 | let a = 0.5; // 12 bytes
41 |
42 | Taking the 64KB heap size into account, the whole heap can hold about 5.5K of floating point numbers or 16K of small integers.
43 |
44 | ## Use typed arrays when possible
45 |
46 | The problem with floating point numbers become more significant if you have to deal with number arrays. To mitigate that, JerryScript supports [JS Typed Arrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays). They are array-like objects which holds unboxed numbers of the predefined type. Typed arrays operate on the preallocated memory buffer, thus you cannot resize them (no push/pop/shift/unshift methods). They, however, give you an opportunity to save memory.
47 |
48 | If you have array of floating point numbers, consider usage of [Float32Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array). It uses three times less memory than the regular array of floats.
49 |
50 | // Regular array of floats will allocate 12KB in total
51 | let b = Array( 1024 ); // 4KB for the array with values
52 | for( let i = 0; i++; i < 1024 ) b[ i ] = i + 0.5; // + 8KB with floats
53 |
54 | // Just 4KB will be allocated
55 | let a = new Float32Array( 1024 );
56 |
57 | If you have array of integers, consider usage of [Int16Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int16Array) (two times less memory), [Int8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int8Array)(four times less memory), or their unsigned versions. [Int32Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int32Array) has no advantage over the regular array of small integers.
58 |
59 | ## Prefer small objects
60 |
61 | An object is represented in memory as linked list of property pairs, taking about 8 bytes per propery plus some constant (obj size = Math.ceil( propsCount / 2 ) * 16 + 16 ).
62 |
63 | let empty = {}; // 16 bytes are allocated.
64 |
65 | 
66 |
67 | When the number of object properties is greater than 16, it will allocate the property hashmap in addition to the property list. Therefore, an approximate object size is:
68 |
69 | - ~8 bytes per property for small objects + 16 bytes (16 members and less)
70 | - ~10 bytes per property + some bigger constant (~32 bytes) for large object.
71 |
72 | Closure (lexical environment) and objects use the same internal structure, which means that there will be no difference in memory layout for these examples:
73 |
74 | // Object
75 | let o1 = { a : 1, b : 2 };
76 |
77 | let f1 = ( () => {
78 | let a = 1, b = 2;
79 | return () => a + b; // Will allocate an object with a shape similar to 'o1'
80 | })();
81 |
82 | Thus, it's beneficial that both objects and functions will not be excessively large.
83 |
84 | ## Consider object of arrays vs array of objects
85 |
86 | Suppose we have an array of objects of uniform shape:
87 |
88 | const points = [ { x : 1.5, y : 1.5 }, ... ];
89 |
90 | An object takes about 2 * 8 = 16 bytes, 64-bit floats will take another 16 giving us 32 bytes per object. So, `points` array will take about 32 * N + 4 * N = 36 * N bytes (calculations are approximate, constants are ignored).
91 |
92 | Now lets turn the data structure inside out. It will take 24 * N + 16 bytes which is about 30% less than the previous option.
93 |
94 | const points = { xs : [ 1.5, ... ], ys : [ 1.5, ... };
95 |
96 | Having the arrays of numbers, we can use `Float64Array`, which will give us 16 * N + 16 bytes. It's more than twice less than for the original code.
97 |
98 | const points = { xs : new Float64Array( N ), ys : new Float64Array( N ) };
99 |
100 | And finally, if we switch from `Float64Array` to `Float32Array`, it will cut the size of the data structure further by half, giving us 8 * N + 16. Now our code is 4.5 times less memory hungry than the original one.
101 |
102 | ## Pack boolean flags to bitmaps
103 |
104 | This object holding the set of boolean flags will take about 4 x 8 = 32 bytes.
105 |
106 | const x = {
107 | a : true,
108 | b : true,
109 | c : true,
110 | d : true
111 | }
112 | ...
113 | if( x.a ) doA();
114 | if( x.d ) doD();
115 |
116 | x.b = true;
117 | x.c = false;
118 |
119 | The corresponding binary flas bitmap would be just an integer value taking 4 bytes. The single JerryScript value with small integer can fit up to 29 bit flags.
120 |
121 | let x = 0b1111;
122 | ...
123 | if( x & 0b0001 ) doA();
124 | if( x & 0b1000 ) doD();
125 |
126 | x |= 0b0100;
127 | x &= 0b1011;
128 |
129 | ## Functions are objects too
130 |
131 | The following constant defined in the application will take about 7*8 ~ 64 bytes of heap. Literal strings are being allocated in the separate literal storage during the parsing phase.
132 |
133 | const days = {
134 | "Sunday": "No School",
135 | "Monday": "Normal",
136 | "Tuesday": "Normal",
137 | "Wednesday": "Normal",
138 | "Thursday": "Normal",
139 | "Friday": "Normal",
140 | "Saturday": "No School"
141 | }
142 |
143 | This code, however, completely avoids this 64 bytes allocation:
144 |
145 | function dayToSchedule( day ){
146 | switch( day ){
147 | case "Sunday": return "No School";
148 | case "Monday": return "Normal";
149 | case "Tuesday": return "Normal";
150 | case "Wednesday": return "Normal";
151 | case "Thursday": return "Normal";
152 | case "Friday": return "Normal";
153 | case "Saturday": return "No School;
154 | }
155 | }
156 |
157 | Seems to be a good idea. Right? Nah, it's not! Surprisingly, **tests shows that the first option consumes less memory**.
158 |
159 | Thing is that we're not really avoiding an object's creation here as we intended. Functions are first-class objects in JS. When we define a function, the function object with properties is being created on the heap, not just the bytecode for the function body. An empty function *add about ~80 bytes in total*. The bytecode with a switch statement + function object makes `dayToSchedule()` to consume more memory than the statically allocated `days` object.
160 |
161 | The fact that even an empty function reduce our memory by 80-100 bytes leads us to important conclustion: **don't make a function without a reason.** If your particular function is small that's fine, but the programming style relying on small functions should be avoided if possible.
162 |
163 | ## Static vs dynamic resource allocation
164 |
165 | Now let's take the `days` object from the previous example, and try to wrap its creation in a function. It might seem that if we delay the object creation to the moment when it will be really needed, it will help us to save some memory.
166 |
167 | function getDays(){
168 | return {
169 | "Sunday": "No School",
170 | "Monday": "Normal",
171 | "Tuesday": "Normal",
172 | "Wednesday": "Normal",
173 | "Thursday": "Normal",
174 | "Friday": "Normal",
175 | "Saturday": "No School"
176 | }
177 | }
178 |
179 | Again, the code above consumes **more memory** than the statically allocated `days` object from the previous section _even if `getDays()` function is never called_, because the function is not just the bytecode but an object too and `days` object is too small. It would make sense to do this trick if the object is large enough (more than 16 props, contains nested members, etc).
180 |
181 | As a general rule for embedded programming in a constrained environment, the static resource allocation is preferable. Try to reduce dynamic allocation to a reasonable minimum, and assign object references with `null` as soon as you don't need them.
182 |
183 | ## What about JS functional programming, it's so cool
184 |
185 | No, it's not. Not in JerryScript. "JS functional programming" relies on both small functions and dynamically created immutable objects. You should do exactly opposite things in a resource-constrained environment like Fitbit smartwatch.
186 |
187 | Also, React-like frameworks are impossible. You can't add/remove SVG DOM nodes in Fitbit SDK, and SVG DOM manipulation is generally much faster than comparing trees in memory. Not to mention that you don't really have memory; just modest 64KB.
188 |
189 | ## DO NOT use for-of loop (weird)
190 |
191 | Fitbit SDK uses TypeScript to transpile modern JavaScript to ES5 which is recognized by JerryScript VM. Many ES6 and ES7 features comes with a hidden cost. For unclear reason, the `for-of` loop dramatically increase the memory usage.
192 |
193 | // DO NOT:
194 | for( let y of x ) z += y;
195 |
196 | When we replace it with a raw for loop it will release about 750 bytes of memory:
197 |
198 | // DO:
199 | for( let i = x.length; i--; ) z += y[i];
200 |
201 | It shouldn't be so. But it is.
202 |
203 | ## In doubts? Measure.
204 |
205 | 1. Do not over-optimize until you are in trouble. There's a good chance that 64K will be enough for your smartwatch app.
206 | 2. Do not guess when making optimizations. Measure.
207 |
208 | There's the special API to [measure an amount of allocated JS memory](https://dev.fitbit.com/build/reference/device-api/system/). It's not always 100% byte-to-byte accurate, but it can give you a good idea when your optimizations really helped or made things worse.
209 |
210 | import { memory } from "system";
211 | console.log("JS memory: " + memory.js.used + "/" + memory.js.total);
212 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "fitbit": {
3 | "appUUID": "d84e4e76-0ded-40f2-98f7-f4de4dc27fc4",
4 | "appType": "app",
5 | "appDisplayName": "boilerplate",
6 | "iconFile": "resources/icon.png",
7 | "wipeColor": "#607d8b",
8 | "requestedPermissions": [],
9 | "i18n": {
10 | "en": {
11 | "name": "boilerplate"
12 | }
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/resources/index.gui:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/screen1.gui:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/screen2.gui:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/styles.css:
--------------------------------------------------------------------------------
1 | text {
2 | font-size: 32;
3 | font-family: System-Regular;
4 | font-weight: regular;
5 | text-length: 32;
6 | fill: white;
7 | }
8 |
--------------------------------------------------------------------------------
/resources/widgets.gui:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------