Hi,
By design, QML relies on dynamic scoping to resolve properties. For 
example, in the binding expression

foo + 1

"foo" can either be a property of the inner-most QML item, or somewhere 
up the item hierarchy, or the context, or ultimately in the Global Object.

The QML scoping is not a use case JS engines are optimized for.
The objective is to find a way to obey the QML scoping rules in JS, 
while still achieving good performance.
(Let it be said that I'm not an expert on those scoping rules, though.)

THE "WITH" WAY

The naive way to implement the scoping in JS is to have a bunch of with 
statements to put the items in the hierarchy on the scope:

with ($item) {
   with ($child) {
       (function() { return foo + 1; })() // this is the anonymous 
function QML generates for the binding expression "foo + 1"
   }
}

In order for this to have any chance of being fast upon repeated 
evaluation, "foo" should be resolved to a particular object the first 
time, and the JS engine should cache that object and reuse it the next 
time if possible. The cache will be valid iff none of the objects in the 
scope (and their prototypes) have changed class; this is the case for 
QML, since QML objects are "static" (can't gain new properties, can't 
have properties removed).

The optimization could be even more aggressive on the JS engine side by 
specializing for "sealed" objects. The ES5 function Object.seal() is 
used to prevent properties from being added to or removed from an 
object. This is precisely the case with QML items. When all the objects 
(and their prototypes) in the scope are sealed, the lookup of "foo" 
could be translated to a direct lookup in scope object N, without even 
any subsequent need to check the validity of that.

A simple test with V8 bleeding_edge r7355:

o = {};
for (var i = 0; i < 1000000; ++i) {
   with (o) // uncomment to get optimal performance
     Math.sin(i);
}

On my machine, simply having that with-statement there increases the 
runtime by 8x, even though we know o (pretend it's a QML item) will 
never have a "Math" property.


THE "WITHOUT" WAY

The problem with "with" is that it's effectively deprecated. It's not 
even allowed in ES5 strict mode. Therefore it seems wrong for us to be 
pushing in that direction.

The alternative, then, if we still insist on ultimately doing the 
resolution lazily on the JS side, is to present a flat (but 
"intercepted"-by-us) scope to the JS implementation, and handle the 
hierarchical resolution of properties in an interceptor.

An idea for that is to add e.g. an overload of v8::Script::Run() that 
takes the (global) object to use as the scope when evaluating a binding.
Each QML item would share the scope object for all its bindings.
The scope object would have to be invalidated upon item reparenting, or 
if a new context object is set (QDeclarativeContext::setContextObject()).

Apart from the challenge of implementing such a specialized Run() 
(and/or the cost of swapping the global object), there's another issue 
with this approach. To perform the resolution lazily, we would have to 
use v8 interceptors. v8 interceptors always override per-property 
accessors on the instance, so it's not possible to lazily install a 
(fast) accessor for the particular property, which would then be called 
directly on subsequent accesses. As the V8 docs say, an accessor will 
only be used if the interceptor returns an empty handle.

We want to avoid using interceptors on every property access, because 
associate mapping of v8::String is slow (compared to QtScript/JSC-based 
QML, where it's a pointer comparison).

There are problems with having the interceptor in the prototype chain 
(Object.prototype.hasOwnProperty() returning false for lazily resolved 
properties; property writes not intercepted), so let's not go there.

If the interceptor on the instance was only called as a fallback 
(_after_ accessors), that would essentially solve it for us. But I don't 
know how feasible it is to change this behavior upstream. We could give 
it a shot, though? How about proposing a (global) flag that changes the 
behavior for all interceptors?


THE "JUST DON'T DO IT IN JAVASCRIPT" WAY

The final option, which we actually discussed at the workshop, is to 
avoid having free variables (property references without a "someObject." 
prefix) in the binding expressions (as passed to JS) in the first place.

This means that QML would analyze the original binding expression at 
first evaluation time, resolve the property accesses to real objects in 
the hierarchy, and rewrite the expression to reference the properties 
directly on the object(s).
As with the custom-scope-object option, the bindings would have to be 
invalidated under certain conditions (reparenting etc.).

Since we generate anonymous functions for the binding expressions, the 
particular objects where properties are to be found can be passed as 
arguments to that function. Example:

Original input: foo + bar
Upon analysis, QML finds that "foo" exists on item2, and "bar" on item1 
(an ancestor of item2).
Rewritten expression: $1.foo + $2.bar
Final anonymous function: function($1, $2) { return $1.foo + $2.bar; }

And QML would call the function with the proper objects as arguments.
For these "hidden" arguments, it would be nice to use the internal 
"extended" JS syntax of V8, so that e.g. %1 and %2 can be used as 
identifiers (to avoid any clash with user-introduced variables).

The end result is that no magic scoping lookup is done on the JS side, 
all the JS property access is done through accessors (yay), and V8 can 
do what it's good at (hidden class optimization to ensure subsequent 
evaluations of the binding are fast).

However ... . . . There may be some cases where the rewritten expression 
would become invalidated mid-way through execution of the JS function 
itself. E.g., what happens if the QML input accesses "foo" (which would 
be resolved in _some ancestor_), does a reparenting, and accesses "foo" 
again from the same expression/function (where "foo" should now be 
resolved in a different object)? With the rewriting approach, the object 
reference would be stale (since the already-invalidated binding is still 
in the middle of execution).

Are the current semantics of QML well-defined in this case?
For sure, we can always rewrite property accesses that are on the object 
itself ("%0.foo", including "%0.parent"), maybe that covers many (most?) 
real-world cases.

There are for sure some other things I forgot in this analysis, like how 
to deal with ID-based lookup (where the identifier doesn't reference a 
property of an object in the scope chain, but rather an item).


SUMMARY

Potential V8 things to look into:
- Adding flag for making interceptors real fallbacks (we really, truly 
need it we want/need to do efficient lazy lookup on the JS side)
- Making it possible to use the "extended" JS syntax through the public 
API (%-prefixed identifiers)
- General-purpose optimization of with-statement (cache class of objects 
in scope chain)
- Aggressive optimization of with-statement (all objects in scope are 
sealed -- very QML-specific)
- Implementing overload of Script::Run() that takes custom object to use 
as global object

Potential QML/JS things to look into:
- Get a clear picture of the exact scoping rules we need to obey 
(autotests to the rescue?)
- Expression/function rewriting

Regards,
Kent
_______________________________________________
Qt-script mailing list
[email protected]
http://lists.qt.nokia.com/mailman/listinfo/qt-script

Reply via email to