LCOV - code coverage report
Current view: top level - Core/Scripting - OOJSFrameCallbacks.m (source / functions) Hit Total Coverage
Test: coverxygen.info Lines: 0 31 0.0 %
Date: 2025-05-28 07:50:54 Functions: 0 0 -

          Line data    Source code
       1           0 : /*
       2             : 
       3             : OOJSFrameCallbacks.m
       4             : 
       5             : 
       6             : Copyright (C) 2011-2013 Jens Ayton
       7             : 
       8             : Permission is hereby granted, free of charge, to any person obtaining a copy
       9             : of this software and associated documentation files (the "Software"), to deal
      10             : in the Software without restriction, including without limitation the rights
      11             : to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      12             : copies of the Software, and to permit persons to whom the Software is
      13             : furnished to do so, subject to the following conditions:
      14             : 
      15             : The above copyright notice and this permission notice shall be included in all
      16             : copies or substantial portions of the Software.
      17             : 
      18             : THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
      19             : IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      20             : FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
      21             : AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
      22             : LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
      23             : OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
      24             : SOFTWARE.
      25             : 
      26             : */
      27             : 
      28             : #import "OOJSFrameCallbacks.h"
      29             : #import "OOJSEngineTimeManagement.h"
      30             : #import "OOCollectionExtractors.h"
      31             : 
      32             : 
      33             : /*
      34             :         By default, tracking IDs are scrambled to discourage people from trying to
      35             :         be clever or making assumptions about them. If DEBUG_FCB_SIMPLE_TRACKING_IDS
      36             :         is non-zero, tracking IDs starting from 1 and rising monotonously are used
      37             :         instead. Additionally, the next ID is reset to 1 when all frame callbacks
      38             :         are removed.
      39             : */
      40             : #ifndef DEBUG_FCB_SIMPLE_TRACKING_IDS
      41           0 : #define DEBUG_FCB_SIMPLE_TRACKING_IDS   0
      42             : #endif
      43             : 
      44             : #ifndef DEBUG_FCB_VERBOSE_LOGGING
      45           0 : #define DEBUG_FCB_VERBOSE_LOGGING               0
      46             : #endif
      47             : 
      48             : 
      49             : 
      50             : #if defined (NDEBUG) && DEBUG_FCB_SIMPLE_TRACKING_IDS
      51             : #error Deployment builds may not be built with DEBUG_FCB_SIMPLE_TRACKING_IDS.
      52             : #endif
      53             : 
      54             : #if DEBUG_FCB_VERBOSE_LOGGING
      55             : #define FCBLog                                  OOLog
      56             : #define FCBLogIndentIf                  OOLogIndentIf
      57             : #define FCBLogOutdentIf                 OOLogOutdentIf
      58             : #else
      59           0 : #define FCBLog(...)                             do {} while (0)
      60           0 : #define FCBLogIndentIf(key)             do {} while (0)
      61           0 : #define FCBLogOutdentIf(key)    do {} while (0)
      62             : #endif
      63             : 
      64             : 
      65           0 : enum
      66             : {
      67             :         kMinCount                                       = 16,
      68             :         
      69             : #if DEBUG_FCB_SIMPLE_TRACKING_IDS
      70             :         kIDScrambleMask                         = 0,
      71             :         kIDIncrement                            = 1
      72             : #else
      73             :         kIDScrambleMask                         = 0x2315EB16,   // Just a random number.
      74             :         kIDIncrement                            = 992699                // A large prime number, to produce a non-obvious sequence which still uses all 2^32 values.
      75             : #endif
      76             : };
      77             : 
      78             : 
      79           0 : typedef struct
      80             : {
      81           0 :         jsval                                   callback;
      82           0 :         uint32                                  trackingID;
      83           0 :         uint32                                  _padding;
      84             : } CallbackEntry;
      85             : 
      86             : 
      87           0 : static CallbackEntry    *sCallbacks;
      88           0 : static NSUInteger               sCount;                 // Number of slots in use.
      89           0 : static NSUInteger               sSpace;                 // Number of slots allocated.
      90           0 : static NSUInteger               sHighWaterMark; // Number of slots which are GC roots.
      91           0 : static NSMutableArray   *sDeferredOps;  // Deferred adds/removes while running.
      92           0 : static uint32                   sNextID;
      93           0 : static BOOL                             sRunning;
      94             : 
      95             : 
      96             : // Methods
      97             : static JSBool GlobalAddFrameCallback(JSContext *context, uintN argc, jsval *vp);
      98             : static JSBool GlobalRemoveFrameCallback(JSContext *context, uintN argc, jsval *vp);
      99             : static JSBool GlobalIsValidFrameCallback(JSContext *context, uintN argc, jsval *vp);
     100             : 
     101             : 
     102             : // Internals
     103             : static BOOL AddCallback(JSContext *context, jsval callback, uint32 trackingID, NSString **errorString);
     104             : static BOOL GrowCallbackList(JSContext *context, NSString **errorString);
     105             : 
     106             : static BOOL GetIndexForTrackingID(uint32 trackingID, NSUInteger *outIndex);
     107             : 
     108             : static BOOL RemoveCallbackWithTrackingID(JSContext *context, uint32 trackingID);
     109             : static void RemoveCallbackAtIndex(JSContext *context, NSUInteger index);
     110             : 
     111             : static void QueueDeferredOperation(NSString *opType, uint32 trackingID, OOJSValue *value);
     112             : static void RunDeferredOperations(JSContext *context);
     113             : 
     114             : 
     115             : // MARK: Public
     116             : 
     117           0 : void InitOOJSFrameCallbacks(JSContext *context, JSObject *global)
     118             : {
     119             :         JS_DefineFunction(context, global, "addFrameCallback", GlobalAddFrameCallback, 1, OOJS_METHOD_READONLY);
     120             :         JS_DefineFunction(context, global, "removeFrameCallback", GlobalRemoveFrameCallback, 1, OOJS_METHOD_READONLY);
     121             :         JS_DefineFunction(context, global, "isValidFrameCallback", GlobalIsValidFrameCallback, 1, OOJS_METHOD_READONLY);
     122             :         
     123             : #if DEBUG_FCB_SIMPLE_TRACKING_IDS
     124             :         sNextID = 1;
     125             : #else
     126             :         // Set randomish initial ID to catch bad habits.
     127             :         sNextID =  [[NSDate date] timeIntervalSinceReferenceDate];
     128             : #endif
     129             : }
     130             : 
     131             : 
     132           0 : void OOJSFrameCallbacksInvoke(OOTimeDelta inDeltaT)
     133             : {
     134             :         NSCAssert1(!sRunning, @"%s cannot be called while frame callbacks are running.", __PRETTY_FUNCTION__);
     135             :         
     136             :         if (sCount != 0)
     137             :         {
     138             :                 const OOTimeDelta       delta = inDeltaT * [UNIVERSE timeAccelerationFactor];
     139             :                 JSContext                       *context = OOJSAcquireContext();
     140             :                 jsval                           deltaVal, result;
     141             :                 NSUInteger                      i;
     142             :                 
     143             :                 if (EXPECT(JS_NewNumberValue(context, delta, &deltaVal)))
     144             :                 {
     145             :                         // Block mutations.
     146             :                         sRunning = YES;
     147             :                         
     148             :                         /*
     149             :                                 The watchdog timer only fires once per second in deployment builds,
     150             :                                 but in testrelease builds at least we can keep them on a short leash.
     151             :                         */
     152             :                         OOJSStartTimeLimiterWithTimeLimit(0.1);
     153             :                         
     154             :                         for (i = 0; i < sCount; i++)
     155             :                         {
     156             :                                 // TODO: remove out of scope callbacks - post MNSR!
     157             :                                 JS_CallFunctionValue(context, NULL, sCallbacks[i].callback, 1, &deltaVal, &result);
     158             :                                 JS_ReportPendingException(context);
     159             :                         }
     160             :                         
     161             :                         OOJSStopTimeLimiter();
     162             :                         sRunning = NO;
     163             :                         
     164             :                         if (EXPECT_NOT(sDeferredOps != NULL))
     165             :                         {
     166             :                                 RunDeferredOperations(context);
     167             :                                 DESTROY(sDeferredOps);
     168             :                         }
     169             :                 }
     170             :                 OOJSRelinquishContext(context);
     171             :         }
     172             : }
     173             : 
     174             : 
     175           0 : void OOJSFrameCallbacksRemoveAll(void)
     176             : {
     177             :         NSCAssert1(!sRunning, @"%s cannot be called while frame callbacks are running.", __PRETTY_FUNCTION__);
     178             :         
     179             :         if (sCount != 0)
     180             :         {
     181             :                 JSContext *context = OOJSAcquireContext();
     182             :                 while (sCount != 0)  RemoveCallbackAtIndex(context, sCount - 1);
     183             :                 OOJSRelinquishContext(context);
     184             :         }
     185             : }
     186             : 
     187             : 
     188             : // MARK: Methods
     189             : 
     190             : // addFrameCallback(callback : Function) : Number
     191           0 : static JSBool GlobalAddFrameCallback(JSContext *context, uintN argc, jsval *vp)
     192             : {
     193             :         OOJS_NATIVE_ENTER(context)
     194             :         
     195             :         // Get callback argument and verify that it's a function.
     196             :         jsval callback = OOJS_ARGV[0];
     197             :         if (EXPECT_NOT(argc < 1 || !OOJSValueIsFunction(context, callback)))
     198             :         {
     199             :                 OOJSReportBadArguments(context, nil, @"addFrameCallback", MIN(argc, 1U), OOJS_ARGV, nil, @"function");
     200             :                 return NO;
     201             :         }
     202             :         
     203             :         // Assign a tracking ID.
     204             :         uint32 trackingID = sNextID ^ kIDScrambleMask;
     205             :         sNextID += kIDIncrement;
     206             :         
     207             :         if (EXPECT(!sRunning))
     208             :         {
     209             :                 // Add to list immediately.
     210             :                 NSString *errorString = nil;
     211             :                 if (EXPECT_NOT(!AddCallback(context, callback, trackingID, &errorString)))
     212             :                 {
     213             :                         OOJSReportError(context, @"%@", errorString);
     214             :                         return NO;
     215             :                 }
     216             :         }
     217             :         else
     218             :         {
     219             :                 // Defer mutations during callback invocation.
     220             :                 FCBLog(@"script.frameCallback.debug.add.deferred", @"Deferring addition of frame callback with tracking ID %u.", trackingID);
     221             :                 QueueDeferredOperation(@"add", trackingID, [OOJSValue valueWithJSValue:callback inContext:context]);
     222             :         }
     223             :         
     224             :         OOJS_RETURN_INT(trackingID);
     225             :         
     226             :         OOJS_NATIVE_EXIT
     227             : }
     228             : 
     229             : 
     230             : // removeFrameCallback(trackingID : Number)
     231           0 : static JSBool GlobalRemoveFrameCallback(JSContext *context, uintN argc, jsval *vp)
     232             : {
     233             :         OOJS_NATIVE_ENTER(context)
     234             :         
     235             :         // Get tracking ID argument.
     236             :         uint32 trackingID;
     237             :         if (EXPECT_NOT(argc < 1 || !JS_ValueToECMAUint32(context, OOJS_ARGV[0], &trackingID)))
     238             :         {
     239             :                 OOJSReportBadArguments(context, nil, @"removeFrameCallback", MIN(argc, 1U), OOJS_ARGV, nil, @"frame callback tracking ID");
     240             :                 return NO;
     241             :         }
     242             :         
     243             :         if (EXPECT(!sRunning))
     244             :         {
     245             :                 // Remove it.
     246             :                 if (EXPECT_NOT(!RemoveCallbackWithTrackingID(context, trackingID)))
     247             :                 {
     248             :                         OOJSReportWarning(context, @"removeFrameCallback(): invalid tracking ID.");
     249             :                 }
     250             :         }
     251             :         else
     252             :         {
     253             :                 // Defer mutations during callback invocation.
     254             :                 FCBLog(@"script.frameCallback.debug.remove.deferred", @"Deferring removal of frame callback with tracking ID %u.", trackingID);
     255             :                 QueueDeferredOperation(@"remove", trackingID, nil);
     256             :         }
     257             :         
     258             :         OOJS_RETURN_VOID;
     259             :         
     260             :         OOJS_NATIVE_EXIT
     261             : }
     262             : 
     263             : 
     264             : // isValidFrameCallback(trackingID : Number)
     265           0 : static JSBool GlobalIsValidFrameCallback(JSContext *context, uintN argc, jsval *vp)
     266             : {
     267             :         OOJS_NATIVE_ENTER(context)
     268             :         
     269             :         if (EXPECT_NOT(argc < 1))
     270             :         {
     271             :                 OOJSReportBadArguments(context, nil, @"isValidFrameCallback", 0, OOJS_ARGV, nil, @"frame callback tracking ID");
     272             :                 return NO;
     273             :         }
     274             :         
     275             :         // Get tracking ID argument.
     276             :         uint32 trackingID;
     277             :         if (EXPECT_NOT(!JS_ValueToECMAUint32(context, OOJS_ARGV[0], &trackingID)))
     278             :         {
     279             :                 OOJS_RETURN_BOOL(NO);
     280             :         }
     281             :         
     282             :         NSUInteger index;
     283             :         OOJS_RETURN_BOOL(GetIndexForTrackingID(trackingID, &index));
     284             :         
     285             :         OOJS_NATIVE_EXIT
     286             : }
     287             : 
     288             : 
     289             : // MARK: Internals
     290             : 
     291           0 : static BOOL AddCallback(JSContext *context, jsval callback, uint32 trackingID, NSString **errorString)
     292             : {
     293             :         NSCParameterAssert(context != NULL && JS_IsInRequest(context));
     294             :         NSCParameterAssert(errorString != NULL);
     295             :         NSCAssert1(!sRunning, @"%s cannot be called while frame callbacks are running.", __PRETTY_FUNCTION__);
     296             :         
     297             :         if (EXPECT_NOT(sCount == sSpace))
     298             :         {
     299             :                 if (!GrowCallbackList(context, errorString))  return NO;
     300             :         }
     301             :         
     302             :         FCBLog(@"script.frameCallback.debug.add", @"Adding frame callback with tracking ID %u.", trackingID);
     303             :         
     304             :         sCallbacks[sCount].callback = callback;
     305             :         if (sCount >= sHighWaterMark)
     306             :         {
     307             :                 // If we haven't used this slot before, root it.
     308             :                 
     309             :                 if (EXPECT_NOT(!OOJSAddGCValueRoot(context, &sCallbacks[sCount].callback, "frame callback")))
     310             :                 {
     311             :                         *errorString = @"Failed to add GC root for frame callback.";
     312             :                         return NO;
     313             :                 }
     314             :                 
     315             :                 sHighWaterMark = sCount + 1;
     316             :         }
     317             :         
     318             :         sCallbacks[sCount].trackingID = trackingID;
     319             :         sCount++;
     320             :         
     321             :         return YES;
     322             : }
     323             : 
     324             : 
     325           0 : static BOOL GrowCallbackList(JSContext *context, NSString **errorString)
     326             : {
     327             :         NSCParameterAssert(context != NULL && JS_IsInRequest(context));
     328             :         NSCParameterAssert(errorString != NULL);
     329             :         
     330             :         NSUInteger newSpace = MAX(sSpace * 2, (NSUInteger)kMinCount);
     331             :         
     332             :         CallbackEntry *newCallbacks = calloc(sizeof (CallbackEntry), newSpace);
     333             :         if (newCallbacks == NULL)  return NO;
     334             :         
     335             :         CallbackEntry *oldCallbacks = sCallbacks;
     336             :         
     337             :         // Root and copy occupied slots.
     338             :         NSUInteger newHighWaterMark = sCount;
     339             :         NSUInteger i;
     340             :         for (i = 0; i < newHighWaterMark; i++)
     341             :         {
     342             :                 if (EXPECT_NOT(!OOJSAddGCValueRoot(context, &newCallbacks[i].callback, "frame callback")))
     343             :                 {
     344             :                         // If we can't root them all, we fail; unroot all entries to date, free the buffer and return NO.
     345             :                         NSUInteger j;
     346             :                         for (j = 0; j < i; j++)
     347             :                         {
     348             :                                 JS_RemoveValueRoot(context, &newCallbacks[j].callback);
     349             :                         }
     350             :                         free(newCallbacks);
     351             :                         
     352             :                         *errorString = @"Failed to add GC root for frame callback.";
     353             :                         return NO;
     354             :                 }
     355             :                 newCallbacks[i] = oldCallbacks[i];
     356             :         }
     357             :         
     358             :         // Unroot old array's slots.
     359             :         for (i = 0; i < sHighWaterMark; i++)
     360             :         {
     361             :                 JS_RemoveValueRoot(context, &oldCallbacks[i].callback);
     362             :         }
     363             :         
     364             :         // We only rooted the occupied slots, so reset high water mark.
     365             :         sHighWaterMark = newHighWaterMark;
     366             :         
     367             :         // Replace array.
     368             :         sCallbacks = newCallbacks;
     369             :         free(oldCallbacks);
     370             :         sSpace = newSpace;
     371             :         
     372             :         return YES;
     373             : }
     374             : 
     375             : 
     376           0 : static BOOL GetIndexForTrackingID(uint32 trackingID, NSUInteger *outIndex)
     377             : {
     378             :         NSCParameterAssert(outIndex != NULL);
     379             :         
     380             :         /*      It is assumed that few frame callbacks will be active at once, so a
     381             :                 linear search is reasonable. If they become unexpectedly popular, we
     382             :                 can switch to a sorted list or a separate lookup table without changing
     383             :                 the API.
     384             :         */
     385             :         NSUInteger i;
     386             :         for (i = 0; i < sCount; i++)
     387             :         {
     388             :                 if (sCallbacks[i].trackingID == trackingID)
     389             :                 {
     390             :                         *outIndex = i;
     391             :                         return YES;
     392             :                 }
     393             :         }
     394             :         
     395             :         return NO;
     396             : }
     397             : 
     398             : 
     399           0 : static BOOL RemoveCallbackWithTrackingID(JSContext *context, uint32 trackingID)
     400             : {
     401             :         NSCParameterAssert(context != NULL && JS_IsInRequest(context));
     402             :         NSCAssert1(!sRunning, @"%s cannot be called while frame callbacks are running.", __PRETTY_FUNCTION__);
     403             :         
     404             :         NSUInteger index = 0;
     405             :         if (GetIndexForTrackingID(trackingID, &index))
     406             :         {
     407             :                 RemoveCallbackAtIndex(context, index);
     408             :                 return YES;
     409             :         }
     410             :         
     411             :         return NO;
     412             : }
     413             : 
     414             : 
     415           0 : static void RemoveCallbackAtIndex(JSContext *context, NSUInteger index)
     416             : {
     417             :         NSCParameterAssert(context != NULL && JS_IsInRequest(context));
     418             :         NSCParameterAssert(index < sCount && sCallbacks != NULL);
     419             :         NSCAssert1(!sRunning, @"%s cannot be called while frame callbacks are running.", __PRETTY_FUNCTION__);
     420             :         
     421             :         FCBLog(@"script.frameCallback.debug.remove", @"Removing frame callback with tracking ID %u.", sCallbacks[index].trackingID);
     422             :         
     423             :         // Overwrite entry to be removed with last entry, and decrement count.
     424             :         sCount--;
     425             :         sCallbacks[index] = sCallbacks[sCount];
     426             :         sCallbacks[sCount].callback = JSVAL_NULL;
     427             :         
     428             : #if DEBUG_FCB_SIMPLE_TRACKING_IDS
     429             :         if (sCount == 0)
     430             :         {
     431             :                 OOLog(@"script.frameCallback.debug.reset", @"All frame callbacks removed, resetting next ID to 1.");
     432             :                 sNextID = 1;
     433             :         }
     434             : #endif
     435             : }
     436             : 
     437             : 
     438           0 : static void QueueDeferredOperation(NSString *opType, uint32 trackingID, OOJSValue *value)
     439             : {
     440             :         NSCAssert1(sRunning, @"%s can only be called while frame callbacks are running.", __PRETTY_FUNCTION__);
     441             :         
     442             :         if (sDeferredOps == nil)  sDeferredOps = [[NSMutableArray alloc] init];
     443             :         [sDeferredOps addObject:[NSDictionary dictionaryWithObjectsAndKeys:
     444             :                                                          opType, @"operation",
     445             :                                                          [NSNumber numberWithInt:trackingID], @"trackingID",
     446             :                                                          value, @"value",
     447             :                                                          nil]];
     448             : }
     449             : 
     450             : 
     451           0 : static void RunDeferredOperations(JSContext *context)
     452             : {
     453             :         NSDictionary            *operation = nil;
     454             :         NSEnumerator            *operationEnum = nil;
     455             :         
     456             :         FCBLog(@"script.frameCallback.debug.run-deferred", @"Running %lu deferred frame callback operations.", (long)[sDeferredOps count]);
     457             :         FCBLogIndentIf(@"script.frameCallback.debug.run-deferred");
     458             :         
     459             :         for (operationEnum = [sDeferredOps objectEnumerator]; (operation = [operationEnum nextObject]); )
     460             :         {
     461             :                 NSString        *opType = [operation objectForKey:@"operation"];
     462             :                 uint32          trackingID = [operation oo_intForKey:@"trackingID"];
     463             :                 
     464             :                 if ([opType isEqualToString:@"add"])
     465             :                 {
     466             :                         OOJSValue       *callbackObj = [operation objectForKey:@"value"];
     467             :                         NSString        *errorString = nil;
     468             :                         
     469             :                         if (!AddCallback(context, OOJSValueFromNativeObject(context, callbackObj), trackingID, &errorString))
     470             :                         {
     471             :                                 OOLogWARN(@"script.frameCallback.deferredAdd.failed", @"Deferred frame callback insertion failed: %@", errorString);
     472             :                         }
     473             :                 }
     474             :                 else if ([opType isEqualToString:@"remove"])
     475             :                 {
     476             :                         RemoveCallbackWithTrackingID(context, trackingID);
     477             :                 }
     478             :         }
     479             :         
     480             :         FCBLogOutdentIf(@"script.frameCallback.debug.run-deferred");
     481             : }

Generated by: LCOV version 1.14