Regress141275.mm   [plain text]

/*
* 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 <Foundation/Foundation.h>
#import <objc/objc.h>
#import <objc/runtime.h>

#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 <https://webkit.org/b/141275>
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