[TIMOB-12551] JavaScript-to-JavaScript optimizing compiler
GitHub Issue | n/a |
Type | Story |
Priority | Low |
Status | Closed |
Resolution | Won't Fix |
Resolution Date | 2017-07-19T17:34:48.000+0000 |
Affected Version/s | n/a |
Fix Version/s | n/a |
Components | Android, CLI, Code Processor, Core, iOS |
Labels | andoid, ast_transformation, core, ios, javascript, javascriptcore, performance, source_maps |
Reporter | Matt Langston |
Assignee | Chris Barber |
Created | 2013-02-01T18:47:21.000+0000 |
Updated | 2018-08-02T22:20:03.000+0000 |
Description
The performance of native bridge invocations shall be improved in Q1
for both iOS and Android. This will be done by creating an optimizing
javascript-to-javascript compiler that identifies non-performant
Titanium API use-cases and transforms them into performant native
bridge invocations.
The plan for completing this task is:
1. Automate collection of app performance data.
2. Identify Titanium performance bottlenecks from performance data.
3. Analyze javascript AST for common Titanium API idioms.
4. Create optimization algorithms (AST transformations) for these
idioms identified as performance bottlenecks.
From Jeff's README.md in his compiler prototype project:
Compiler Prototype
==================
The compiler prototype is a simple set of compile-time optimizations
that are applied to speed up native bridge invocations.
Concept
-------
The concept is very simple.
The Javascript native bridge binds a set of JavaScript objects (the
Titanium API) and eventually a method is bound into a callback
implementation in native land. The JS engine performs native bridge
calls to lookup the JavaScript object and eventually invoke the bound
native function in this simple example:
> Ti.API.log('foo')
The JS engine makes the following invocations underneath the cover:
> Ti => Object
> Object.API => Object
> Object.log => Function
> Function('foo')
So, a simple one-liner requires 4 invocations in the JS engine.
Additionally, in this example, foo is an argument that must be
marshalled from a JS native object into a native object. In
Objective-C, this turns a JSValueRef (or JSStringRef) into a
NSString. This happens before the underlying JSFunction is mapped into
an Objective-C method. To make matters worse, in Objective-C, we have
to do additional lookups to find the appropriate TiModule and then
build an NSInvocation to invoke the method against the Objective-C
class. All of this takes time.
This prototype attempts to speed up the invocation above by performing
a few simple steps.
First, we avoid the Object lookups by turns a Titanium API into a
bound symbol in the JS engine. So, Ti.API.log become T$1 (as an
example).
Second, we attempt to speed up static arguments that are required to
be marshalled during invocation into a symbol table which are only
marshalled once. The symbol table is pre-compiled and subsequent
references of the same symbol will use a pre-marshalled copy.
Third, we pre-bind a compiled method and set of pre-marshalled
arguments into a jump table. This is similar to using a function
pointer (but as an instance of a compiled pointer to a module and
invocation).
The result is that you can get 50-75% pure invocation speed ups. For
methods that have static arguments, you can achieve even better
results.
Limitations
-----------
The current prototype uses a titanium module and requires a compile
method to be called at the top of each compiled file to load up the
symbols and bind the symbols. This has convenience in that we can do
it without having to make any changes to Titanium itself. However, we
may want to consider adding the compile method into Titanium
(unpublished) to make maintenance easier and eliminate the need for a
module.
The second limitation is that we are speeding up only function calls
and a few global symbols (such as Ti.UI.FILL). Eventually, we may
want to do the same to properties.
Third, the prototype is using a compiler hook (good) but only is
working with Alloy. This is not a limitation per se, but i didn't
want to have to deal with figuring out how to redirect the compiled JS
files into a separate temporary directory and causing XCode to point
at that instead of Resources -- since we don't want to override
Resources (except in Alloy, that's how it operates, so that's OK).
Fourth, I'm using a uglify-based compiler by hand instead of using
Bryan's titanium code processor. The current implementation of the
code processor doesn't allow you to mutate code in his plugins (or in
the processor itself). Ideally, this would actually be a code
processor plugin that would simply mutate code in a pipeline during
compilation.
Fifth, the current prototype is iOS only. This is because I'm most
familiar with iOS and don't know the Android compiler / bridge for V8
as well as the old version. However, I think this same concept works
for Android conceptually.
Six, since I'm not using the code processor, I'm not using the JSCA
file to understand the API. Moving to the code processor eliminates
that issue.
The last overall limitation is that we might want to actually
pre-compile out the jump tables and use actually compiled function
callbacks instead of the TiCompiledMethod object. If we moved to more
of a true compiled / generated code, that would be rather trivial.
This would likely give us even more speedup and much less object
garbage and much lower memory. You would simply generate the C
function that would directly call the module and method instead of
having to have a generic TiCompiledMethod and you could by pass the
entire Kroll overhead. I believe the new V8 compiled bindings do
something similar in Android.
Components
----------
CLI Hook
The CLI hook file is under plugins/ti.compiler/hooks and in the file
ti.compiler.js
Module Source
The module source for iOS is under compiler_module.
Notes
-----
The ti.compiler plugin must come after the ti.alloy in tiapp.xml.
Afterthought
------------
I think this concept can be dramatically further expanded to really
build a compiler that can pre-process code and optimize, compile and
generally make performance a lot faster than it does today.
Collapsing properties
For example, take the following code:
var v = Ti.UI.createView();
v.width = Ti.UI.FILL;
v.height = Ti.UI.FILL;
v.backgroundColor = "red";
w.add(v);
This could easily be re-written to be much faster (prior to
optimizations from prototype):
w.add(Ti.UI.createView({width:Ti.UI.FILL,height:Ti.UI.FILL,backgroundColor:"red"}));
After optimizations further:
w.add(T$1());
Multiple invocations
Another example that is common, adding multiple objects to a view
heirarchy.
w.add(view1);
w.add(view2);
w.add(view3);
This could be optimized into:
w.add(view1,view2,view3);
In this example, passing 3 objects over a view to add to the window
will be MUCH faster because you go from 3 separate invocations to one
and more importantly, because this method is required to be invocked
on the UI Thread, 3 separate UI thread blocks.
Additionally, at some point, you could move the same optimized method
onto the native side such as it would turn into the following:
T$1();
And on the native side, it would simply do the work of adding the 3
views to a window by native references.
Subsequent code blocks
My ultimate belief is that most of the static code written in Titanium
JS can be pre-compiled and essentially reduced into few JS engine
functions.
Take the following static code block:
var w = Ti.UI.createWindow();
var v = Ti.UI.createView({width:TI.UI.FILL,height:TI.UI.FILL,backgroundColor:"white"});
var b = Ti.UI.createButton({
text:"Hello",
width:TI.UI.SIZE,
height:Ti.UI.SIZE
});
v.add(b);
w.add(v);
b.addEventListener(function(){
alert("hello world");
});
w.open();
This should be able to be reduced in JS code to:
var r$1 = T$1(), w = r$1[0], v = r$1[1], b = r$1[2];
b.addEventListener(function(){
alert("hello world");
});
w.open();
We should be able to take subsequent code blocks that are static and
collapse them into code blocks in native that can be mapped to one JS
symbol pointer.
String consts
We might be able to get much better optimizations in the JS engine by
turning static code strings into JS consts at compile time. In a ton
of cases in an app, you will use inline strings. Its generally faster
to declare them as a const and then use the const variable name
instead. This could be a simple optimizations done by the compiler.
Dead code removal
We need to remove dead code or unused variables and functions. For
example, in Alloy in the alloy.js file, we define a function named
isTabletFallback. This method isn't used. We could simply optimize
on compile and deterine which functions / symbols are reachable and
then remove them if not.
For example, Alloy generates the following:
var Alloy = require("alloy"), _ = Alloy._, Backbone = Alloy.Backbone;
Alloy.createController("index");
This is useful is you need to reference Backbone or underscore
libraries in the app.js stub. However, in almost all scenarios, the
developer never does this in a standard Alloy app.
The code above could be easily reduced to the following if it's not
modified:
require('alloy').createController('index')
This creates no variables in memory that have to be later garbage
collected and is much more efficient in execution.
- Jeff
Attachments
Come up to speed on Titanium app development. Collected initial performance data from NBC app running on iOS device.
Project Requirements: 1. An optimizing JavaScript compiler will be created that reduces bridge traffic between the JavaScrtipt interpreter and the native platform. 2. Initial optimizations will include: 2.1 namespace-folding for native Titanium object (to eliminate repeated key/value lookups across the bridge): var message = 'hello'; Ti.API.log(foo) becomes T$1(foo) (as an example) 2.2 pre-bind-methods-and-arguments Titanium JavaScript functions with constant arguments will have their arguments pre-bound on the native side of the bridge. For example, Ti.API.log('foo') becomes T$2(). The JavaScript constants (e.g. strings, numbers and booleans) that are passed to these native functions will be marshaled across the bridge only once and will remain cached for subsequent lookups in a symbol table. This will avoid repeated type conversions (e.g. JSStringRef <-> NSString, JSValueRef <-> NSNumber, etc.). 2.3 property-aggregation (performed prior to namespace folding): var v = Ti.UI.createView(); v.width = Ti.UI.FILL; v.height = Ti.UI.FILL; v.backgroundColor = "red"; w.add(v); is transformed to w.add(Ti.UI.createView({width:Ti.UI.FILL,height:Ti.UI.FILL,backgroundColor:"red"})); 2.4 view-aggregation Adding multiple objects to a view hierarchy like this: w.add(view1); w.add(view2); w.add(view3); will be transformed to this: w.add(view1,view2,view3); 3. The optimizer will be optional and selectively enabled for specific customers. 4. A test suite will guarantee that the transformed javascript will be remain functionally invariant to the original javascript. 5. The collection of bridge performance data will be automated so that performance improvements are demonstrable and to allow for the identification of regressions. 6. The design will allow for the compiler to target all Titanium platforms, but the initial implementation will only target the iOS platform. Proposed Architectural Components: 1. Modular AST Processing Pipeline 1.1. This will allow for additional optimizations to be added over time. 1.2 The design is based on tree grammars that allows for multiple passes over the AST. 1.3 There is a separate grammar/production for each optimization. 2. The native components will be written in C/C++ for platform portability. 3. The Test Suite will include multiple tests for each optimization.
Minutes from meeting planning meeting for optimizing javascript compiler Date: 2013.02.14 14:00 In Attendance: Max Stepanov Bryan Hughes, Blain Hamon, Allen Yeung, Vishal Duggal, Josh Roesslein - Adding a pure js wrapper would benefit all platforms, but is a longer term project and not feasible in next 6-8 weeks. - Security is a concern during the pre-compile since js constant strings could contain sensitive data (e.g. passwords,etc.) - Property aggregation should include applyProperties - Pre-binding doesn't require all arguments to be constants. The constant arguments can be pre-bound via the symbol table while dynamic arguments are passed as-is. - Crittercism concern since source code is changed. Disable for now (like magnification) until source maps are supported. - Provide different optimization levels instead of just off/on, e.g. -O, -O1, -O2, etc. Be prepared that aggressive optimization may change runtime behavior. - The code processor could be integrated to inform the optimizer, but this is a longer term project. For example, the cp could tell the optimizer about "var v = w" aliases. - Add a JIRA ticket for 1.4 above (i.e. view-aggregaton) for platform parity since this is currently only implemented on iOS.
Closing old "Won't fix" tickets. If you disagree, please reopen.