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 : }
|