]>
Commit | Line | Data |
---|---|---|
ed1e77d3 A |
1 | /* |
2 | * Copyright (C) 2015 Apple Inc. All rights reserved. | |
3 | * | |
4 | * Redistribution and use in source and binary forms, with or without | |
5 | * modification, are permitted provided that the following conditions | |
6 | * are met: | |
7 | * 1. Redistributions of source code must retain the above copyright | |
8 | * notice, this list of conditions and the following disclaimer. | |
9 | * 2. Redistributions in binary form must reproduce the above copyright | |
10 | * notice, this list of conditions and the following disclaimer in the | |
11 | * documentation and/or other materials provided with the distribution. | |
12 | * | |
13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' | |
14 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, | |
15 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
16 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS | |
17 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
19 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
20 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
21 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
22 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF | |
23 | * THE POSSIBILITY OF SUCH DAMAGE. | |
24 | */ | |
25 | ||
26 | #import "config.h" | |
27 | #import "Regress141275.h" | |
28 | ||
29 | #import <Foundation/Foundation.h> | |
30 | #import <objc/objc.h> | |
31 | #import <objc/runtime.h> | |
32 | ||
33 | #if JSC_OBJC_API_ENABLED | |
34 | ||
35 | extern "C" void JSSynchronousGarbageCollectForDebugging(JSContextRef); | |
36 | ||
37 | extern int failed; | |
38 | ||
39 | static const NSUInteger scriptToEvaluate = 50; | |
40 | ||
41 | @interface JSTEvaluator : NSObject | |
42 | - (instancetype)initWithScript:(NSString*)script; | |
43 | ||
44 | - (void)insertSignPostWithCompletion:(void(^)(NSError* error))completionHandler; | |
45 | ||
46 | - (void)evaluateScript:(NSString*)script completion:(void(^)(NSError* error))completionHandler; | |
47 | - (void)evaluateBlock:(void(^)(JSContext* context))evaluationBlock completion:(void(^)(NSError* error))completionHandler; | |
48 | ||
49 | - (void)waitForTasksDoneAndReportResults; | |
50 | @end | |
51 | ||
52 | ||
53 | static const NSString* JSTEvaluatorThreadContextKey = @"JSTEvaluatorThreadContextKey"; | |
54 | ||
55 | /* | |
56 | * A JSTEvaluatorThreadContext is kept in the thread dictionary of threads used by JSEvaluator. | |
57 | * | |
58 | * This includes the run loop thread, and any threads used by _jsSourcePerformQueue to execute a task. | |
59 | */ | |
60 | @interface JSTEvaluatorThreadContext : NSObject | |
61 | @property (weak) JSTEvaluator* evaluator; | |
62 | @property (strong) JSContext* jsContext; | |
63 | @end | |
64 | ||
65 | @implementation JSTEvaluatorThreadContext | |
66 | @end | |
67 | ||
68 | ||
69 | /*! | |
70 | * A JSTEvaluatorTask is a single task to be executed. | |
71 | * | |
72 | * JSTEvaluator keeps a list of pending tasks. The run loop thread is repsonsible for feeding pending tasks to the _jsSourcePerformQueue, while respecting sign posts. | |
73 | */ | |
74 | @interface JSTEvaluatorTask : NSObject | |
75 | ||
76 | @property (nonatomic, copy) void (^evaluateBlock)(JSContext* jsContext); | |
77 | @property (nonatomic, copy) void (^completionHandler)(NSError* error); | |
78 | @property (nonatomic, copy) NSError* error; | |
79 | ||
80 | + (instancetype)evaluatorTaskWithEvaluateBlock:(void (^)(JSContext*))block completionHandler:(void (^)(NSError* error))completionBlock; | |
81 | ||
82 | @end | |
83 | ||
84 | @implementation JSTEvaluatorTask | |
85 | ||
86 | + (instancetype)evaluatorTaskWithEvaluateBlock:(void (^)(JSContext*))evaluationBlock completionHandler:(void (^)(NSError* error))completionHandler | |
87 | { | |
88 | JSTEvaluatorTask* task = [self new]; | |
89 | task.evaluateBlock = evaluationBlock; | |
90 | task.completionHandler = completionHandler; | |
91 | return task; | |
92 | } | |
93 | ||
94 | @end | |
95 | ||
96 | @implementation JSTEvaluator { | |
97 | dispatch_queue_t _jsSourcePerformQueue; | |
98 | dispatch_semaphore_t _allScriptsDone; | |
99 | CFRunLoopRef _jsThreadRunLoop; | |
100 | CFRunLoopSourceRef _jsThreadRunLoopSource; | |
101 | JSContext* _jsContext; | |
102 | NSMutableArray* __pendingTasks; | |
103 | } | |
104 | ||
105 | - (instancetype)init | |
106 | { | |
107 | self = [super init]; | |
108 | if (self) { | |
109 | _jsSourcePerformQueue = dispatch_queue_create("JSTEval", DISPATCH_QUEUE_CONCURRENT); | |
110 | ||
111 | _allScriptsDone = dispatch_semaphore_create(0); | |
112 | ||
113 | _jsContext = [JSContext new]; | |
114 | _jsContext.name = @"JSTEval"; | |
115 | __pendingTasks = [NSMutableArray new]; | |
116 | ||
117 | NSThread* jsThread = [[NSThread alloc] initWithTarget:self selector:@selector(_jsThreadMain) object:nil]; | |
118 | [jsThread setName:@"JSTEval"]; | |
119 | [jsThread start]; | |
120 | ||
121 | } | |
122 | return self; | |
123 | } | |
124 | ||
125 | - (instancetype)initWithScript:(NSString*)script | |
126 | { | |
127 | self = [self init]; | |
128 | if (self) { | |
129 | __block NSError* scriptError = nil; | |
130 | dispatch_semaphore_t dsema = dispatch_semaphore_create(0); | |
131 | [self evaluateScript:script | |
132 | completion:^(NSError* error) { | |
133 | scriptError = error; | |
134 | dispatch_semaphore_signal(dsema); | |
135 | }]; | |
136 | dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER); | |
137 | } | |
138 | return self; | |
139 | } | |
140 | ||
141 | - (void)_accessPendingTasksWithBlock:(void(^)(NSMutableArray* pendingTasks))block | |
142 | { | |
143 | @synchronized(self) { | |
144 | block(__pendingTasks); | |
145 | if (__pendingTasks.count > 0) { | |
146 | if (_jsThreadRunLoop && _jsThreadRunLoopSource) { | |
147 | CFRunLoopSourceSignal(_jsThreadRunLoopSource); | |
148 | CFRunLoopWakeUp(_jsThreadRunLoop); | |
149 | } | |
150 | } | |
151 | } | |
152 | } | |
153 | ||
154 | - (void)insertSignPostWithCompletion:(void(^)(NSError* error))completionHandler | |
155 | { | |
156 | [self _accessPendingTasksWithBlock:^(NSMutableArray* pendingTasks) { | |
157 | JSTEvaluatorTask* task = [JSTEvaluatorTask evaluatorTaskWithEvaluateBlock:nil | |
158 | completionHandler:completionHandler]; | |
159 | ||
160 | [pendingTasks addObject:task]; | |
161 | }]; | |
162 | } | |
163 | ||
164 | - (void)evaluateScript:(NSString*)script completion:(void(^)(NSError* error))completionHandler | |
165 | { | |
166 | [self evaluateBlock:^(JSContext* context) { | |
167 | [context evaluateScript:script]; | |
168 | } completion:completionHandler]; | |
169 | } | |
170 | ||
171 | - (void)evaluateBlock:(void(^)(JSContext* context))evaluationBlock completion:(void(^)(NSError* error))completionHandler | |
172 | { | |
173 | NSParameterAssert(evaluationBlock != nil); | |
174 | [self _accessPendingTasksWithBlock:^(NSMutableArray* pendingTasks) { | |
175 | JSTEvaluatorTask* task = [JSTEvaluatorTask evaluatorTaskWithEvaluateBlock:evaluationBlock | |
176 | completionHandler:completionHandler]; | |
177 | ||
178 | [pendingTasks addObject:task]; | |
179 | }]; | |
180 | } | |
181 | ||
182 | - (void)waitForTasksDoneAndReportResults | |
183 | { | |
184 | NSString* passFailString = @"PASSED"; | |
185 | ||
186 | if (!dispatch_semaphore_wait(_allScriptsDone, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC))) { | |
187 | int totalScriptsRun = [_jsContext[@"counter"] toInt32]; | |
188 | ||
189 | if (totalScriptsRun != scriptToEvaluate) { | |
190 | passFailString = @"FAILED"; | |
191 | failed = 1; | |
192 | } | |
193 | ||
194 | NSLog(@" Ran a total of %d scripts: %@", totalScriptsRun, passFailString); | |
195 | } else { | |
196 | passFailString = @"FAILED"; | |
197 | failed = 1; | |
198 | NSLog(@" Error, timeout waiting for all tasks to complete: %@", passFailString); | |
199 | } | |
200 | } | |
201 | ||
202 | static void __JSTRunLoopSourceScheduleCallBack(void* info, CFRunLoopRef rl, CFStringRef) | |
203 | { | |
204 | @autoreleasepool { | |
205 | [(__bridge JSTEvaluator*)info _sourceScheduledOnRunLoop:rl]; | |
206 | } | |
207 | } | |
208 | ||
209 | static void __JSTRunLoopSourcePerformCallBack(void* info ) | |
210 | { | |
211 | @autoreleasepool { | |
212 | [(__bridge JSTEvaluator*)info _sourcePerform]; | |
213 | } | |
214 | } | |
215 | ||
216 | static void __JSTRunLoopSourceCancelCallBack(void* info, CFRunLoopRef rl, CFStringRef) | |
217 | { | |
218 | @autoreleasepool { | |
219 | [(__bridge JSTEvaluator*)info _sourceCanceledOnRunLoop:rl]; | |
220 | } | |
221 | } | |
222 | ||
223 | - (void)_jsThreadMain | |
224 | { | |
225 | @autoreleasepool { | |
226 | const CFIndex kRunLoopSourceContextVersion = 0; | |
227 | CFRunLoopSourceContext sourceContext = { | |
228 | kRunLoopSourceContextVersion, (__bridge void*)(self), | |
229 | NULL, NULL, NULL, NULL, NULL, | |
230 | __JSTRunLoopSourceScheduleCallBack, | |
231 | __JSTRunLoopSourceCancelCallBack, | |
232 | __JSTRunLoopSourcePerformCallBack | |
233 | }; | |
234 | ||
235 | @synchronized(self) { | |
236 | _jsThreadRunLoop = CFRunLoopGetCurrent(); | |
237 | CFRetain(_jsThreadRunLoop); | |
238 | ||
239 | _jsThreadRunLoopSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &sourceContext); | |
240 | CFRunLoopAddSource(_jsThreadRunLoop, _jsThreadRunLoopSource, kCFRunLoopDefaultMode); | |
241 | } | |
242 | ||
243 | CFRunLoopRun(); | |
244 | ||
245 | @synchronized(self) { | |
246 | NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary]; | |
247 | [threadDict removeObjectForKey:threadDict[JSTEvaluatorThreadContextKey]]; | |
248 | ||
249 | CFRelease(_jsThreadRunLoopSource); | |
250 | _jsThreadRunLoopSource = NULL; | |
251 | ||
252 | CFRelease(_jsThreadRunLoop); | |
253 | _jsThreadRunLoop = NULL; | |
254 | ||
255 | __pendingTasks = nil; | |
256 | } | |
257 | } | |
258 | } | |
259 | ||
260 | - (void)_sourceScheduledOnRunLoop:(CFRunLoopRef)runLoop | |
261 | { | |
262 | UNUSED_PARAM(runLoop); | |
263 | assert([[[NSThread currentThread] name] isEqualToString:@"JSTEval"]); | |
264 | ||
265 | // Wake up the run loop in case requests were submitted prior to the | |
266 | // run loop & run loop source getting created. | |
267 | CFRunLoopSourceSignal(_jsThreadRunLoopSource); | |
268 | CFRunLoopWakeUp(_jsThreadRunLoop); | |
269 | } | |
270 | ||
271 | - (void)_setupEvaluatorThreadContextIfNeeded | |
272 | { | |
273 | NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary]; | |
274 | JSTEvaluatorThreadContext* context = threadDict[JSTEvaluatorThreadContextKey]; | |
275 | // The evaluator may be other evualuator, or nil if this thread has not been used before. Eaither way take ownership. | |
276 | if (context.evaluator != self) { | |
277 | context = [JSTEvaluatorThreadContext new]; | |
278 | context.evaluator = self; | |
279 | threadDict[JSTEvaluatorThreadContextKey] = context; | |
280 | } | |
281 | } | |
282 | ||
283 | - (void)_callCompletionHandler:(void(^)(NSError* error))completionHandler ifNeededWithError:(NSError*)error | |
284 | { | |
285 | if (completionHandler) { | |
286 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ | |
287 | completionHandler(error); | |
288 | }); | |
289 | } | |
290 | } | |
291 | ||
292 | - (void)_sourcePerform | |
293 | { | |
294 | assert([[[NSThread currentThread] name] isEqualToString:@"JSTEval"]); | |
295 | ||
296 | __block NSArray* tasks = nil; | |
297 | [self _accessPendingTasksWithBlock:^(NSMutableArray* pendingTasks) { | |
298 | // No signpost, take all tasks. | |
299 | tasks = [pendingTasks copy]; | |
300 | [pendingTasks removeAllObjects]; | |
301 | }]; | |
302 | ||
303 | if (tasks.count > 0) { | |
304 | for (JSTEvaluatorTask* task in tasks) { | |
305 | dispatch_block_t block = ^{ | |
306 | NSError* error = nil; | |
307 | if (task.evaluateBlock) { | |
308 | [self _setupEvaluatorThreadContextIfNeeded]; | |
309 | task.evaluateBlock(_jsContext); | |
310 | if (_jsContext.exception) { | |
311 | NSLog(@"Did fail on JSContext: %@", _jsContext.name); | |
312 | NSDictionary* userInfo = @{ NSLocalizedDescriptionKey : [_jsContext.exception[@"message"] toString] }; | |
313 | error = [NSError errorWithDomain:@"JSTEvaluator" code:1 userInfo:userInfo]; | |
314 | _jsContext.exception = nil; | |
315 | } | |
316 | } | |
317 | [self _callCompletionHandler:task.completionHandler ifNeededWithError:error]; | |
318 | }; | |
319 | ||
320 | if (task.evaluateBlock) | |
321 | dispatch_async(_jsSourcePerformQueue, block); | |
322 | else | |
323 | dispatch_barrier_async(_jsSourcePerformQueue, block); | |
324 | } | |
325 | ||
326 | dispatch_barrier_sync(_jsSourcePerformQueue, ^{ | |
327 | if ([_jsContext[@"counter"] toInt32] == scriptToEvaluate) | |
328 | dispatch_semaphore_signal(_allScriptsDone); | |
329 | }); | |
330 | } | |
331 | } | |
332 | ||
333 | - (void)_sourceCanceledOnRunLoop:(CFRunLoopRef)runLoop | |
334 | { | |
335 | UNUSED_PARAM(runLoop); | |
336 | assert([[[NSThread currentThread] name] isEqualToString:@"JSTEval"]); | |
337 | ||
338 | @synchronized(self) { | |
339 | assert(_jsThreadRunLoop); | |
340 | assert(_jsThreadRunLoopSource); | |
341 | ||
342 | CFRunLoopRemoveSource(_jsThreadRunLoop, _jsThreadRunLoopSource, kCFRunLoopDefaultMode); | |
343 | CFRunLoopStop(_jsThreadRunLoop); | |
344 | } | |
345 | } | |
346 | ||
347 | @end | |
348 | ||
349 | void runRegress141275() | |
350 | { | |
351 | // Test that we can execute the same script from multiple threads with a shared context. | |
352 | // See <https://webkit.org/b/141275> | |
353 | NSLog(@"TEST: Testing multiple threads executing the same script with a shared context"); | |
354 | ||
355 | @autoreleasepool { | |
356 | JSTEvaluator* evaluator = [[JSTEvaluator alloc] initWithScript:@"this['counter'] = 0;"]; | |
357 | ||
358 | void (^showErrorIfNeeded)(NSError* error) = ^(NSError* error) { | |
359 | if (error) { | |
360 | dispatch_async(dispatch_get_main_queue(), ^{ | |
361 | NSLog(@"Error: %@", error); | |
362 | }); | |
363 | } | |
364 | }; | |
365 | ||
366 | [evaluator evaluateBlock:^(JSContext* context) { | |
367 | JSSynchronousGarbageCollectForDebugging([context JSGlobalContextRef]); | |
368 | } completion:showErrorIfNeeded]; | |
369 | ||
370 | [evaluator evaluateBlock:^(JSContext* context) { | |
371 | context[@"wait"] = ^{ | |
372 | [NSThread sleepForTimeInterval:0.01]; | |
373 | }; | |
374 | } completion:^(NSError* error) { | |
375 | if (error) { | |
376 | dispatch_async(dispatch_get_main_queue(), ^{ | |
377 | NSLog(@"Error: %@", error); | |
378 | }); | |
379 | } | |
380 | for (unsigned i = 0; i < scriptToEvaluate; i++) | |
381 | [evaluator evaluateScript:@"this['counter']++; this['wait']();" completion:showErrorIfNeeded]; | |
382 | }]; | |
383 | ||
384 | [evaluator waitForTasksDoneAndReportResults]; | |
385 | } | |
386 | } | |
387 | ||
388 | #endif // JSC_OBJC_API_ENABLED |