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#include "config.h"
27#include "ExecutionTimeLimitTest.h"
28
29#include "InitializeThreading.h"
30#include "JSContextRefPrivate.h"
31#include "JavaScript.h"
32#include "Options.h"
33
34#include <wtf/Atomics.h>
35#include <wtf/CPUTime.h>
36#include <wtf/Condition.h>
37#include <wtf/Lock.h>
38#include <wtf/Threading.h>
39#include <wtf/text/StringBuilder.h>
40
41#if HAVE(MACH_EXCEPTIONS)
42#include <dispatch/dispatch.h>
43#endif
44
45using JSC::Options;
46
47static JSGlobalContextRef context = nullptr;
48
49static JSValueRef currentCPUTimeAsJSFunctionCallback(JSContextRef ctx, JSObjectRef functionObject, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
50{
51 UNUSED_PARAM(functionObject);
52 UNUSED_PARAM(thisObject);
53 UNUSED_PARAM(argumentCount);
54 UNUSED_PARAM(arguments);
55 UNUSED_PARAM(exception);
56
57 ASSERT(JSContextGetGlobalContext(ctx) == context);
58 return JSValueMakeNumber(ctx, CPUTime::forCurrentThread().seconds());
59}
60
61bool shouldTerminateCallbackWasCalled = false;
62static bool shouldTerminateCallback(JSContextRef, void*)
63{
64 shouldTerminateCallbackWasCalled = true;
65 return true;
66}
67
68bool cancelTerminateCallbackWasCalled = false;
69static bool cancelTerminateCallback(JSContextRef, void*)
70{
71 cancelTerminateCallbackWasCalled = true;
72 return false;
73}
74
75int extendTerminateCallbackCalled = 0;
76static bool extendTerminateCallback(JSContextRef ctx, void*)
77{
78 extendTerminateCallbackCalled++;
79 if (extendTerminateCallbackCalled == 1) {
80 JSContextGroupRef contextGroup = JSContextGetGroup(ctx);
81 JSContextGroupSetExecutionTimeLimit(contextGroup, .200f, extendTerminateCallback, 0);
82 return false;
83 }
84 return true;
85}
86
87#if HAVE(MACH_EXCEPTIONS)
88bool dispatchTerminateCallbackCalled = false;
89static bool dispatchTermitateCallback(JSContextRef, void*)
90{
91 dispatchTerminateCallbackCalled = true;
92 return true;
93}
94#endif
95
96struct TierOptions {
97 const char* tier;
98 Seconds timeLimitAdjustment;
99 const char* optionsStr;
100};
101
102static void testResetAfterTimeout(bool& failed)
103{
104 JSValueRef v = nullptr;
105 JSValueRef exception = nullptr;
106 const char* reentryScript = "100";
107 JSStringRef script = JSStringCreateWithUTF8CString(reentryScript);
108 v = JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
109 JSStringRelease(script);
110 if (exception) {
111 printf("FAIL: Watchdog timeout was not reset.\n");
112 failed = true;
113 } else if (!JSValueIsNumber(context, v) || JSValueToNumber(context, v, nullptr) != 100) {
114 printf("FAIL: Script result is not as expected.\n");
115 failed = true;
116 }
117}
118
119int testExecutionTimeLimit()
120{
121 static const TierOptions tierOptionsList[] = {
122 { "LLINT", 0_ms, "--useConcurrentJIT=false --useLLInt=true --useJIT=false" },
123 { "Baseline", 0_ms, "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=false" },
124 { "DFG", 200_ms, "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=true --useFTLJIT=false" },
125 { "FTL", 500_ms, "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=true --useFTLJIT=true" },
126 };
127
128 bool failed = false;
129
130 JSC::initializeThreading();
131 Options::initialize(); // Ensure options is initialized first.
132
133 for (auto tierOptions : tierOptionsList) {
134 StringBuilder savedOptionsBuilder;
135 Options::dumpAllOptionsInALine(savedOptionsBuilder);
136
137 Options::setOptions(tierOptions.optionsStr);
138
139 Seconds tierAdjustment = tierOptions.timeLimitAdjustment;
140 Seconds timeLimit;
141
142 context = JSGlobalContextCreateInGroup(nullptr, nullptr);
143
144 JSContextGroupRef contextGroup = JSContextGetGroup(context);
145 JSObjectRef globalObject = JSContextGetGlobalObject(context);
146 ASSERT(JSValueIsObject(context, globalObject));
147
148 JSValueRef exception = nullptr;
149
150 JSStringRef currentCPUTimeStr = JSStringCreateWithUTF8CString("currentCPUTime");
151 JSObjectRef currentCPUTimeFunction = JSObjectMakeFunctionWithCallback(context, currentCPUTimeStr, currentCPUTimeAsJSFunctionCallback);
152 JSObjectSetProperty(context, globalObject, currentCPUTimeStr, currentCPUTimeFunction, kJSPropertyAttributeNone, nullptr);
153 JSStringRelease(currentCPUTimeStr);
154
155 /* Test script on another thread: */
156 timeLimit = 100_ms + tierAdjustment;
157 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), shouldTerminateCallback, 0);
158 {
159 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
160
161 JSStringRef script = JSStringCreateWithUTF8CString("function foo() { while (true) { } } foo();");
162 exception = nullptr;
163 JSValueRef* exn = &exception;
164 shouldTerminateCallbackWasCalled = false;
165 auto thread = Thread::create("Rogue thread", [=] {
166 JSEvaluateScript(context, script, nullptr, nullptr, 1, exn);
167 });
168
169 sleep(timeAfterWatchdogShouldHaveFired);
170
171 if (shouldTerminateCallbackWasCalled)
172 printf("PASS: %s script timed out as expected.\n", tierOptions.tier);
173 else {
174 printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
175 exit(1);
176 }
177
178 if (!exception) {
179 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
180 exit(1);
181 }
182
183 thread->waitForCompletion();
184 testResetAfterTimeout(failed);
185
186 JSStringRelease(script);
187 }
188
189 /* Test script timeout: */
190 timeLimit = 100_ms + tierAdjustment;
191 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), shouldTerminateCallback, 0);
192 {
193 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
194
195 StringBuilder scriptBuilder;
196 scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
197 scriptBuilder.appendFixedPrecisionNumber(timeAfterWatchdogShouldHaveFired.seconds());
198 scriptBuilder.appendLiteral(") break; } } foo();");
199
200 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
201 exception = nullptr;
202 shouldTerminateCallbackWasCalled = false;
203 auto startTime = CPUTime::forCurrentThread();
204 JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
205 auto endTime = CPUTime::forCurrentThread();
206 JSStringRelease(script);
207
208 if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) && shouldTerminateCallbackWasCalled)
209 printf("PASS: %s script timed out as expected.\n", tierOptions.tier);
210 else {
211 if ((endTime - startTime) >= timeAfterWatchdogShouldHaveFired)
212 printf("FAIL: %s script did not time out as expected.\n", tierOptions.tier);
213 if (!shouldTerminateCallbackWasCalled)
214 printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
215 failed = true;
216 }
217
218 if (!exception) {
219 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
220 failed = true;
221 }
222
223 testResetAfterTimeout(failed);
224 }
225
226 /* Test script timeout with tail calls: */
227 timeLimit = 100_ms + tierAdjustment;
228 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), shouldTerminateCallback, 0);
229 {
230 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
231
232 StringBuilder scriptBuilder;
233 scriptBuilder.appendLiteral("var startTime = currentCPUTime();"
234 "function recurse(i) {"
235 "'use strict';"
236 "if (i % 1000 === 0) {"
237 "if (currentCPUTime() - startTime >");
238 scriptBuilder.appendFixedPrecisionNumber(timeAfterWatchdogShouldHaveFired.seconds());
239 scriptBuilder.appendLiteral(" ) { return; }");
240 scriptBuilder.appendLiteral(" }");
241 scriptBuilder.appendLiteral(" return recurse(i + 1); }");
242 scriptBuilder.appendLiteral("recurse(0);");
243
244 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
245 exception = nullptr;
246 shouldTerminateCallbackWasCalled = false;
247 auto startTime = CPUTime::forCurrentThread();
248 JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
249 auto endTime = CPUTime::forCurrentThread();
250 JSStringRelease(script);
251
252 if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) && shouldTerminateCallbackWasCalled)
253 printf("PASS: %s script with infinite tail calls timed out as expected .\n", tierOptions.tier);
254 else {
255 if ((endTime - startTime) >= timeAfterWatchdogShouldHaveFired)
256 printf("FAIL: %s script with infinite tail calls did not time out as expected.\n", tierOptions.tier);
257 if (!shouldTerminateCallbackWasCalled)
258 printf("FAIL: %s script with infinite tail calls' timeout callback was not called.\n", tierOptions.tier);
259 failed = true;
260 }
261
262 if (!exception) {
263 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
264 failed = true;
265 }
266
267 testResetAfterTimeout(failed);
268 }
269
270 /* Test the script timeout's TerminatedExecutionException should NOT be catchable: */
271 timeLimit = 100_ms + tierAdjustment;
272 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), shouldTerminateCallback, 0);
273 {
274 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
275
276 StringBuilder scriptBuilder;
277 scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); try { while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
278 scriptBuilder.appendFixedPrecisionNumber(timeAfterWatchdogShouldHaveFired.seconds());
279 scriptBuilder.appendLiteral(") break; } } catch(e) { } } foo();");
280
281 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
282 exception = nullptr;
283 shouldTerminateCallbackWasCalled = false;
284
285 auto startTime = CPUTime::forCurrentThread();
286 JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
287 auto endTime = CPUTime::forCurrentThread();
288
289 JSStringRelease(script);
290
291 if (((endTime - startTime) >= timeAfterWatchdogShouldHaveFired) || !shouldTerminateCallbackWasCalled) {
292 if (!((endTime - startTime) < timeAfterWatchdogShouldHaveFired))
293 printf("FAIL: %s script did not time out as expected.\n", tierOptions.tier);
294 if (!shouldTerminateCallbackWasCalled)
295 printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
296 failed = true;
297 }
298
299 if (exception)
300 printf("PASS: %s TerminatedExecutionException was not catchable as expected.\n", tierOptions.tier);
301 else {
302 printf("FAIL: %s TerminatedExecutionException was caught.\n", tierOptions.tier);
303 failed = true;
304 }
305
306 testResetAfterTimeout(failed);
307 }
308
309 /* Test script timeout with no callback: */
310 timeLimit = 100_ms + tierAdjustment;
311 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), 0, 0);
312 {
313 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
314
315 StringBuilder scriptBuilder;
316 scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
317 scriptBuilder.appendFixedPrecisionNumber(timeAfterWatchdogShouldHaveFired.seconds());
318 scriptBuilder.appendLiteral(") break; } } foo();");
319
320 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
321 exception = nullptr;
322 shouldTerminateCallbackWasCalled = false;
323
324 auto startTime = CPUTime::forCurrentThread();
325 JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
326 auto endTime = CPUTime::forCurrentThread();
327
328 JSStringRelease(script);
329
330 if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) && !shouldTerminateCallbackWasCalled)
331 printf("PASS: %s script timed out as expected when no callback is specified.\n", tierOptions.tier);
332 else {
333 if ((endTime - startTime) >= timeAfterWatchdogShouldHaveFired)
334 printf("FAIL: %s script did not time out as expected when no callback is specified.\n", tierOptions.tier);
335 else
336 printf("FAIL: %s script called stale callback function.\n", tierOptions.tier);
337 failed = true;
338 }
339
340 if (!exception) {
341 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
342 failed = true;
343 }
344
345 testResetAfterTimeout(failed);
346 }
347
348 /* Test script timeout cancellation: */
349 timeLimit = 100_ms + tierAdjustment;
350 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), cancelTerminateCallback, 0);
351 {
352 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
353
354 StringBuilder scriptBuilder;
355 scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
356 scriptBuilder.appendFixedPrecisionNumber(timeAfterWatchdogShouldHaveFired.seconds());
357 scriptBuilder.appendLiteral(") break; } } foo();");
358
359 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
360 exception = nullptr;
361 cancelTerminateCallbackWasCalled = false;
362
363 auto startTime = CPUTime::forCurrentThread();
364 JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
365 auto endTime = CPUTime::forCurrentThread();
366
367 JSStringRelease(script);
368
369 if (((endTime - startTime) >= timeAfterWatchdogShouldHaveFired) && cancelTerminateCallbackWasCalled && !exception)
370 printf("PASS: %s script timeout was cancelled as expected.\n", tierOptions.tier);
371 else {
372 if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) || exception)
373 printf("FAIL: %s script timeout was not cancelled.\n", tierOptions.tier);
374 if (!cancelTerminateCallbackWasCalled)
375 printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
376 failed = true;
377 }
378
379 if (exception) {
380 printf("FAIL: %s Unexpected TerminatedExecutionException thrown.\n", tierOptions.tier);
381 failed = true;
382 }
383 }
384
385 /* Test script timeout extension: */
386 timeLimit = 100_ms + tierAdjustment;
387 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), extendTerminateCallback, 0);
388 {
389 Seconds timeBeforeExtendedDeadline = 250_ms + tierAdjustment;
390 Seconds timeAfterExtendedDeadline = 600_ms + tierAdjustment;
391 Seconds maxBusyLoopTime = 750_ms + tierAdjustment;
392
393 StringBuilder scriptBuilder;
394 scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
395 scriptBuilder.appendFixedPrecisionNumber(maxBusyLoopTime.seconds()); // in seconds.
396 scriptBuilder.appendLiteral(") break; } } foo();");
397
398 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
399 exception = nullptr;
400 extendTerminateCallbackCalled = 0;
401
402 auto startTime = CPUTime::forCurrentThread();
403 JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
404 auto endTime = CPUTime::forCurrentThread();
405 auto deltaTime = endTime - startTime;
406
407 JSStringRelease(script);
408
409 if ((deltaTime >= timeBeforeExtendedDeadline) && (deltaTime < timeAfterExtendedDeadline) && (extendTerminateCallbackCalled == 2) && exception)
410 printf("PASS: %s script timeout was extended as expected.\n", tierOptions.tier);
411 else {
412 if (deltaTime < timeBeforeExtendedDeadline)
413 printf("FAIL: %s script timeout was not extended as expected.\n", tierOptions.tier);
414 else if (deltaTime >= timeAfterExtendedDeadline)
415 printf("FAIL: %s script did not timeout.\n", tierOptions.tier);
416
417 if (extendTerminateCallbackCalled < 1)
418 printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
419 if (extendTerminateCallbackCalled < 2)
420 printf("FAIL: %s script timeout callback was not called after timeout extension.\n", tierOptions.tier);
421
422 if (!exception)
423 printf("FAIL: %s TerminatedExecutionException was not thrown during timeout extension test.\n", tierOptions.tier);
424
425 failed = true;
426 }
427 }
428
429#if HAVE(MACH_EXCEPTIONS)
430 /* Test script timeout from dispatch queue: */
431 timeLimit = 100_ms + tierAdjustment;
432 JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit.seconds(), dispatchTermitateCallback, 0);
433 {
434 Seconds timeAfterWatchdogShouldHaveFired = 300_ms + tierAdjustment;
435
436 StringBuilder scriptBuilder;
437 scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
438 scriptBuilder.appendFixedPrecisionNumber(timeAfterWatchdogShouldHaveFired.seconds());
439 scriptBuilder.appendLiteral(") break; } } foo();");
440
441 JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
442 exception = nullptr;
443 dispatchTerminateCallbackCalled = false;
444
445 // We have to do this since blocks can only capture things as const.
446 JSGlobalContextRef& contextRef = context;
447 JSStringRef& scriptRef = script;
448 JSValueRef& exceptionRef = exception;
449
450 Lock syncLock;
451 Lock& syncLockRef = syncLock;
452 Condition synchronize;
453 Condition& synchronizeRef = synchronize;
454 bool didSynchronize = false;
455 bool& didSynchronizeRef = didSynchronize;
456
457 Seconds startTime;
458 Seconds endTime;
459
460 Seconds& startTimeRef = startTime;
461 Seconds& endTimeRef = endTime;
462
463 dispatch_group_t group = dispatch_group_create();
464 dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
465 startTimeRef = CPUTime::forCurrentThread();
466 JSEvaluateScript(contextRef, scriptRef, nullptr, nullptr, 1, &exceptionRef);
467 endTimeRef = CPUTime::forCurrentThread();
468 auto locker = WTF::holdLock(syncLockRef);
469 didSynchronizeRef = true;
470 synchronizeRef.notifyAll();
471 });
472
473 auto locker = holdLock(syncLock);
474 synchronize.wait(syncLock, [&] { return didSynchronize; });
475
476 if (((endTime - startTime) < timeAfterWatchdogShouldHaveFired) && dispatchTerminateCallbackCalled)
477 printf("PASS: %s script on dispatch queue timed out as expected.\n", tierOptions.tier);
478 else {
479 if ((endTime - startTime) >= timeAfterWatchdogShouldHaveFired)
480 printf("FAIL: %s script on dispatch queue did not time out as expected.\n", tierOptions.tier);
481 if (!shouldTerminateCallbackWasCalled)
482 printf("FAIL: %s script on dispatch queue timeout callback was not called.\n", tierOptions.tier);
483 failed = true;
484 }
485
486 JSStringRelease(script);
487 }
488#endif
489
490 JSGlobalContextRelease(context);
491
492 Options::setOptions(savedOptionsBuilder.toString().ascii().data());
493 }
494
495 return failed;
496}
497