Chromium Code Reviews| Index: src/object-observe.js |
| diff --git a/src/object-observe.js b/src/object-observe.js |
| index f5e0d9d563a08e0507d01b03cc519370e49d0215..9781dbd57f07002e9d8f049c72ece7df8f64691f 100644 |
| --- a/src/object-observe.js |
| +++ b/src/object-observe.js |
| @@ -31,8 +31,9 @@ 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 = { __proto__: null }; |
| + observationState.anyPendingObservers = false; |
| observationState.nextCallbackPriority = 0; |
| } |
| @@ -59,126 +60,190 @@ 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; |
|
rossberg
2013/08/07 13:02:14
Hm, this hack could be avoided if AcceptArgIsValid
rafaelw
2013/08/07 20:36:28
That's not the issue. objectInfo.performing is a t
|
| +} |
| + |
| +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]; |
|
rossberg
2013/08/07 13:02:14
Nit: !!typeMap[type]
rafaelw
2013/08/07 20:36:28
Done.
|
| +} |
| + |
| +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; |
| +} |
| -function CreateObserver(callback, accept) { |
| - var observer = { |
| +var defaultAcceptTypes = TypeMapCreateFromList([ |
| + 'new', |
| + 'updated', |
| + 'deleted', |
| + 'prototype', |
| + 'reconfigured' |
| +]); |
| + |
| +// An Observer is a registration to observe an object by a callback with a |
| +// a given set of accept types. If the set of accept types is the default |
|
rossberg
2013/08/07 13:02:14
Nit: duplicate "a"
rafaelw
2013/08/07 20:36:28
Done.
|
| +// 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 ObjectInfoGetOrCreate(object) { |
| + var objectInfo = objectInfoMap.get(object); |
| + if (IS_UNDEFINED(objectInfo)) { |
| + if (!%IsJSProxy(object)) |
| + %SetIsObserved(object); |
| + |
| + objectInfo = { |
| + object: object, |
| + changeObserver: null, |
| + changeObservers: null, |
| + notifier: null, |
| + performing: null, |
| + performingCount: 0, |
| + }; |
| + objectInfoMap.set(object, objectInfo); |
| } |
| + return objectInfo; |
| +} |
| - return true; |
| +function ObjectInfoGet(object) { |
|
rossberg
2013/08/07 13:02:14
Is this still used at all? If not, you could as we
rafaelw
2013/08/07 20:36:28
Done.
|
| + return objectInfoMap.get(object); |
| } |
| -function ObserverIsInactive(observer, objectInfo) { |
| - return !ObserverIsActive(observer, objectInfo); |
| +function ObjectInfoGetFromNotifier(notifier) { |
| + return notifierObjectInfoMap.get(notifier); |
| } |
| -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++; |
| +function ObjectInfoGetNotifier(objectInfo) { |
| + if (IS_NULL(objectInfo.notifier)) { |
| + objectInfo.notifier = { __proto__: notifierPrototype }; |
| + notifierObjectInfoMap.set(objectInfo.notifier, objectInfo); |
| } |
| - if (i !== j) |
| - from.length = from.length - (i - j); |
| + return objectInfo.notifier; |
| } |
| -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 ObjectInfoGetObject(objectInfo) { |
| + return objectInfo.object; |
| +} |
| + |
| +// The set of observers on an object is called 'changeObservers'. The first |
| +// observer is referenced directly via objectInfo.changeObserver. When a second |
|
rossberg
2013/08/07 13:02:14
Wouldn't it be slightly simpler to use only 'chang
rafaelw
2013/08/07 20:36:28
The problem is that an "observer" can be normal (a
rossberg
2013/08/09 11:26:51
I don't feel too strongly about it. But I would ar
rafaelw
2013/08/09 14:14:37
Fair enough. Done.
On 2013/08/09 11:26:51, rossbe
|
| +// is added, changeObservers "normalizes" to become a mapping of callback |
| +// priority -> observer and is then stored on objectInfo.changeObservers. |
| +function ObjectInfoNormalizeChangeObservers(objectInfo) { |
| + var observer = objectInfo.changeObserver; |
| + objectInfo.changeObserver = null; |
| + objectInfo.changeObservers = { __proto__: null }; |
| + var callback = ObserverGetCallback(observer); |
| + var callbackInfo = CallbackInfoGet(callback); |
| + var priority = CallbackInfoGetPriority(callbackInfo); |
| + objectInfo.changeObservers[priority] = observer; |
| +} |
| + |
| +function ObjectInfoAddObserver(objectInfo, callback, acceptList) { |
| + var callbackInfo = CallbackInfoGetOrCreate(callback); |
| + var observer = ObserverCreate(callback, acceptList); |
| + |
| + if (!objectInfo.changeObserver && !objectInfo.changeObservers) { |
| + objectInfo.changeObserver = observer; |
| + return; |
| } |
| - if (anyRemoved) |
| - RemoveNullElements(from); |
| + if (objectInfo.changeObserver) |
| + ObjectInfoNormalizeChangeObservers(objectInfo); |
| + |
| + var priority = CallbackInfoGetPriority(callbackInfo); |
| + objectInfo.changeObservers[priority] = observer; |
| } |
| -function BeginPerformChange(objectInfo, type) { |
| - objectInfo.performing[type] = (objectInfo.performing[type] || 0) + 1; |
| - objectInfo.performingCount++; |
| - RepartitionObservers(ObserverIsInactive, |
| - objectInfo.changeObservers, |
| - objectInfo.inactiveObservers, |
| - objectInfo); |
| +function ObjectInfoRemoveObserver(objectInfo, callback) { |
| + if (objectInfo.changeObserver) { |
| + if (callback === ObserverGetCallback(objectInfo.changeObserver)) |
| + objectInfo.changeObserver = null; |
| + |
| + return; |
| + } |
| + |
| + var callbackInfo = CallbackInfoGet(callback); |
| + var priority = CallbackInfoGetPriority(callbackInfo); |
| + delete objectInfo.changeObservers[priority]; |
| } |
| -function EndPerformChange(objectInfo, type) { |
| - objectInfo.performing[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; |
| - } |
| - } |
| +function ObjectInfoHasActiveObservers(objectInfo) { |
| + if (IS_UNDEFINED(objectInfo)) |
| return false; |
| + |
| + if (objectInfo.changeObserver) |
| + return ObserverIsActive(objectInfo.changeObserver, objectInfo); |
| + |
| + for (var priority in objectInfo.changeObservers) { |
| + if (ObserverIsActive(objectInfo.changeObservers[priority], objectInfo)) |
| + return true; |
| } |
| - if (!remove(objectInfo.changeObservers)) |
| - remove(objectInfo.inactiveObservers); |
| + return false; |
| +} |
| + |
| +function ObjectInfoAddPerformingType(objectInfo, type) { |
| + objectInfo.performing = objectInfo.performing || TypeMapCreate(); |
| + TypeMapAddType(objectInfo.performing, type); |
| + objectInfo.performingCount++; |
| +} |
| + |
| +function ObjectInfoRemovePerformingType(objectInfo, type) { |
| + objectInfo.performingCount--; |
| + TypeMapRemoveType(objectInfo.performing, type); |
| +} |
| + |
| +function ObjectInfoGetPerformingTypes(objectInfo) { |
| + return objectInfo.performingCount > 0 ? objectInfo.performing : null; |
| } |
| function AcceptArgIsValid(arg) { |
| @@ -198,12 +263,28 @@ function AcceptArgIsValid(arg) { |
| return true; |
| } |
| -function EnsureCallbackPriority(callback) { |
| - if (!callbackInfoMap.has(callback)) |
| - callbackInfoMap.set(callback, observationState.nextCallbackPriority++); |
| +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 CallbackInfoGetPriority(callbackInfo) { |
| + if (IS_NUMBER(callbackInfo)) |
| + return callbackInfo; |
| + else |
| + return callbackInfo.priority; |
| } |
| -function NormalizeCallbackInfo(callback) { |
| +function CallbackInfoNormalize(callback) { |
| var callbackInfo = callbackInfoMap.get(callback); |
| if (IS_NUMBER(callbackInfo)) { |
| var priority = callbackInfo; |
| @@ -214,32 +295,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 = ObjectInfoGetOrCreate(object); |
| + ObjectInfoAddObserver(objectInfo, callback, acceptList); |
| return object; |
| } |
| @@ -253,7 +320,7 @@ function ObjectUnobserve(object, callback) { |
| if (IS_UNDEFINED(objectInfo)) |
| return object; |
| - EnsureObserverRemoved(objectInfo, callback); |
| + ObjectInfoRemoveObserver(objectInfo, callback); |
| return object; |
| } |
| @@ -268,41 +335,51 @@ function ArrayUnobserve(object, callback) { |
| return ObjectUnobserve(object, callback); |
| } |
| -function EnqueueToCallback(callback, changeRecord) { |
| - var callbackInfo = NormalizeCallbackInfo(callback); |
| +function ObserverEnqueueIfActive(observer, objectInfo, changeRecord) { |
| + if (!ObserverIsActive(observer, objectInfo) || |
| + !TypeMapHasType(ObserverGetAcceptTypes(observer), changeRecord.type)) { |
| + return; |
| + } |
| + |
| + var callback = ObserverGetCallback(observer); |
| + var callbackInfo = CallbackInfoNormalize(callback); |
| observationState.pendingObservers[callbackInfo.priority] = callback; |
| + observationState.anyPendingObservers = true; |
| callbackInfo.push(changeRecord); |
| %SetObserverDeliveryPending(); |
| } |
| -function EnqueueChangeRecord(changeRecord, observers) { |
| +function ObjectInfoEnqueueChangeRecord(objectInfo, changeRecord) { |
| // 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; |
| + if (objectInfo.changeObserver) { |
| + var observer = objectInfo.changeObserver; |
| + ObserverEnqueueIfActive(observer, objectInfo, changeRecord); |
| + return; |
| + } |
| - EnqueueToCallback(observer.callback, changeRecord); |
| + for (var priority in objectInfo.changeObservers) { |
| + var observer = objectInfo.changeObservers[priority]; |
| + ObserverEnqueueIfActive(observer, objectInfo, changeRecord); |
| } |
| } |
| 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 +392,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 +413,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,15 +430,16 @@ function ObjectNotifierNotify(changeRecord) { |
| } |
| ObjectFreeze(newRecord); |
| - EnqueueChangeRecord(newRecord, objectInfo.changeObservers); |
| + ObjectInfoEnqueueChangeRecord(objectInfo, newRecord); |
| } |
| function ObjectNotifierPerformChange(changeType, changeFn, receiver) { |
| 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"); |
| @@ -375,15 +452,11 @@ function ObjectNotifierPerformChange(changeType, changeFn, receiver) { |
| receiver = ToObject(receiver); |
| } |
| - var objectInfo = objectInfoMap.get(target); |
| - if (IS_UNDEFINED(objectInfo)) |
| - return; |
| - |
| - BeginPerformChange(objectInfo, changeType); |
| + ObjectInfoAddPerformingType(objectInfo, changeType); |
| try { |
| %_CallFunction(receiver, changeFn); |
| } finally { |
| - EndPerformChange(objectInfo, changeType); |
| + ObjectInfoRemovePerformingType(objectInfo, changeType); |
| } |
| } |
| @@ -393,18 +466,8 @@ function ObjectGetNotifier(object) { |
| if (ObjectIsFrozen(object)) return null; |
| - var objectInfo = objectInfoMap.get(object); |
| - if (IS_UNDEFINED(objectInfo)) { |
| - objectInfo = CreateObjectInfo(object); |
| - %SetIsObserved(object); |
| - } |
| - |
| - if (IS_NULL(objectInfo.notifier)) { |
| - objectInfo.notifier = { __proto__: notifierPrototype }; |
| - notifierTargetMap.set(objectInfo.notifier, object); |
| - } |
| - |
| - return objectInfo.notifier; |
| + var objectInfo = ObjectInfoGetOrCreate(object); |
| + return ObjectInfoGetNotifier(objectInfo); |
| } |
| function CallbackDeliverPending(callback) { |
| @@ -435,9 +498,10 @@ function ObjectDeliverChangeRecords(callback) { |
| } |
| function DeliverChangeRecords() { |
| - while (observationState.pendingObservers.length) { |
| + while (observationState.anyPendingObservers) { |
|
rossberg
2013/08/07 13:02:14
I don't think you need to make this flag part of t
rafaelw
2013/08/07 20:36:28
I don't understand. observationState.anyPendingObs
rossberg
2013/08/09 11:26:51
What I mean is that all you are really implementin
rafaelw
2013/08/09 14:14:37
I tried & profiled this version, but it made deliv
rossberg
2013/08/13 09:19:25
Hm, that sounds like a lot. Did you try a version
rafaelw
2013/08/19 18:51:24
That one is 3x more expensive.
On 2013/08/13 09:1
|
| var pendingObservers = observationState.pendingObservers; |
| - observationState.pendingObservers = new InternalArray; |
| + observationState.pendingObservers = { __proto__: null }; |
| + observationState.anyPendingObservers = false |
| for (var i in pendingObservers) { |
| CallbackDeliverPending(pendingObservers[i]); |
| } |