diff options
Diffstat (limited to 'chromium/v8/src/object-observe.js')
-rw-r--r-- | chromium/v8/src/object-observe.js | 437 |
1 files changed, 274 insertions, 163 deletions
diff --git a/chromium/v8/src/object-observe.js b/chromium/v8/src/object-observe.js index a5c12bf0098..1035792e8b6 100644 --- a/chromium/v8/src/object-observe.js +++ b/chromium/v8/src/object-observe.js @@ -27,12 +27,41 @@ "use strict"; +// Overview: +// +// This file contains all of the routing and accounting for Object.observe. +// User code will interact with these mechanisms via the Object.observe APIs +// and, as a side effect of mutation objects which are observed. The V8 runtime +// (both C++ and JS) will interact with these mechanisms primarily by enqueuing +// proper change records for objects which were mutated. The Object.observe +// routing and accounting consists primarily of three participants +// +// 1) ObjectInfo. This represents the observed state of a given object. It +// records what callbacks are observing the object, with what options, and +// what "change types" are in progress on the object (i.e. via +// notifier.performChange). +// +// 2) CallbackInfo. This represents a callback used for observation. It holds +// the records which must be delivered to the callback, as well as the global +// priority of the callback (which determines delivery order between +// callbacks). +// +// 3) observationState.pendingObservers. This is the set of observers which +// have change records which must be delivered. During "normal" delivery +// (i.e. not Object.deliverChangeRecords), this is the mechanism by which +// callbacks are invoked in the proper order until there are no more +// change records pending to a callback. +// +// Note that in order to reduce allocation and processing costs, the +// implementation of (1) and (2) have "optimized" states which represent +// common cases which can be handled more efficiently. + var observationState = %GetObservationState(); if (IS_UNDEFINED(observationState.callbackInfoMap)) { observationState.callbackInfoMap = %ObservationWeakMapCreate(); observationState.objectInfoMap = %ObservationWeakMapCreate(); - observationState.notifierTargetMap = %ObservationWeakMapCreate(); - observationState.pendingObservers = new InternalArray; + observationState.notifierObjectInfoMap = %ObservationWeakMapCreate(); + observationState.pendingObservers = null; observationState.nextCallbackPriority = 0; } @@ -59,126 +88,191 @@ ObservationWeakMap.prototype = { var callbackInfoMap = new ObservationWeakMap(observationState.callbackInfoMap); var objectInfoMap = new ObservationWeakMap(observationState.objectInfoMap); -var notifierTargetMap = - new ObservationWeakMap(observationState.notifierTargetMap); - -function CreateObjectInfo(object) { - var info = { - changeObservers: new InternalArray, - notifier: null, - inactiveObservers: new InternalArray, - performing: { __proto__: null }, - performingCount: 0, - }; - objectInfoMap.set(object, info); - return info; +var notifierObjectInfoMap = + new ObservationWeakMap(observationState.notifierObjectInfoMap); + +function TypeMapCreate() { + return { __proto__: null }; } -var defaultAcceptTypes = { - __proto__: null, - 'new': true, - 'updated': true, - 'deleted': true, - 'prototype': true, - 'reconfigured': true -}; +function TypeMapAddType(typeMap, type, ignoreDuplicate) { + typeMap[type] = ignoreDuplicate ? 1 : (typeMap[type] || 0) + 1; +} -function CreateObserver(callback, accept) { - var observer = { +function TypeMapRemoveType(typeMap, type) { + typeMap[type]--; +} + +function TypeMapCreateFromList(typeList) { + var typeMap = TypeMapCreate(); + for (var i = 0; i < typeList.length; i++) { + TypeMapAddType(typeMap, typeList[i], true); + } + return typeMap; +} + +function TypeMapHasType(typeMap, type) { + return !!typeMap[type]; +} + +function TypeMapIsDisjointFrom(typeMap1, typeMap2) { + if (!typeMap1 || !typeMap2) + return true; + + for (var type in typeMap1) { + if (TypeMapHasType(typeMap1, type) && TypeMapHasType(typeMap2, type)) + return false; + } + + return true; +} + +var defaultAcceptTypes = TypeMapCreateFromList([ + 'new', + 'updated', + 'deleted', + 'prototype', + 'reconfigured' +]); + +// An Observer is a registration to observe an object by a callback with +// a given set of accept types. If the set of accept types is the default +// set for Object.observe, the observer is represented as a direct reference +// to the callback. An observer never changes its accept types and thus never +// needs to "normalize". +function ObserverCreate(callback, acceptList) { + return IS_UNDEFINED(acceptList) ? callback : { __proto__: null, callback: callback, - accept: defaultAcceptTypes + accept: TypeMapCreateFromList(acceptList) }; +} - if (IS_UNDEFINED(accept)) - return observer; - - var acceptMap = { __proto__: null }; - for (var i = 0; i < accept.length; i++) - acceptMap[accept[i]] = true; +function ObserverGetCallback(observer) { + return IS_SPEC_FUNCTION(observer) ? observer : observer.callback; +} - observer.accept = acceptMap; - return observer; +function ObserverGetAcceptTypes(observer) { + return IS_SPEC_FUNCTION(observer) ? defaultAcceptTypes : observer.accept; } function ObserverIsActive(observer, objectInfo) { - if (objectInfo.performingCount === 0) - return true; + return TypeMapIsDisjointFrom(ObjectInfoGetPerformingTypes(objectInfo), + ObserverGetAcceptTypes(observer)); +} - var performing = objectInfo.performing; - for (var type in performing) { - if (performing[type] > 0 && observer.accept[type]) - return false; +function ObjectInfoGet(object) { + var objectInfo = objectInfoMap.get(object); + if (IS_UNDEFINED(objectInfo)) { + if (!%IsJSProxy(object)) + %SetIsObserved(object); + + objectInfo = { + object: object, + changeObservers: null, + notifier: null, + performing: null, + performingCount: 0, + }; + objectInfoMap.set(object, objectInfo); } + return objectInfo; +} - return true; +function ObjectInfoGetFromNotifier(notifier) { + return notifierObjectInfoMap.get(notifier); +} + +function ObjectInfoGetNotifier(objectInfo) { + if (IS_NULL(objectInfo.notifier)) { + objectInfo.notifier = { __proto__: notifierPrototype }; + notifierObjectInfoMap.set(objectInfo.notifier, objectInfo); + } + + return objectInfo.notifier; +} + +function ObjectInfoGetObject(objectInfo) { + return objectInfo.object; } -function ObserverIsInactive(observer, objectInfo) { - return !ObserverIsActive(observer, objectInfo); +function ChangeObserversIsOptimized(changeObservers) { + return typeof changeObservers === 'function' || + typeof changeObservers.callback === 'function'; } -function RemoveNullElements(from) { - var i = 0; - var j = 0; - for (; i < from.length; i++) { - if (from[i] === null) - continue; - if (j < i) - from[j] = from[i]; - j++; +// The set of observers on an object is called 'changeObservers'. The first +// observer is referenced directly via objectInfo.changeObservers. When a second +// is added, changeObservers "normalizes" to become a mapping of callback +// priority -> observer and is then stored on objectInfo.changeObservers. +function ObjectInfoNormalizeChangeObservers(objectInfo) { + if (ChangeObserversIsOptimized(objectInfo.changeObservers)) { + var observer = objectInfo.changeObservers; + var callback = ObserverGetCallback(observer); + var callbackInfo = CallbackInfoGet(callback); + var priority = CallbackInfoGetPriority(callbackInfo); + objectInfo.changeObservers = { __proto__: null }; + objectInfo.changeObservers[priority] = observer; } +} + +function ObjectInfoAddObserver(objectInfo, callback, acceptList) { + var callbackInfo = CallbackInfoGetOrCreate(callback); + var observer = ObserverCreate(callback, acceptList); - if (i !== j) - from.length = from.length - (i - j); + if (!objectInfo.changeObservers) { + objectInfo.changeObservers = observer; + return; + } + + ObjectInfoNormalizeChangeObservers(objectInfo); + var priority = CallbackInfoGetPriority(callbackInfo); + objectInfo.changeObservers[priority] = observer; } -function RepartitionObservers(conditionFn, from, to, objectInfo) { - var anyRemoved = false; - for (var i = 0; i < from.length; i++) { - var observer = from[i]; - if (conditionFn(observer, objectInfo)) { - anyRemoved = true; - from[i] = null; - to.push(observer); - } +function ObjectInfoRemoveObserver(objectInfo, callback) { + if (!objectInfo.changeObservers) + return; + + if (ChangeObserversIsOptimized(objectInfo.changeObservers)) { + if (callback === ObserverGetCallback(objectInfo.changeObservers)) + objectInfo.changeObservers = null; + return; } - if (anyRemoved) - RemoveNullElements(from); + var callbackInfo = CallbackInfoGet(callback); + var priority = CallbackInfoGetPriority(callbackInfo); + delete objectInfo.changeObservers[priority]; } -function BeginPerformChange(objectInfo, type) { - objectInfo.performing[type] = (objectInfo.performing[type] || 0) + 1; +function ObjectInfoHasActiveObservers(objectInfo) { + if (IS_UNDEFINED(objectInfo) || !objectInfo.changeObservers) + return false; + + if (ChangeObserversIsOptimized(objectInfo.changeObservers)) + return ObserverIsActive(objectInfo.changeObservers, objectInfo); + + for (var priority in objectInfo.changeObservers) { + if (ObserverIsActive(objectInfo.changeObservers[priority], objectInfo)) + return true; + } + + return false; +} + +function ObjectInfoAddPerformingType(objectInfo, type) { + objectInfo.performing = objectInfo.performing || TypeMapCreate(); + TypeMapAddType(objectInfo.performing, type); objectInfo.performingCount++; - RepartitionObservers(ObserverIsInactive, - objectInfo.changeObservers, - objectInfo.inactiveObservers, - objectInfo); } -function EndPerformChange(objectInfo, type) { - objectInfo.performing[type]--; +function ObjectInfoRemovePerformingType(objectInfo, type) { objectInfo.performingCount--; - RepartitionObservers(ObserverIsActive, - objectInfo.inactiveObservers, - objectInfo.changeObservers, - objectInfo); -} - -function EnsureObserverRemoved(objectInfo, callback) { - function remove(observerList) { - for (var i = 0; i < observerList.length; i++) { - if (observerList[i].callback === callback) { - observerList.splice(i, 1); - return true; - } - } - return false; - } + TypeMapRemoveType(objectInfo.performing, type); +} - if (!remove(objectInfo.changeObservers)) - remove(objectInfo.inactiveObservers); +function ObjectInfoGetPerformingTypes(objectInfo) { + return objectInfo.performingCount > 0 ? objectInfo.performing : null; } function AcceptArgIsValid(arg) { @@ -198,12 +292,31 @@ function AcceptArgIsValid(arg) { return true; } -function EnsureCallbackPriority(callback) { - if (!callbackInfoMap.has(callback)) - callbackInfoMap.set(callback, observationState.nextCallbackPriority++); +// CallbackInfo's optimized state is just a number which represents its global +// priority. When a change record must be enqueued for the callback, it +// normalizes. When delivery clears any pending change records, it re-optimizes. +function CallbackInfoGet(callback) { + return callbackInfoMap.get(callback); +} + +function CallbackInfoGetOrCreate(callback) { + var callbackInfo = callbackInfoMap.get(callback); + if (!IS_UNDEFINED(callbackInfo)) + return callbackInfo; + + var priority = observationState.nextCallbackPriority++ + callbackInfoMap.set(callback, priority); + return priority; } -function NormalizeCallbackInfo(callback) { +function CallbackInfoGetPriority(callbackInfo) { + if (IS_NUMBER(callbackInfo)) + return callbackInfo; + else + return callbackInfo.priority; +} + +function CallbackInfoNormalize(callback) { var callbackInfo = callbackInfoMap.get(callback); if (IS_NUMBER(callbackInfo)) { var priority = callbackInfo; @@ -214,32 +327,18 @@ function NormalizeCallbackInfo(callback) { return callbackInfo; } -function ObjectObserve(object, callback, accept) { +function ObjectObserve(object, callback, acceptList) { if (!IS_SPEC_OBJECT(object)) throw MakeTypeError("observe_non_object", ["observe"]); if (!IS_SPEC_FUNCTION(callback)) throw MakeTypeError("observe_non_function", ["observe"]); if (ObjectIsFrozen(callback)) throw MakeTypeError("observe_callback_frozen"); - if (!AcceptArgIsValid(accept)) + if (!AcceptArgIsValid(acceptList)) throw MakeTypeError("observe_accept_invalid"); - EnsureCallbackPriority(callback); - - var objectInfo = objectInfoMap.get(object); - if (IS_UNDEFINED(objectInfo)) { - objectInfo = CreateObjectInfo(object); - %SetIsObserved(object); - } - - EnsureObserverRemoved(objectInfo, callback); - - var observer = CreateObserver(callback, accept); - if (ObserverIsActive(observer, objectInfo)) - objectInfo.changeObservers.push(observer); - else - objectInfo.inactiveObservers.push(observer); - + var objectInfo = ObjectInfoGet(object); + ObjectInfoAddObserver(objectInfo, callback, acceptList); return object; } @@ -253,7 +352,7 @@ function ObjectUnobserve(object, callback) { if (IS_UNDEFINED(objectInfo)) return object; - EnsureObserverRemoved(objectInfo, callback); + ObjectInfoRemoveObserver(objectInfo, callback); return object; } @@ -268,41 +367,67 @@ function ArrayUnobserve(object, callback) { return ObjectUnobserve(object, callback); } -function EnqueueToCallback(callback, changeRecord) { - var callbackInfo = NormalizeCallbackInfo(callback); +function ObserverEnqueueIfActive(observer, objectInfo, changeRecord, + needsAccessCheck) { + if (!ObserverIsActive(observer, objectInfo) || + !TypeMapHasType(ObserverGetAcceptTypes(observer), changeRecord.type)) { + return; + } + + var callback = ObserverGetCallback(observer); + if (needsAccessCheck && + // Drop all splice records on the floor for access-checked objects + (changeRecord.type == 'splice' || + !%IsAccessAllowedForObserver( + callback, changeRecord.object, changeRecord.name))) { + return; + } + + var callbackInfo = CallbackInfoNormalize(callback); + if (!observationState.pendingObservers) + observationState.pendingObservers = { __proto__: null }; observationState.pendingObservers[callbackInfo.priority] = callback; callbackInfo.push(changeRecord); %SetObserverDeliveryPending(); } -function EnqueueChangeRecord(changeRecord, observers) { +function ObjectInfoEnqueueChangeRecord(objectInfo, changeRecord, + skipAccessCheck) { // TODO(rossberg): adjust once there is a story for symbols vs proxies. if (IS_SYMBOL(changeRecord.name)) return; - for (var i = 0; i < observers.length; i++) { - var observer = observers[i]; - if (IS_UNDEFINED(observer.accept[changeRecord.type])) - continue; + var needsAccessCheck = !skipAccessCheck && + %IsAccessCheckNeeded(changeRecord.object); - EnqueueToCallback(observer.callback, changeRecord); + if (ChangeObserversIsOptimized(objectInfo.changeObservers)) { + var observer = objectInfo.changeObservers; + ObserverEnqueueIfActive(observer, objectInfo, changeRecord, + needsAccessCheck); + return; + } + + for (var priority in objectInfo.changeObservers) { + var observer = objectInfo.changeObservers[priority]; + ObserverEnqueueIfActive(observer, objectInfo, changeRecord, + needsAccessCheck); } } function BeginPerformSplice(array) { var objectInfo = objectInfoMap.get(array); if (!IS_UNDEFINED(objectInfo)) - BeginPerformChange(objectInfo, 'splice'); + ObjectInfoAddPerformingType(objectInfo, 'splice'); } function EndPerformSplice(array) { var objectInfo = objectInfoMap.get(array); if (!IS_UNDEFINED(objectInfo)) - EndPerformChange(objectInfo, 'splice'); + ObjectInfoRemovePerformingType(objectInfo, 'splice'); } function EnqueueSpliceRecord(array, index, removed, addedCount) { var objectInfo = objectInfoMap.get(array); - if (IS_UNDEFINED(objectInfo) || objectInfo.changeObservers.length === 0) + if (!ObjectInfoHasActiveObservers(objectInfo)) return; var changeRecord = { @@ -315,19 +440,19 @@ function EnqueueSpliceRecord(array, index, removed, addedCount) { ObjectFreeze(changeRecord); ObjectFreeze(changeRecord.removed); - EnqueueChangeRecord(changeRecord, objectInfo.changeObservers); + ObjectInfoEnqueueChangeRecord(objectInfo, changeRecord); } function NotifyChange(type, object, name, oldValue) { var objectInfo = objectInfoMap.get(object); - if (objectInfo.changeObservers.length === 0) + if (!ObjectInfoHasActiveObservers(objectInfo)) return; var changeRecord = (arguments.length < 4) ? { type: type, object: object, name: name } : { type: type, object: object, name: name, oldValue: oldValue }; ObjectFreeze(changeRecord); - EnqueueChangeRecord(changeRecord, objectInfo.changeObservers); + ObjectInfoEnqueueChangeRecord(objectInfo, changeRecord); } var notifierPrototype = {}; @@ -336,17 +461,16 @@ function ObjectNotifierNotify(changeRecord) { if (!IS_SPEC_OBJECT(this)) throw MakeTypeError("called_on_non_object", ["notify"]); - var target = notifierTargetMap.get(this); - if (IS_UNDEFINED(target)) + var objectInfo = ObjectInfoGetFromNotifier(this); + if (IS_UNDEFINED(objectInfo)) throw MakeTypeError("observe_notify_non_notifier"); if (!IS_STRING(changeRecord.type)) throw MakeTypeError("observe_type_non_string"); - var objectInfo = objectInfoMap.get(target); - if (IS_UNDEFINED(objectInfo) || objectInfo.changeObservers.length === 0) + if (!ObjectInfoHasActiveObservers(objectInfo)) return; - var newRecord = { object: target }; + var newRecord = { object: ObjectInfoGetObject(objectInfo) }; for (var prop in changeRecord) { if (prop === 'object') continue; %DefineOrRedefineDataProperty(newRecord, prop, changeRecord[prop], @@ -354,36 +478,28 @@ function ObjectNotifierNotify(changeRecord) { } ObjectFreeze(newRecord); - EnqueueChangeRecord(newRecord, objectInfo.changeObservers); + ObjectInfoEnqueueChangeRecord(objectInfo, newRecord, + true /* skip access check */); } -function ObjectNotifierPerformChange(changeType, changeFn, receiver) { +function ObjectNotifierPerformChange(changeType, changeFn) { if (!IS_SPEC_OBJECT(this)) throw MakeTypeError("called_on_non_object", ["performChange"]); - var target = notifierTargetMap.get(this); - if (IS_UNDEFINED(target)) + var objectInfo = ObjectInfoGetFromNotifier(this); + + if (IS_UNDEFINED(objectInfo)) throw MakeTypeError("observe_notify_non_notifier"); if (!IS_STRING(changeType)) throw MakeTypeError("observe_perform_non_string"); if (!IS_SPEC_FUNCTION(changeFn)) throw MakeTypeError("observe_perform_non_function"); - if (IS_NULL_OR_UNDEFINED(receiver)) { - receiver = %GetDefaultReceiver(changeFn) || receiver; - } else if (!IS_SPEC_OBJECT(receiver) && %IsClassicModeFunction(changeFn)) { - receiver = ToObject(receiver); - } - - var objectInfo = objectInfoMap.get(target); - if (IS_UNDEFINED(objectInfo)) - return; - - BeginPerformChange(objectInfo, changeType); + ObjectInfoAddPerformingType(objectInfo, changeType); try { - %_CallFunction(receiver, changeFn); + %_CallFunction(void 0, changeFn); } finally { - EndPerformChange(objectInfo, changeType); + ObjectInfoRemovePerformingType(objectInfo, changeType); } } @@ -393,15 +509,8 @@ function ObjectGetNotifier(object) { if (ObjectIsFrozen(object)) return null; - var objectInfo = objectInfoMap.get(object); - if (IS_UNDEFINED(objectInfo)) objectInfo = CreateObjectInfo(object); - - if (IS_NULL(objectInfo.notifier)) { - objectInfo.notifier = { __proto__: notifierPrototype }; - notifierTargetMap.set(objectInfo.notifier, object); - } - - return objectInfo.notifier; + var objectInfo = ObjectInfoGet(object); + return ObjectInfoGetNotifier(objectInfo); } function CallbackDeliverPending(callback) { @@ -414,12 +523,14 @@ function CallbackDeliverPending(callback) { var priority = callbackInfo.priority; callbackInfoMap.set(callback, priority); - delete observationState.pendingObservers[priority]; + if (observationState.pendingObservers) + delete observationState.pendingObservers[priority]; + var delivered = []; %MoveArrayContents(callbackInfo, delivered); try { - %Call(void 0, delivered, callback); + %_CallFunction(void 0, delivered, callback); } catch (ex) {} return true; } @@ -432,9 +543,9 @@ function ObjectDeliverChangeRecords(callback) { } function DeliverChangeRecords() { - while (observationState.pendingObservers.length) { + while (observationState.pendingObservers) { var pendingObservers = observationState.pendingObservers; - observationState.pendingObservers = new InternalArray; + observationState.pendingObservers = null; for (var i in pendingObservers) { CallbackDeliverPending(pendingObservers[i]); } |