author | Felipe Gomes <felipc@gmail.com> |
Tue, 15 May 2018 19:49:17 -0300 | |
changeset 418453 | 62451c9687b29739b90830ce876c21f158298247 |
parent 418452 | d813fae62728c6edf2dbcf7ec4e7c0c3e888a6f9 |
child 418454 | 84ec88aaa796b4b86bede156bf7f43fe82870955 |
push id | 34001 |
push user | ebalazs@mozilla.com |
push date | Wed, 16 May 2018 10:01:23 +0000 |
treeherder | mozilla-central@3c9d69736f4a [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | kmag |
bugs | 1457988 |
milestone | 62.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/js/xpconnect/loader/XPCOMUtils.jsm +++ b/js/xpconnect/loader/XPCOMUtils.jsm @@ -552,18 +552,198 @@ var XPCOMUtils = { */ defineConstant: function XPCOMUtils__defineConstant(aObj, aName, aValue) { Object.defineProperty(aObj, aName, { value: aValue, enumerable: true, writable: false }); }, + + /** + * Defines a proxy which acts as a lazy object getter that can be passed + * around as a reference, and will only be evaluated when something in + * that object gets accessed. + * + * The evaluation can be triggered by a function call, by getting or + * setting a property, calling this as a constructor, or enumerating + * the properties of this object (e.g. during an iteration). + * + * Please note that, even after evaluated, the object given to you + * remains being the proxy object (which forwards everything to the + * real object). This is important to correctly use these objects + * in pairs of add+remove listeners, for example. + * If your use case requires access to the direct object, you can + * get it through the untrap callback. + * + * @param aObject + * The object to define the lazy getter on. + * + * You can pass null to aObject if you just want to get this + * proxy through the return value. + * + * @param aName + * The name of the getter to define on aObject. + * + * @param aInitFuncOrResource + * A function or a module that defines what this object actually + * should be when it gets evaluated. This will only ever be called once. + * + * Short-hand: If you pass a string to this parameter, it will be treated + * as the URI of a module to be imported, and aName will be used as + * the symbol to retrieve from the module. + * + * @param aStubProperties + * In this parameter, you can provide an object which contains + * properties from the original object that, when accessed, will still + * prevent the entire object from being evaluated. + * + * These can be copies or simplified versions of the original properties. + * + * One example is to provide an alternative QueryInterface implementation + * to avoid the entire object from being evaluated when it's added as an + * observer (as addObserver calls object.QueryInterface(Ci.nsIObserver)). + * + * Once the object has been evaluated, the properties from the real + * object will be used instead of the ones provided here. + * + * @param aUntrapCallback + * A function that gets called once when the object has just been evaluated. + * You can use this to do some work (e.g. setting properties) that you need + * to do on this object but that can wait until it gets evaluated. + * + * Another use case for this is to use during code development to log when + * this object gets evaluated, to make sure you're not accidentally triggering + * it earlier than expected. + */ + defineLazyProxy: function XPCOMUtils__defineLazyProxy(aObject, aName, aInitFuncOrResource, + aStubProperties, aUntrapCallback) { + let initFunc = aInitFuncOrResource; + + if (typeof(aInitFuncOrResource) == "string") { + initFunc = function () { + let tmp = {}; + ChromeUtils.import(aInitFuncOrResource, tmp); + return tmp[aName]; + }; + } + + let handler = new LazyProxyHandler(aName, initFunc, + aStubProperties, aUntrapCallback); + + /* + * We cannot simply create a lazy getter for the underlying + * object and pass it as the target of the proxy, because + * just passing it in `new Proxy` means it would get + * evaluated. Becase of this, a full handler needs to be + * implemented (the LazyProxyHandler). + * + * So, an empty object is used as the target, and the handler + * replaces it on every call with the real object. + */ + let proxy = new Proxy({}, handler); + + if (aObject) { + Object.defineProperty(aObject, aName, { + value: proxy, + enumerable: true, + writable: true, + }); + } + + return proxy; + }, }; +/** + * LazyProxyHandler + * This class implements the handler used + * in the proxy from defineLazyProxy. + * + * This handler forwards all calls to an underlying object, + * stored as `this.realObject`, which is obtained as the returned + * value from aInitFunc, which will be called on the first time + * time that it needs to be used (with an exception in the get() trap + * for the properties provided in the `aStubProperties` parameter). + */ + +class LazyProxyHandler { + constructor(aName, aInitFunc, aStubProperties, aUntrapCallback) { + this.pending = true; + this.name = aName; + this.initFuncOrResource = aInitFunc; + this.stubProperties = aStubProperties; + this.untrapCallback = aUntrapCallback; + } + + getObject() { + if (this.pending) { + this.realObject = this.initFuncOrResource.call(null); + + if (this.untrapCallback) { + this.untrapCallback.call(null, this.realObject); + this.untrapCallback = null; + } + + this.pending = false; + this.stubProperties = null; + } + return this.realObject; + } + + getPrototypeOf(target) { + return Reflect.getPrototypeOf(this.getObject()); + } + + setPrototypeOf(target, prototype) { + return Reflect.setPrototypeOf(this.getObject(), prototype); + } + + isExtensible(target) { + return Reflect.isExtensible(this.getObject()); + } + + preventExtensions(target) { + return Reflect.preventExtensions(this.getObject()); + } + + getOwnPropertyDescriptor(target, prop) { + return Reflect.getOwnPropertyDescriptor(this.getObject(), prop); + } + + defineProperty(target, prop, descriptor) { + return Reflect.defineProperty(this.getObject(), prop, descriptor); + } + + has(target, prop) { + return Reflect.has(this.getObject(), prop); + } + + get(target, prop, receiver) { + if (this.pending && + this.stubProperties && + Object.prototype.hasOwnProperty.call(this.stubProperties, prop)) { + return this.stubProperties[prop]; + } + return Reflect.get(this.getObject(), prop, receiver); + } + + set(target, prop, value, receiver) { + return Reflect.set(this.getObject(), prop, value, receiver); + } + + deleteProperty(target, prop) { + return Reflect.deleteProperty(this.getObject(), prop); + } + + ownKeys(target) { + return Reflect.ownKeys(this.getObject()); + } +} + var XPCU_lazyPreferenceObserverQI = ChromeUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]); ChromeUtils.defineModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyServiceGetter(XPCOMUtils, "categoryManager", "@mozilla.org/categorymanager;1", "nsICategoryManager");
new file mode 100644 --- /dev/null +++ b/js/xpconnect/tests/unit/test_lazyproxy.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests the method defineLazyProxy from XPCOMUtils.jsm. + */ + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +add_task(function test_lazy_proxy() { + let tmp = {}; + let realObject = { + "prop1": "value1", + "prop2": "value2", + }; + + let evaluated = false; + let untrapCalled = false; + + let lazyProxy = XPCOMUtils.defineLazyProxy( + tmp, + "myLazyProxy", + + // Initiliazer function + function init() { + evaluated = true; + return realObject; + }, + + // Stub properties + { + "prop1": "stub" + }, + + // Untrap callback + function untrapCallback(obj) { + Assert.equal(obj, realObject, "The underlying object can be obtained in the untrap callback"); + untrapCalled = true; + } + ); + + // Check that the proxy returned and the one + // defined in tmp are the same. + // + // Note: Assert.strictEqual can't be used here + // because it wants to stringify the two objects + // compared, which defeats the lazy proxy. + Assert.ok(lazyProxy === tmp.myLazyProxy, "Return value and object defined are the same"); + + Assert.ok(Cu.isProxy(lazyProxy), "Returned value is in fact a proxy"); + + // Check that just using the proxy above didn't + // trigger the lazy getter evaluation. + Assert.ok(!evaluated, "The lazy proxy hasn't been evaluated yet"); + Assert.ok(!untrapCalled, "The untrap callback hasn't been called yet"); + + // Accessing a stubbed property returns the stub + // value and doesn't trigger evaluation. + Assert.equal(lazyProxy.prop1, "stub", "Accessing a stubbed property returns the stubbed value"); + + Assert.ok(!evaluated, "The access to the stubbed property above didn't evaluate the lazy proxy"); + Assert.ok(!untrapCalled, "The untrap callback hasn't been called yet"); + + // Now the access to another property will trigger + // the evaluation, as expected. + Assert.equal(lazyProxy.prop2, "value2", "Property access is correctly forwarded to the underlying object"); + + Assert.ok(evaluated, "Accessing a non-stubbed property triggered the proxy evaluation"); + Assert.ok(untrapCalled, "The untrap callback was called"); + + // The value of prop1 is now the real value and not the stub value. + Assert.equal(lazyProxy.prop1, "value1", "The value of prop1 is now the real value and not the stub one"); +}); + +add_task(function test_module_version() { + // Test that passing a string instead of an initialization function + // makes this behave like a lazy module getter. + const NET_UTIL_URI = "resource://gre/modules/NetUtil.jsm"; + let underlyingObject; + + Cu.unload(NET_UTIL_URI); + + let lazyProxy = XPCOMUtils.defineLazyProxy( + null, + "NetUtil", + NET_UTIL_URI, + null, /* no stubs */ + function untrapCallback(object) { + underlyingObject = object; + } + ); + + Assert.ok(!Cu.isModuleLoaded(NET_UTIL_URI), "The NetUtil module was not loaded by the lazy proxy definition"); + + // Access the object, which will evaluate the proxy. + lazyProxy.foo = "bar"; + + // Module was loaded. + Assert.ok(Cu.isModuleLoaded(NET_UTIL_URI), "The NetUtil module was loaded"); + + let { NetUtil } = ChromeUtils.import(NET_UTIL_URI, {}); + + // Avoids a gigantic stringification in the logs. + Assert.ok(NetUtil === underlyingObject, "The module loaded is the same as the one directly obtained by ChromeUtils.import"); + + // Proxy correctly passed the setter to the underlying object. + Assert.equal(NetUtil.foo, "bar", "Proxy correctly passed the setter to the underlying object"); + + delete lazyProxy.foo; + + // Proxy correctly passed the delete operation to the underlying object. + Assert.ok(!NetUtil.hasOwnProperty("foo"), "Proxy correctly passed the delete operation to the underlying object"); +});
--- a/js/xpconnect/tests/unit/xpcshell.ini +++ b/js/xpconnect/tests/unit/xpcshell.ini @@ -88,16 +88,17 @@ head = head_ongc.js head = head_ongc.js [test_onGarbageCollection-05.js] head = head_ongc.js [test_reflect_parse.js] [test_localeCompare.js] [test_recursive_import.js] [test_xpcomutils.js] [test_unload.js] +[test_lazyproxy.js] [test_attributes.js] [test_params.js] [test_tearoffs.js] [test_want_components.js] [test_components.js] [test_allowedDomains.js] [test_allowedDomainsXHR.js] [test_nuke_sandbox.js]