-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Only serialize properties of objects which are accessed #169
Comments
Tempting to think that functions accessing a read-only object property can be optimized to access only that property even if another function accesses the object whole. However, that's not possible, as the function accessing the object whole could use const O = Object,
d = O['define' + 'Property'];
function write(o, n, v) {
d( o, n, { value: v } );
}
const obj = { x: 1, y: 2 };
export default {
getX() {
return obj.x;
},
setX(v) {
write(obj, 'x', v);
}
};
You can optimize |
Detecting read-only properties will also need to spot assignment via deconstruction e.g.: ( { a: obj.x } = { a: 123 } );
[ obj.x ] = [ 123 ];
[ { a: { b: obj.x } } ] = [ { a: { b: 123 } } ];
( { ...obj.x } = {} );
[ ...obj.x ] = []; |
Another optimization killer: If object can be accessed via function fn(obj, obj2) {
return {
getX: () => obj.x,
deleteProp: n => delete arguments[n].x
};
}
export default fn( { x: 1 }, { x: 2 } ); It's not possible to optimize access to If NB In sloppy mode, |
Concerning when to serialize function scope vars:
The problem with (2) is determining circularity. This is quite simple for the object itself (which is the value of the external var). Could just check if a record exists for the value and, if so, whether it has a However, the problem is where the object has properties which are circular. e.g.: let obj = { x: 1 };
const methods = {
getX: () => obj.x
};
obj.methods = methods;
export default {
methods,
setObj: v => obj = v
};
When When Solutions1. Record stackAt the time This would require keeping a record of the stack at all times when serializing (currently no such facility exists) and a mechanism for swapping the stack for an older one when a deoptimization occurs. 2. Two-pass serializationSerialization could be performed in 2 phases:
Currently these two phases occur together in one pass. Functions would initially be traced on assumption object property accesses can be fully optimized. After phase 1 is complete, all the information necessary to decide which functions need to be deoptimized has been gathered. Any scope vars which need to be deoptimized can then be traced. It's only during the 2nd phase - as JS code is written out - that it's determined where there are circular references. This would be a major rewrite - the way Livepack is written at present doesn't support this at all. Every serialization function would need to be split into two - a However, it would have some other advantages: Once collapsing top-level scopes into external vars is implemented (#81), I think it will allow avoiding unnecessary late assignments, as things that would have been circular references cease to be so. In example above, whether i.e. output could be either: // getX defined inline
const methods = {
getX: () => obj.x
};
let obj = { x: 1, methods };
export default {
methods,
setObj: v => obj = v
}; // getX defined with scope function
let obj = { x: 1 };
const methods = {
getX: (obj => () => obj.x)(obj)
};
// This extra assignment is required, as `obj` must be defined before `methods` due to its use above
obj.methods = methods;
export default {
methods,
setObj: v => obj = v
}; It may be possible to still achieve this without the change to 2 passes, as long as However, I think this case makes that impossible, as it's only when function create(obj) {
const methods = {
getX: () => obj.x
};
obj.methods = methods;
return {
methods,
setObj: v => obj = v
};
};
const obj = { x: 1 };
export default create( obj );
obj.getX2 = create( {} ).methods.getX; Other advantages:
I also thought it would help select the most appropriate optimization (optimization 2 or 3 in top post above) to apply depending on what code-splitting needs are. Currently this is not known at serialization time, but actually it'd be possible even without this change as scopes are created after all dependencies are traced anyway. |
Two-phase serialization now has its own issue: #426 |
Input:
Current output:
As
x
is only property ofobj
which is accessed, this could be reduced to:Optimizations
The following optimizations can be applied:
1. Omit unused properties
Where only certain properties of an object are accessed, any other properties can be omitted:
Note property
z
has been discarded in output.2. Break object properties apart with scopes
Where object is never used as a whole (only individual properties accessed by name), each property can be split into a separate scope var.
getX()
+setX()
can be code-split into a separate file fromgetY()
+setY()
.3. Break object properties apart with object wrappers
Where object is never used as a whole (only individual properties accessed by name), each property can be wrapped in a separate object.
Using 2 wrapper objects is slightly more verbose than output from optimizations (1) or (2), but more code-splittable than either.
getX()
,setX()
,getY()
andsetY()
could each be in separate files withobjX
andobjY
split into separate common files.4. Reduce to static values
Where a property is read only (never written to in any functions serialized), the property can be reduced to a static value.
This is completely code-splittable. It's more efficient than any of the other approaches above, but only works if
obj.x
andobj.y
are read-only.Optimization killers
None of these optimizations can be used if:
const objCopy = obj;
orfn( obj )
obj[ name ]
this
in a method call e.g.obj.getX()
and.getX()
usesthis
delete obj.x
) so a later access may fall through to object's prototypeeval()
has access to object in scope (no way to know ahead of time how the object will be used)Tempting to think could still apply optimization (3) in cases of undefined properties by defining object wrapper as
objX = Object.create( originalObjectPrototype )
. However, this won't work as it's possible object's prototype is altered later withObject.setPrototypeOf()
.It's impossible to accurately detect any changes made to the object with
Object.defineProperty()
- which could change property values, or change properties to getters/setters. However, this isn't a problem - the call toObject.defineProperty( obj )
would involve using the object standalone, and so would prevent optimization due to restriction (1) above.ESM
These optimizations would also have effect of tree-shaking ESM (#53).
ESM is transpiled to CommonJS in Livepack's Babel plugin, prior to being run or serialized:
Consequently, when this function is serialized, the whole of the
_react
object is in scope and is serialized, whereas all we actually need is the.createElement
property.Optimization (4) (the most efficient one) would apply, except in case of
export let
where the value of the export can be changed dynamically in a function (pretty rare case).Difficulties
I can foresee several difficulties implementing this:
_react.createElement()
,createElement()
's code must be analysed to see if it usesthis
. Optimizations can only be used if it doesn't.The text was updated successfully, but these errors were encountered: