Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-12551] JavaScript-to-JavaScript optimizing compiler

GitHub Issuen/a
TypeStory
PriorityLow
StatusClosed
ResolutionWon't Fix
Resolution Date2017-07-19T17:34:48.000+0000
Affected Version/sn/a
Fix Version/sn/a
ComponentsAndroid, CLI, Code Processor, Core, iOS
Labelsandoid, ast_transformation, core, ios, javascript, javascriptcore, performance, source_maps
ReporterMatt Langston
AssigneeChris Barber
Created2013-02-01T18:47:21.000+0000
Updated2018-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

FileDateSize
testcompile.zip2013-02-01T18:47:21.000+00006032538

Comments

  1. Matt Langston 2013-02-01

    Come up to speed on Titanium app development. Collected initial performance data from NBC app running on iOS device.
  2. Matt Langston 2013-02-14

    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.
  3. Matt Langston 2013-02-15

    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.
  4. Eric Merriman 2018-08-02

    Closing old "Won't fix" tickets. If you disagree, please reopen.

JSON Source