• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2012 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5(function(global, utils) {
6
7"use strict";
8
9%CheckIsBootstrapping();
10
11// -------------------------------------------------------------------
12// Imports
13
14var GetHash;
15var GlobalArray = global.Array;
16var GlobalObject = global.Object;
17var InternalArray = utils.InternalArray;
18var MakeTypeError;
19
20utils.Import(function(from) {
21  GetHash = from.GetHash;
22  MakeTypeError = from.MakeTypeError;
23});
24
25// -------------------------------------------------------------------
26
27// Overview:
28//
29// This file contains all of the routing and accounting for Object.observe.
30// User code will interact with these mechanisms via the Object.observe APIs
31// and, as a side effect of mutation objects which are observed. The V8 runtime
32// (both C++ and JS) will interact with these mechanisms primarily by enqueuing
33// proper change records for objects which were mutated. The Object.observe
34// routing and accounting consists primarily of three participants
35//
36// 1) ObjectInfo. This represents the observed state of a given object. It
37//    records what callbacks are observing the object, with what options, and
38//    what "change types" are in progress on the object (i.e. via
39//    notifier.performChange).
40//
41// 2) CallbackInfo. This represents a callback used for observation. It holds
42//    the records which must be delivered to the callback, as well as the global
43//    priority of the callback (which determines delivery order between
44//    callbacks).
45//
46// 3) observationState.pendingObservers. This is the set of observers which
47//    have change records which must be delivered. During "normal" delivery
48//    (i.e. not Object.deliverChangeRecords), this is the mechanism by which
49//    callbacks are invoked in the proper order until there are no more
50//    change records pending to a callback.
51//
52// Note that in order to reduce allocation and processing costs, the
53// implementation of (1) and (2) have "optimized" states which represent
54// common cases which can be handled more efficiently.
55
56var observationState;
57
58var notifierPrototype = {};
59
60// We have to wait until after bootstrapping to grab a reference to the
61// observationState object, since it's not possible to serialize that
62// reference into the snapshot.
63function GetObservationStateJS() {
64  if (IS_UNDEFINED(observationState)) {
65    observationState = %GetObservationState();
66  }
67
68  // TODO(adamk): Consider moving this code into heap.cc
69  if (IS_UNDEFINED(observationState.callbackInfoMap)) {
70    observationState.callbackInfoMap = %ObservationWeakMapCreate();
71    observationState.objectInfoMap = %ObservationWeakMapCreate();
72    observationState.notifierObjectInfoMap = %ObservationWeakMapCreate();
73    observationState.pendingObservers = null;
74    observationState.nextCallbackPriority = 0;
75    observationState.lastMicrotaskId = 0;
76  }
77
78  return observationState;
79}
80
81
82function GetPendingObservers() {
83  return GetObservationStateJS().pendingObservers;
84}
85
86
87function SetPendingObservers(pendingObservers) {
88  GetObservationStateJS().pendingObservers = pendingObservers;
89}
90
91
92function GetNextCallbackPriority() {
93  return GetObservationStateJS().nextCallbackPriority++;
94}
95
96
97function nullProtoObject() {
98  return { __proto__: null };
99}
100
101
102function TypeMapCreate() {
103  return nullProtoObject();
104}
105
106
107function TypeMapAddType(typeMap, type, ignoreDuplicate) {
108  typeMap[type] = ignoreDuplicate ? 1 : (typeMap[type] || 0) + 1;
109}
110
111
112function TypeMapRemoveType(typeMap, type) {
113  typeMap[type]--;
114}
115
116
117function TypeMapCreateFromList(typeList, length) {
118  var typeMap = TypeMapCreate();
119  for (var i = 0; i < length; i++) {
120    TypeMapAddType(typeMap, typeList[i], true);
121  }
122  return typeMap;
123}
124
125
126function TypeMapHasType(typeMap, type) {
127  return !!typeMap[type];
128}
129
130
131function TypeMapIsDisjointFrom(typeMap1, typeMap2) {
132  if (!typeMap1 || !typeMap2)
133    return true;
134
135  for (var type in typeMap1) {
136    if (TypeMapHasType(typeMap1, type) && TypeMapHasType(typeMap2, type))
137      return false;
138  }
139
140  return true;
141}
142
143
144var defaultAcceptTypes = (function() {
145  var defaultTypes = [
146    'add',
147    'update',
148    'delete',
149    'setPrototype',
150    'reconfigure',
151    'preventExtensions'
152  ];
153  return TypeMapCreateFromList(defaultTypes, defaultTypes.length);
154})();
155
156
157// An Observer is a registration to observe an object by a callback with
158// a given set of accept types. If the set of accept types is the default
159// set for Object.observe, the observer is represented as a direct reference
160// to the callback. An observer never changes its accept types and thus never
161// needs to "normalize".
162function ObserverCreate(callback, acceptList) {
163  if (IS_UNDEFINED(acceptList))
164    return callback;
165  var observer = nullProtoObject();
166  observer.callback = callback;
167  observer.accept = acceptList;
168  return observer;
169}
170
171
172function ObserverGetCallback(observer) {
173  return IS_CALLABLE(observer) ? observer : observer.callback;
174}
175
176
177function ObserverGetAcceptTypes(observer) {
178  return IS_CALLABLE(observer) ? defaultAcceptTypes : observer.accept;
179}
180
181
182function ObserverIsActive(observer, objectInfo) {
183  return TypeMapIsDisjointFrom(ObjectInfoGetPerformingTypes(objectInfo),
184                               ObserverGetAcceptTypes(observer));
185}
186
187
188function ObjectInfoGetOrCreate(object) {
189  var objectInfo = ObjectInfoGet(object);
190  if (IS_UNDEFINED(objectInfo)) {
191    if (!IS_PROXY(object)) {
192      %SetIsObserved(object);
193    }
194    objectInfo = {
195      object: object,
196      changeObservers: null,
197      notifier: null,
198      performing: null,
199      performingCount: 0,
200    };
201    %WeakCollectionSet(GetObservationStateJS().objectInfoMap,
202                       object, objectInfo, GetHash(object));
203  }
204  return objectInfo;
205}
206
207
208function ObjectInfoGet(object) {
209  return %WeakCollectionGet(GetObservationStateJS().objectInfoMap, object,
210                            GetHash(object));
211}
212
213
214function ObjectInfoGetFromNotifier(notifier) {
215  return %WeakCollectionGet(GetObservationStateJS().notifierObjectInfoMap,
216                            notifier, GetHash(notifier));
217}
218
219
220function ObjectInfoGetNotifier(objectInfo) {
221  if (IS_NULL(objectInfo.notifier)) {
222    var notifier = { __proto__: notifierPrototype };
223    objectInfo.notifier = notifier;
224    %WeakCollectionSet(GetObservationStateJS().notifierObjectInfoMap,
225                       notifier, objectInfo, GetHash(notifier));
226  }
227
228  return objectInfo.notifier;
229}
230
231
232function ChangeObserversIsOptimized(changeObservers) {
233  return IS_CALLABLE(changeObservers) ||
234         IS_CALLABLE(changeObservers.callback);
235}
236
237
238// The set of observers on an object is called 'changeObservers'. The first
239// observer is referenced directly via objectInfo.changeObservers. When a second
240// is added, changeObservers "normalizes" to become a mapping of callback
241// priority -> observer and is then stored on objectInfo.changeObservers.
242function ObjectInfoNormalizeChangeObservers(objectInfo) {
243  if (ChangeObserversIsOptimized(objectInfo.changeObservers)) {
244    var observer = objectInfo.changeObservers;
245    var callback = ObserverGetCallback(observer);
246    var callbackInfo = CallbackInfoGet(callback);
247    var priority = CallbackInfoGetPriority(callbackInfo);
248    objectInfo.changeObservers = nullProtoObject();
249    objectInfo.changeObservers[priority] = observer;
250  }
251}
252
253
254function ObjectInfoAddObserver(objectInfo, callback, acceptList) {
255  var callbackInfo = CallbackInfoGetOrCreate(callback);
256  var observer = ObserverCreate(callback, acceptList);
257
258  if (!objectInfo.changeObservers) {
259    objectInfo.changeObservers = observer;
260    return;
261  }
262
263  ObjectInfoNormalizeChangeObservers(objectInfo);
264  var priority = CallbackInfoGetPriority(callbackInfo);
265  objectInfo.changeObservers[priority] = observer;
266}
267
268function ObjectInfoRemoveObserver(objectInfo, callback) {
269  if (!objectInfo.changeObservers)
270    return;
271
272  if (ChangeObserversIsOptimized(objectInfo.changeObservers)) {
273    if (callback === ObserverGetCallback(objectInfo.changeObservers))
274      objectInfo.changeObservers = null;
275    return;
276  }
277
278  var callbackInfo = CallbackInfoGet(callback);
279  var priority = CallbackInfoGetPriority(callbackInfo);
280  objectInfo.changeObservers[priority] = null;
281}
282
283function ObjectInfoHasActiveObservers(objectInfo) {
284  if (IS_UNDEFINED(objectInfo) || !objectInfo.changeObservers)
285    return false;
286
287  if (ChangeObserversIsOptimized(objectInfo.changeObservers))
288    return ObserverIsActive(objectInfo.changeObservers, objectInfo);
289
290  for (var priority in objectInfo.changeObservers) {
291    var observer = objectInfo.changeObservers[priority];
292    if (!IS_NULL(observer) && ObserverIsActive(observer, objectInfo))
293      return true;
294  }
295
296  return false;
297}
298
299
300function ObjectInfoAddPerformingType(objectInfo, type) {
301  objectInfo.performing = objectInfo.performing || TypeMapCreate();
302  TypeMapAddType(objectInfo.performing, type);
303  objectInfo.performingCount++;
304}
305
306
307function ObjectInfoRemovePerformingType(objectInfo, type) {
308  objectInfo.performingCount--;
309  TypeMapRemoveType(objectInfo.performing, type);
310}
311
312
313function ObjectInfoGetPerformingTypes(objectInfo) {
314  return objectInfo.performingCount > 0 ? objectInfo.performing : null;
315}
316
317
318function ConvertAcceptListToTypeMap(arg) {
319  // We use undefined as a sentinel for the default accept list.
320  if (IS_UNDEFINED(arg))
321    return arg;
322
323  if (!IS_RECEIVER(arg)) throw MakeTypeError(kObserveInvalidAccept);
324
325  var len = TO_INTEGER(arg.length);
326  if (len < 0) len = 0;
327
328  return TypeMapCreateFromList(arg, len);
329}
330
331
332// CallbackInfo's optimized state is just a number which represents its global
333// priority. When a change record must be enqueued for the callback, it
334// normalizes. When delivery clears any pending change records, it re-optimizes.
335function CallbackInfoGet(callback) {
336  return %WeakCollectionGet(GetObservationStateJS().callbackInfoMap, callback,
337                            GetHash(callback));
338}
339
340
341function CallbackInfoSet(callback, callbackInfo) {
342  %WeakCollectionSet(GetObservationStateJS().callbackInfoMap,
343                     callback, callbackInfo, GetHash(callback));
344}
345
346
347function CallbackInfoGetOrCreate(callback) {
348  var callbackInfo = CallbackInfoGet(callback);
349  if (!IS_UNDEFINED(callbackInfo))
350    return callbackInfo;
351
352  var priority = GetNextCallbackPriority();
353  CallbackInfoSet(callback, priority);
354  return priority;
355}
356
357
358function CallbackInfoGetPriority(callbackInfo) {
359  if (IS_NUMBER(callbackInfo))
360    return callbackInfo;
361  else
362    return callbackInfo.priority;
363}
364
365
366function CallbackInfoNormalize(callback) {
367  var callbackInfo = CallbackInfoGet(callback);
368  if (IS_NUMBER(callbackInfo)) {
369    var priority = callbackInfo;
370    callbackInfo = new InternalArray;
371    callbackInfo.priority = priority;
372    CallbackInfoSet(callback, callbackInfo);
373  }
374  return callbackInfo;
375}
376
377
378function ObjectObserve(object, callback, acceptList) {
379  if (!IS_RECEIVER(object))
380    throw MakeTypeError(kObserveNonObject, "observe", "observe");
381  if (%IsJSGlobalProxy(object))
382    throw MakeTypeError(kObserveGlobalProxy, "observe");
383  if (%IsAccessCheckNeeded(object))
384    throw MakeTypeError(kObserveAccessChecked, "observe");
385  if (!IS_CALLABLE(callback))
386    throw MakeTypeError(kObserveNonFunction, "observe");
387  if (%object_is_frozen(callback))
388    throw MakeTypeError(kObserveCallbackFrozen);
389
390  var objectObserveFn = %GetObjectContextObjectObserve(object);
391  return objectObserveFn(object, callback, acceptList);
392}
393
394
395function NativeObjectObserve(object, callback, acceptList) {
396  var objectInfo = ObjectInfoGetOrCreate(object);
397  var typeList = ConvertAcceptListToTypeMap(acceptList);
398  ObjectInfoAddObserver(objectInfo, callback, typeList);
399  return object;
400}
401
402
403function ObjectUnobserve(object, callback) {
404  if (!IS_RECEIVER(object))
405    throw MakeTypeError(kObserveNonObject, "unobserve", "unobserve");
406  if (%IsJSGlobalProxy(object))
407    throw MakeTypeError(kObserveGlobalProxy, "unobserve");
408  if (!IS_CALLABLE(callback))
409    throw MakeTypeError(kObserveNonFunction, "unobserve");
410
411  var objectInfo = ObjectInfoGet(object);
412  if (IS_UNDEFINED(objectInfo))
413    return object;
414
415  ObjectInfoRemoveObserver(objectInfo, callback);
416  return object;
417}
418
419
420function ArrayObserve(object, callback) {
421  return ObjectObserve(object, callback, ['add',
422                                          'update',
423                                          'delete',
424                                          'splice']);
425}
426
427
428function ArrayUnobserve(object, callback) {
429  return ObjectUnobserve(object, callback);
430}
431
432
433function ObserverEnqueueIfActive(observer, objectInfo, changeRecord) {
434  if (!ObserverIsActive(observer, objectInfo) ||
435      !TypeMapHasType(ObserverGetAcceptTypes(observer), changeRecord.type)) {
436    return;
437  }
438
439  var callback = ObserverGetCallback(observer);
440  if (!%ObserverObjectAndRecordHaveSameOrigin(callback, changeRecord.object,
441                                              changeRecord)) {
442    return;
443  }
444
445  var callbackInfo = CallbackInfoNormalize(callback);
446  if (IS_NULL(GetPendingObservers())) {
447    SetPendingObservers(nullProtoObject());
448    if (DEBUG_IS_ACTIVE) {
449      var id = ++GetObservationStateJS().lastMicrotaskId;
450      var name = "Object.observe";
451      %EnqueueMicrotask(function() {
452        %DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
453        ObserveMicrotaskRunner();
454        %DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });
455      });
456      %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });
457    } else {
458      %EnqueueMicrotask(ObserveMicrotaskRunner);
459    }
460  }
461  GetPendingObservers()[callbackInfo.priority] = callback;
462  callbackInfo.push(changeRecord);
463}
464
465
466function ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord, type) {
467  if (!ObjectInfoHasActiveObservers(objectInfo))
468    return;
469
470  var hasType = !IS_UNDEFINED(type);
471  var newRecord = hasType ?
472      { object: objectInfo.object, type: type } :
473      { object: objectInfo.object };
474
475  for (var prop in changeRecord) {
476    if (prop === 'object' || (hasType && prop === 'type')) continue;
477    %DefineDataPropertyUnchecked(
478        newRecord, prop, changeRecord[prop], READ_ONLY + DONT_DELETE);
479  }
480  %object_freeze(newRecord);
481
482  ObjectInfoEnqueueInternalChangeRecord(objectInfo, newRecord);
483}
484
485
486function ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord) {
487  // TODO(rossberg): adjust once there is a story for symbols vs proxies.
488  if (IS_SYMBOL(changeRecord.name)) return;
489
490  if (ChangeObserversIsOptimized(objectInfo.changeObservers)) {
491    var observer = objectInfo.changeObservers;
492    ObserverEnqueueIfActive(observer, objectInfo, changeRecord);
493    return;
494  }
495
496  for (var priority in objectInfo.changeObservers) {
497    var observer = objectInfo.changeObservers[priority];
498    if (IS_NULL(observer))
499      continue;
500    ObserverEnqueueIfActive(observer, objectInfo, changeRecord);
501  }
502}
503
504
505function BeginPerformSplice(array) {
506  var objectInfo = ObjectInfoGet(array);
507  if (!IS_UNDEFINED(objectInfo))
508    ObjectInfoAddPerformingType(objectInfo, 'splice');
509}
510
511
512function EndPerformSplice(array) {
513  var objectInfo = ObjectInfoGet(array);
514  if (!IS_UNDEFINED(objectInfo))
515    ObjectInfoRemovePerformingType(objectInfo, 'splice');
516}
517
518
519function EnqueueSpliceRecord(array, index, removed, addedCount) {
520  var objectInfo = ObjectInfoGet(array);
521  if (!ObjectInfoHasActiveObservers(objectInfo))
522    return;
523
524  var changeRecord = {
525    type: 'splice',
526    object: array,
527    index: index,
528    removed: removed,
529    addedCount: addedCount
530  };
531
532  %object_freeze(changeRecord);
533  %object_freeze(changeRecord.removed);
534  ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord);
535}
536
537
538function NotifyChange(type, object, name, oldValue) {
539  var objectInfo = ObjectInfoGet(object);
540  if (!ObjectInfoHasActiveObservers(objectInfo))
541    return;
542
543  var changeRecord;
544  if (arguments.length == 2) {
545    changeRecord = { type: type, object: object };
546  } else if (arguments.length == 3) {
547    changeRecord = { type: type, object: object, name: name };
548  } else {
549    changeRecord = {
550      type: type,
551      object: object,
552      name: name,
553      oldValue: oldValue
554    };
555  }
556
557  %object_freeze(changeRecord);
558  ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord);
559}
560
561
562function ObjectNotifierNotify(changeRecord) {
563  if (!IS_RECEIVER(this))
564    throw MakeTypeError(kCalledOnNonObject, "notify");
565
566  var objectInfo = ObjectInfoGetFromNotifier(this);
567  if (IS_UNDEFINED(objectInfo))
568    throw MakeTypeError(kObserveNotifyNonNotifier);
569  if (!IS_STRING(changeRecord.type))
570    throw MakeTypeError(kObserveTypeNonString);
571
572  ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord);
573}
574
575
576function ObjectNotifierPerformChange(changeType, changeFn) {
577  if (!IS_RECEIVER(this))
578    throw MakeTypeError(kCalledOnNonObject, "performChange");
579
580  var objectInfo = ObjectInfoGetFromNotifier(this);
581  if (IS_UNDEFINED(objectInfo))
582    throw MakeTypeError(kObserveNotifyNonNotifier);
583  if (!IS_STRING(changeType))
584    throw MakeTypeError(kObservePerformNonString);
585  if (!IS_CALLABLE(changeFn))
586    throw MakeTypeError(kObservePerformNonFunction);
587
588  var performChangeFn = %GetObjectContextNotifierPerformChange(objectInfo);
589  performChangeFn(objectInfo, changeType, changeFn);
590}
591
592
593function NativeObjectNotifierPerformChange(objectInfo, changeType, changeFn) {
594  ObjectInfoAddPerformingType(objectInfo, changeType);
595
596  var changeRecord;
597  try {
598    changeRecord = changeFn();
599  } finally {
600    ObjectInfoRemovePerformingType(objectInfo, changeType);
601  }
602
603  if (IS_RECEIVER(changeRecord))
604    ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord, changeType);
605}
606
607
608function ObjectGetNotifier(object) {
609  if (!IS_RECEIVER(object))
610    throw MakeTypeError(kObserveNonObject, "getNotifier", "getNotifier");
611  if (%IsJSGlobalProxy(object))
612    throw MakeTypeError(kObserveGlobalProxy, "getNotifier");
613  if (%IsAccessCheckNeeded(object))
614    throw MakeTypeError(kObserveAccessChecked, "getNotifier");
615
616  if (%object_is_frozen(object)) return null;
617
618  if (!%ObjectWasCreatedInCurrentOrigin(object)) return null;
619
620  var getNotifierFn = %GetObjectContextObjectGetNotifier(object);
621  return getNotifierFn(object);
622}
623
624
625function NativeObjectGetNotifier(object) {
626  var objectInfo = ObjectInfoGetOrCreate(object);
627  return ObjectInfoGetNotifier(objectInfo);
628}
629
630
631function CallbackDeliverPending(callback) {
632  var callbackInfo = CallbackInfoGet(callback);
633  if (IS_UNDEFINED(callbackInfo) || IS_NUMBER(callbackInfo))
634    return false;
635
636  // Clear the pending change records from callback and return it to its
637  // "optimized" state.
638  var priority = callbackInfo.priority;
639  CallbackInfoSet(callback, priority);
640
641  var pendingObservers = GetPendingObservers();
642  if (!IS_NULL(pendingObservers))
643    delete pendingObservers[priority];
644
645  // TODO: combine the following runtime calls for perf optimization.
646  var delivered = [];
647  %MoveArrayContents(callbackInfo, delivered);
648  %DeliverObservationChangeRecords(callback, delivered);
649
650  return true;
651}
652
653
654function ObjectDeliverChangeRecords(callback) {
655  if (!IS_CALLABLE(callback))
656    throw MakeTypeError(kObserveNonFunction, "deliverChangeRecords");
657
658  while (CallbackDeliverPending(callback)) {}
659}
660
661
662function ObserveMicrotaskRunner() {
663  var pendingObservers = GetPendingObservers();
664  if (!IS_NULL(pendingObservers)) {
665    SetPendingObservers(null);
666    for (var i in pendingObservers) {
667      CallbackDeliverPending(pendingObservers[i]);
668    }
669  }
670}
671
672// -------------------------------------------------------------------
673
674utils.InstallFunctions(notifierPrototype, DONT_ENUM, [
675  "notify", ObjectNotifierNotify,
676  "performChange", ObjectNotifierPerformChange
677]);
678
679var ObserveObjectMethods = [
680  "deliverChangeRecords", ObjectDeliverChangeRecords,
681  "getNotifier", ObjectGetNotifier,
682  "observe", ObjectObserve,
683  "unobserve", ObjectUnobserve
684];
685
686var ObserveArrayMethods = [
687  "observe", ArrayObserve,
688  "unobserve", ArrayUnobserve
689];
690
691// TODO(adamk): Figure out why this prototype removal has to
692// happen as part of initial snapshotting.
693var removePrototypeFn = function(f, i) {
694  if (i % 2 === 1) %FunctionRemovePrototype(f);
695};
696ObserveObjectMethods.forEach(removePrototypeFn);
697ObserveArrayMethods.forEach(removePrototypeFn);
698
699%InstallToContext([
700  "native_object_get_notifier", NativeObjectGetNotifier,
701  "native_object_notifier_perform_change", NativeObjectNotifierPerformChange,
702  "native_object_observe", NativeObjectObserve,
703  "observers_begin_perform_splice", BeginPerformSplice,
704  "observers_end_perform_splice", EndPerformSplice,
705  "observers_enqueue_splice", EnqueueSpliceRecord,
706  "observers_notify_change", NotifyChange,
707]);
708
709utils.Export(function(to) {
710  to.ObserveArrayMethods = ObserveArrayMethods;
711  to.ObserveBeginPerformSplice = BeginPerformSplice;
712  to.ObserveEndPerformSplice = EndPerformSplice;
713  to.ObserveEnqueueSpliceRecord = EnqueueSpliceRecord;
714  to.ObserveObjectMethods = ObserveObjectMethods;
715});
716
717})
718