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