/* * Copyright (C) 2015 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "Regress141275.h" #import #import #import #if JSC_OBJC_API_ENABLED extern "C" void JSSynchronousGarbageCollectForDebugging(JSContextRef); extern int failed; static const NSUInteger scriptToEvaluate = 50; @interface JSTEvaluator : NSObject - (instancetype)initWithScript:(NSString*)script; - (void)insertSignPostWithCompletion:(void(^)(NSError* error))completionHandler; - (void)evaluateScript:(NSString*)script completion:(void(^)(NSError* error))completionHandler; - (void)evaluateBlock:(void(^)(JSContext* context))evaluationBlock completion:(void(^)(NSError* error))completionHandler; - (void)waitForTasksDoneAndReportResults; @end static const NSString* JSTEvaluatorThreadContextKey = @"JSTEvaluatorThreadContextKey"; /* * A JSTEvaluatorThreadContext is kept in the thread dictionary of threads used by JSEvaluator. * * This includes the run loop thread, and any threads used by _jsSourcePerformQueue to execute a task. */ @interface JSTEvaluatorThreadContext : NSObject @property (weak) JSTEvaluator* evaluator; @property (strong) JSContext* jsContext; @end @implementation JSTEvaluatorThreadContext @end /*! * A JSTEvaluatorTask is a single task to be executed. * * JSTEvaluator keeps a list of pending tasks. The run loop thread is repsonsible for feeding pending tasks to the _jsSourcePerformQueue, while respecting sign posts. */ @interface JSTEvaluatorTask : NSObject @property (nonatomic, copy) void (^evaluateBlock)(JSContext* jsContext); @property (nonatomic, copy) void (^completionHandler)(NSError* error); @property (nonatomic, copy) NSError* error; + (instancetype)evaluatorTaskWithEvaluateBlock:(void (^)(JSContext*))block completionHandler:(void (^)(NSError* error))completionBlock; @end @implementation JSTEvaluatorTask + (instancetype)evaluatorTaskWithEvaluateBlock:(void (^)(JSContext*))evaluationBlock completionHandler:(void (^)(NSError* error))completionHandler { JSTEvaluatorTask* task = [self new]; task.evaluateBlock = evaluationBlock; task.completionHandler = completionHandler; return task; } @end @implementation JSTEvaluator { dispatch_queue_t _jsSourcePerformQueue; dispatch_semaphore_t _allScriptsDone; CFRunLoopRef _jsThreadRunLoop; CFRunLoopSourceRef _jsThreadRunLoopSource; JSContext* _jsContext; NSMutableArray* __pendingTasks; } - (instancetype)init { self = [super init]; if (self) { _jsSourcePerformQueue = dispatch_queue_create("JSTEval", DISPATCH_QUEUE_CONCURRENT); _allScriptsDone = dispatch_semaphore_create(0); _jsContext = [JSContext new]; _jsContext.name = @"JSTEval"; __pendingTasks = [NSMutableArray new]; NSThread* jsThread = [[NSThread alloc] initWithTarget:self selector:@selector(_jsThreadMain) object:nil]; [jsThread setName:@"JSTEval"]; [jsThread start]; } return self; } - (instancetype)initWithScript:(NSString*)script { self = [self init]; if (self) { __block NSError* scriptError = nil; dispatch_semaphore_t dsema = dispatch_semaphore_create(0); [self evaluateScript:script completion:^(NSError* error) { scriptError = error; dispatch_semaphore_signal(dsema); }]; dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER); } return self; } - (void)_accessPendingTasksWithBlock:(void(^)(NSMutableArray* pendingTasks))block { @synchronized(self) { block(__pendingTasks); if (__pendingTasks.count > 0) { if (_jsThreadRunLoop && _jsThreadRunLoopSource) { CFRunLoopSourceSignal(_jsThreadRunLoopSource); CFRunLoopWakeUp(_jsThreadRunLoop); } } } } - (void)insertSignPostWithCompletion:(void(^)(NSError* error))completionHandler { [self _accessPendingTasksWithBlock:^(NSMutableArray* pendingTasks) { JSTEvaluatorTask* task = [JSTEvaluatorTask evaluatorTaskWithEvaluateBlock:nil completionHandler:completionHandler]; [pendingTasks addObject:task]; }]; } - (void)evaluateScript:(NSString*)script completion:(void(^)(NSError* error))completionHandler { [self evaluateBlock:^(JSContext* context) { [context evaluateScript:script]; } completion:completionHandler]; } - (void)evaluateBlock:(void(^)(JSContext* context))evaluationBlock completion:(void(^)(NSError* error))completionHandler { NSParameterAssert(evaluationBlock != nil); [self _accessPendingTasksWithBlock:^(NSMutableArray* pendingTasks) { JSTEvaluatorTask* task = [JSTEvaluatorTask evaluatorTaskWithEvaluateBlock:evaluationBlock completionHandler:completionHandler]; [pendingTasks addObject:task]; }]; } - (void)waitForTasksDoneAndReportResults { NSString* passFailString = @"PASSED"; if (!dispatch_semaphore_wait(_allScriptsDone, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC))) { int totalScriptsRun = [_jsContext[@"counter"] toInt32]; if (totalScriptsRun != scriptToEvaluate) { passFailString = @"FAILED"; failed = 1; } NSLog(@" Ran a total of %d scripts: %@", totalScriptsRun, passFailString); } else { passFailString = @"FAILED"; failed = 1; NSLog(@" Error, timeout waiting for all tasks to complete: %@", passFailString); } } static void __JSTRunLoopSourceScheduleCallBack(void* info, CFRunLoopRef rl, CFStringRef) { @autoreleasepool { [(__bridge JSTEvaluator*)info _sourceScheduledOnRunLoop:rl]; } } static void __JSTRunLoopSourcePerformCallBack(void* info ) { @autoreleasepool { [(__bridge JSTEvaluator*)info _sourcePerform]; } } static void __JSTRunLoopSourceCancelCallBack(void* info, CFRunLoopRef rl, CFStringRef) { @autoreleasepool { [(__bridge JSTEvaluator*)info _sourceCanceledOnRunLoop:rl]; } } - (void)_jsThreadMain { @autoreleasepool { const CFIndex kRunLoopSourceContextVersion = 0; CFRunLoopSourceContext sourceContext = { kRunLoopSourceContextVersion, (__bridge void*)(self), NULL, NULL, NULL, NULL, NULL, __JSTRunLoopSourceScheduleCallBack, __JSTRunLoopSourceCancelCallBack, __JSTRunLoopSourcePerformCallBack }; @synchronized(self) { _jsThreadRunLoop = CFRunLoopGetCurrent(); CFRetain(_jsThreadRunLoop); _jsThreadRunLoopSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &sourceContext); CFRunLoopAddSource(_jsThreadRunLoop, _jsThreadRunLoopSource, kCFRunLoopDefaultMode); } CFRunLoopRun(); @synchronized(self) { NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary]; [threadDict removeObjectForKey:threadDict[JSTEvaluatorThreadContextKey]]; CFRelease(_jsThreadRunLoopSource); _jsThreadRunLoopSource = NULL; CFRelease(_jsThreadRunLoop); _jsThreadRunLoop = NULL; __pendingTasks = nil; } } } - (void)_sourceScheduledOnRunLoop:(CFRunLoopRef)runLoop { UNUSED_PARAM(runLoop); assert([[[NSThread currentThread] name] isEqualToString:@"JSTEval"]); // Wake up the run loop in case requests were submitted prior to the // run loop & run loop source getting created. CFRunLoopSourceSignal(_jsThreadRunLoopSource); CFRunLoopWakeUp(_jsThreadRunLoop); } - (void)_setupEvaluatorThreadContextIfNeeded { NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary]; JSTEvaluatorThreadContext* context = threadDict[JSTEvaluatorThreadContextKey]; // The evaluator may be other evualuator, or nil if this thread has not been used before. Eaither way take ownership. if (context.evaluator != self) { context = [JSTEvaluatorThreadContext new]; context.evaluator = self; threadDict[JSTEvaluatorThreadContextKey] = context; } } - (void)_callCompletionHandler:(void(^)(NSError* error))completionHandler ifNeededWithError:(NSError*)error { if (completionHandler) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ completionHandler(error); }); } } - (void)_sourcePerform { assert([[[NSThread currentThread] name] isEqualToString:@"JSTEval"]); __block NSArray* tasks = nil; [self _accessPendingTasksWithBlock:^(NSMutableArray* pendingTasks) { // No signpost, take all tasks. tasks = [pendingTasks copy]; [pendingTasks removeAllObjects]; }]; if (tasks.count > 0) { for (JSTEvaluatorTask* task in tasks) { dispatch_block_t block = ^{ NSError* error = nil; if (task.evaluateBlock) { [self _setupEvaluatorThreadContextIfNeeded]; task.evaluateBlock(_jsContext); if (_jsContext.exception) { NSLog(@"Did fail on JSContext: %@", _jsContext.name); NSDictionary* userInfo = @{ NSLocalizedDescriptionKey : [_jsContext.exception[@"message"] toString] }; error = [NSError errorWithDomain:@"JSTEvaluator" code:1 userInfo:userInfo]; _jsContext.exception = nil; } } [self _callCompletionHandler:task.completionHandler ifNeededWithError:error]; }; if (task.evaluateBlock) dispatch_async(_jsSourcePerformQueue, block); else dispatch_barrier_async(_jsSourcePerformQueue, block); } dispatch_barrier_sync(_jsSourcePerformQueue, ^{ if ([_jsContext[@"counter"] toInt32] == scriptToEvaluate) dispatch_semaphore_signal(_allScriptsDone); }); } } - (void)_sourceCanceledOnRunLoop:(CFRunLoopRef)runLoop { UNUSED_PARAM(runLoop); assert([[[NSThread currentThread] name] isEqualToString:@"JSTEval"]); @synchronized(self) { assert(_jsThreadRunLoop); assert(_jsThreadRunLoopSource); CFRunLoopRemoveSource(_jsThreadRunLoop, _jsThreadRunLoopSource, kCFRunLoopDefaultMode); CFRunLoopStop(_jsThreadRunLoop); } } @end void runRegress141275() { // Test that we can execute the same script from multiple threads with a shared context. // See NSLog(@"TEST: Testing multiple threads executing the same script with a shared context"); @autoreleasepool { JSTEvaluator* evaluator = [[JSTEvaluator alloc] initWithScript:@"this['counter'] = 0;"]; void (^showErrorIfNeeded)(NSError* error) = ^(NSError* error) { if (error) { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"Error: %@", error); }); } }; [evaluator evaluateBlock:^(JSContext* context) { JSSynchronousGarbageCollectForDebugging([context JSGlobalContextRef]); } completion:showErrorIfNeeded]; [evaluator evaluateBlock:^(JSContext* context) { context[@"wait"] = ^{ [NSThread sleepForTimeInterval:0.01]; }; } completion:^(NSError* error) { if (error) { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"Error: %@", error); }); } for (unsigned i = 0; i < scriptToEvaluate; i++) [evaluator evaluateScript:@"this['counter']++; this['wait']();" completion:showErrorIfNeeded]; }]; [evaluator waitForTasksDoneAndReportResults]; } } #endif // JSC_OBJC_API_ENABLED