Reland r216808, underlying lldb bug has been fixed.
[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 <chrono>
35 #include <wtf/Atomics.h>
36 #include <wtf/Condition.h>
37 #include <wtf/CurrentTime.h>
38 #include <wtf/Lock.h>
39 #include <wtf/text/StringBuilder.h>
40
41 #if HAVE(MACH_EXCEPTIONS)
42 #include <dispatch/dispatch.h>
43 #endif
44
45 using namespace std::chrono;
46 using JSC::Options;
47
48 static JSGlobalContextRef context = nullptr;
49
50 static JSValueRef currentCPUTimeAsJSFunctionCallback(JSContextRef ctx, JSObjectRef functionObject, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
51 {
52     UNUSED_PARAM(functionObject);
53     UNUSED_PARAM(thisObject);
54     UNUSED_PARAM(argumentCount);
55     UNUSED_PARAM(arguments);
56     UNUSED_PARAM(exception);
57     
58     ASSERT(JSContextGetGlobalContext(ctx) == context);
59     return JSValueMakeNumber(ctx, currentCPUTime().count() / 1000000.);
60 }
61
62 bool shouldTerminateCallbackWasCalled = false;
63 static bool shouldTerminateCallback(JSContextRef, void*)
64 {
65     shouldTerminateCallbackWasCalled = true;
66     return true;
67 }
68
69 bool cancelTerminateCallbackWasCalled = false;
70 static bool cancelTerminateCallback(JSContextRef, void*)
71 {
72     cancelTerminateCallbackWasCalled = true;
73     return false;
74 }
75
76 int extendTerminateCallbackCalled = 0;
77 static bool extendTerminateCallback(JSContextRef ctx, void*)
78 {
79     extendTerminateCallbackCalled++;
80     if (extendTerminateCallbackCalled == 1) {
81         JSContextGroupRef contextGroup = JSContextGetGroup(ctx);
82         JSContextGroupSetExecutionTimeLimit(contextGroup, .200f, extendTerminateCallback, 0);
83         return false;
84     }
85     return true;
86 }
87
88 #if HAVE(MACH_EXCEPTIONS)
89 bool dispatchTerminateCallbackCalled = false;
90 static bool dispatchTermitateCallback(JSContextRef, void*)
91 {
92     dispatchTerminateCallbackCalled = true;
93     return true;
94 }
95 #endif
96
97 struct TierOptions {
98     const char* tier;
99     unsigned timeLimitAdjustmentMillis;
100     const char* optionsStr;
101 };
102
103 static void testResetAfterTimeout(bool& failed)
104 {
105     JSValueRef v = nullptr;
106     JSValueRef exception = nullptr;
107     const char* reentryScript = "100";
108     JSStringRef script = JSStringCreateWithUTF8CString(reentryScript);
109     v = JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
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
119 int testExecutionTimeLimit()
120 {
121     static const TierOptions tierOptionsList[] = {
122         { "LLINT",    0,   "--useConcurrentJIT=false --useLLInt=true --useJIT=false" },
123         { "Baseline", 0,   "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=false" },
124         { "DFG",      0,   "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=true --useFTLJIT=false" },
125         { "FTL",      200, "--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         unsigned tierAdjustmentMillis = tierOptions.timeLimitAdjustmentMillis;
140         double 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 timeout: */
156         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
157         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, shouldTerminateCallback, 0);
158         {
159             unsigned timeAfterWatchdogShouldHaveFired = 300 + tierAdjustmentMillis;
160
161             StringBuilder scriptBuilder;
162             scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
163             scriptBuilder.appendNumber(timeAfterWatchdogShouldHaveFired / 1000.0);
164             scriptBuilder.appendLiteral(") break; } } foo();");
165
166             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
167             exception = nullptr;
168             shouldTerminateCallbackWasCalled = false;
169             auto startTime = currentCPUTime();
170             JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
171             auto endTime = currentCPUTime();
172
173             if (((endTime - startTime) < milliseconds(timeAfterWatchdogShouldHaveFired)) && shouldTerminateCallbackWasCalled)
174                 printf("PASS: %s script timed out as expected.\n", tierOptions.tier);
175             else {
176                 if ((endTime - startTime) >= milliseconds(timeAfterWatchdogShouldHaveFired))
177                     printf("FAIL: %s script did not time out as expected.\n", tierOptions.tier);
178                 if (!shouldTerminateCallbackWasCalled)
179                     printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
180                 failed = true;
181             }
182             
183             if (!exception) {
184                 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
185                 failed = true;
186             }
187
188             testResetAfterTimeout(failed);
189         }
190
191         /* Test script timeout with tail calls: */
192         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
193         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, shouldTerminateCallback, 0);
194         {
195             unsigned timeAfterWatchdogShouldHaveFired = 300 + tierAdjustmentMillis;
196
197             StringBuilder scriptBuilder;
198             scriptBuilder.appendLiteral("var startTime = currentCPUTime();"
199                                  "function recurse(i) {"
200                                      "'use strict';"
201                                      "if (i % 1000 === 0) {"
202                                         "if (currentCPUTime() - startTime >");
203             scriptBuilder.appendNumber(timeAfterWatchdogShouldHaveFired / 1000.0);
204             scriptBuilder.appendLiteral("       ) { return; }");
205             scriptBuilder.appendLiteral("    }");
206             scriptBuilder.appendLiteral("    return recurse(i + 1); }");
207             scriptBuilder.appendLiteral("recurse(0);");
208
209             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
210             exception = nullptr;
211             shouldTerminateCallbackWasCalled = false;
212             auto startTime = currentCPUTime();
213             JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
214             auto endTime = currentCPUTime();
215
216             if (((endTime - startTime) < milliseconds(timeAfterWatchdogShouldHaveFired)) && shouldTerminateCallbackWasCalled)
217                 printf("PASS: %s script with infinite tail calls timed out as expected .\n", tierOptions.tier);
218             else {
219                 if ((endTime - startTime) >= milliseconds(timeAfterWatchdogShouldHaveFired))
220                     printf("FAIL: %s script with infinite tail calls did not time out as expected.\n", tierOptions.tier);
221                 if (!shouldTerminateCallbackWasCalled)
222                     printf("FAIL: %s script with infinite tail calls' timeout callback was not called.\n", tierOptions.tier);
223                 failed = true;
224             }
225             
226             if (!exception) {
227                 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
228                 failed = true;
229             }
230
231             testResetAfterTimeout(failed);
232         }
233
234         /* Test the script timeout's TerminatedExecutionException should NOT be catchable: */
235         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
236         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, shouldTerminateCallback, 0);
237         {
238             unsigned timeAfterWatchdogShouldHaveFired = 300 + tierAdjustmentMillis;
239             
240             StringBuilder scriptBuilder;
241             scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); try { while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
242             scriptBuilder.appendNumber(timeAfterWatchdogShouldHaveFired / 1000.0);
243             scriptBuilder.appendLiteral(") break; } } catch(e) { } } foo();");
244
245             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
246             exception = nullptr;
247             shouldTerminateCallbackWasCalled = false;
248
249             auto startTime = currentCPUTime();
250             JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
251             auto endTime = currentCPUTime();
252             
253             if (((endTime - startTime) >= milliseconds(timeAfterWatchdogShouldHaveFired)) || !shouldTerminateCallbackWasCalled) {
254                 if (!((endTime - startTime) < milliseconds(timeAfterWatchdogShouldHaveFired)))
255                     printf("FAIL: %s script did not time out as expected.\n", tierOptions.tier);
256                 if (!shouldTerminateCallbackWasCalled)
257                     printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
258                 failed = true;
259             }
260             
261             if (exception)
262                 printf("PASS: %s TerminatedExecutionException was not catchable as expected.\n", tierOptions.tier);
263             else {
264                 printf("FAIL: %s TerminatedExecutionException was caught.\n", tierOptions.tier);
265                 failed = true;
266             }
267
268             testResetAfterTimeout(failed);
269         }
270         
271         /* Test script timeout with no callback: */
272         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
273         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, 0, 0);
274         {
275             unsigned timeAfterWatchdogShouldHaveFired = 300 + tierAdjustmentMillis;
276             
277             StringBuilder scriptBuilder;
278             scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
279             scriptBuilder.appendNumber(timeAfterWatchdogShouldHaveFired / 1000.0);
280             scriptBuilder.appendLiteral(") break; } } foo();");
281             
282             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
283             exception = nullptr;
284             shouldTerminateCallbackWasCalled = false;
285
286             auto startTime = currentCPUTime();
287             JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
288             auto endTime = currentCPUTime();
289             
290             if (((endTime - startTime) < milliseconds(timeAfterWatchdogShouldHaveFired)) && !shouldTerminateCallbackWasCalled)
291                 printf("PASS: %s script timed out as expected when no callback is specified.\n", tierOptions.tier);
292             else {
293                 if ((endTime - startTime) >= milliseconds(timeAfterWatchdogShouldHaveFired))
294                     printf("FAIL: %s script did not time out as expected when no callback is specified.\n", tierOptions.tier);
295                 else
296                     printf("FAIL: %s script called stale callback function.\n", tierOptions.tier);
297                 failed = true;
298             }
299             
300             if (!exception) {
301                 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
302                 failed = true;
303             }
304
305             testResetAfterTimeout(failed);
306         }
307         
308         /* Test script timeout cancellation: */
309         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
310         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, cancelTerminateCallback, 0);
311         {
312             unsigned timeAfterWatchdogShouldHaveFired = 300 + tierAdjustmentMillis;
313             
314             StringBuilder scriptBuilder;
315             scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
316             scriptBuilder.appendNumber(timeAfterWatchdogShouldHaveFired / 1000.0);
317             scriptBuilder.appendLiteral(") break; } } foo();");
318
319             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
320             exception = nullptr;
321             cancelTerminateCallbackWasCalled = false;
322
323             auto startTime = currentCPUTime();
324             JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
325             auto endTime = currentCPUTime();
326             
327             if (((endTime - startTime) >= milliseconds(timeAfterWatchdogShouldHaveFired)) && cancelTerminateCallbackWasCalled && !exception)
328                 printf("PASS: %s script timeout was cancelled as expected.\n", tierOptions.tier);
329             else {
330                 if (((endTime - startTime) < milliseconds(timeAfterWatchdogShouldHaveFired)) || exception)
331                     printf("FAIL: %s script timeout was not cancelled.\n", tierOptions.tier);
332                 if (!cancelTerminateCallbackWasCalled)
333                     printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
334                 failed = true;
335             }
336             
337             if (exception) {
338                 printf("FAIL: %s Unexpected TerminatedExecutionException thrown.\n", tierOptions.tier);
339                 failed = true;
340             }
341         }
342         
343         /* Test script timeout extension: */
344         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
345         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, extendTerminateCallback, 0);
346         {
347             unsigned timeBeforeExtendedDeadline = 250 + tierAdjustmentMillis;
348             unsigned timeAfterExtendedDeadline = 600 + tierAdjustmentMillis;
349             unsigned maxBusyLoopTime = 750 + tierAdjustmentMillis;
350
351             StringBuilder scriptBuilder;
352             scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
353             scriptBuilder.appendNumber(maxBusyLoopTime / 1000.0); // in seconds.
354             scriptBuilder.appendLiteral(") break; } } foo();");
355
356             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
357             exception = nullptr;
358             extendTerminateCallbackCalled = 0;
359
360             auto startTime = currentCPUTime();
361             JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
362             auto endTime = currentCPUTime();
363             auto deltaTime = endTime - startTime;
364             
365             if ((deltaTime >= milliseconds(timeBeforeExtendedDeadline)) && (deltaTime < milliseconds(timeAfterExtendedDeadline)) && (extendTerminateCallbackCalled == 2) && exception)
366                 printf("PASS: %s script timeout was extended as expected.\n", tierOptions.tier);
367             else {
368                 if (deltaTime < milliseconds(timeBeforeExtendedDeadline))
369                     printf("FAIL: %s script timeout was not extended as expected.\n", tierOptions.tier);
370                 else if (deltaTime >= milliseconds(timeAfterExtendedDeadline))
371                     printf("FAIL: %s script did not timeout.\n", tierOptions.tier);
372                 
373                 if (extendTerminateCallbackCalled < 1)
374                     printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
375                 if (extendTerminateCallbackCalled < 2)
376                     printf("FAIL: %s script timeout callback was not called after timeout extension.\n", tierOptions.tier);
377                 
378                 if (!exception)
379                     printf("FAIL: %s TerminatedExecutionException was not thrown during timeout extension test.\n", tierOptions.tier);
380                 
381                 failed = true;
382             }
383         }
384
385 #if HAVE(MACH_EXCEPTIONS)
386         /* Test script timeout from dispatch queue: */
387         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
388         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, dispatchTermitateCallback, 0);
389         {
390             unsigned timeAfterWatchdogShouldHaveFired = 300 + tierAdjustmentMillis;
391
392             StringBuilder scriptBuilder;
393             scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
394             scriptBuilder.appendNumber(timeAfterWatchdogShouldHaveFired / 1000.0);
395             scriptBuilder.appendLiteral(") break; } } foo();");
396
397             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
398             exception = nullptr;
399             dispatchTerminateCallbackCalled = false;
400
401             // We have to do this since blocks can only capture things as const.
402             JSGlobalContextRef& contextRef = context;
403             JSStringRef& scriptRef = script;
404             JSValueRef& exceptionRef = exception;
405
406             Lock syncLock;
407             Lock& syncLockRef = syncLock;
408             Condition synchronize;
409             Condition& synchronizeRef = synchronize;
410             bool didSynchronize = false;
411             bool& didSynchronizeRef = didSynchronize;
412
413             std::chrono::microseconds startTime;
414             std::chrono::microseconds endTime;
415
416             std::chrono::microseconds& startTimeRef = startTime;
417             std::chrono::microseconds& endTimeRef = endTime;
418
419             dispatch_group_t group = dispatch_group_create();
420             dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
421                 startTimeRef = currentCPUTime();
422                 JSEvaluateScript(contextRef, scriptRef, nullptr, nullptr, 1, &exceptionRef);
423                 endTimeRef = currentCPUTime();
424                 auto locker = WTF::holdLock(syncLockRef);
425                 didSynchronizeRef = true;
426                 synchronizeRef.notifyAll();
427             });
428
429             auto locker = holdLock(syncLock);
430             synchronize.wait(syncLock, [&] { return didSynchronize; });
431
432             if (((endTime - startTime) < milliseconds(timeAfterWatchdogShouldHaveFired)) && dispatchTerminateCallbackCalled)
433                 printf("PASS: %s script on dispatch queue timed out as expected.\n", tierOptions.tier);
434             else {
435                 if ((endTime - startTime) >= milliseconds(timeAfterWatchdogShouldHaveFired))
436                     printf("FAIL: %s script on dispatch queue did not time out as expected.\n", tierOptions.tier);
437                 if (!shouldTerminateCallbackWasCalled)
438                     printf("FAIL: %s script on dispatch queue timeout callback was not called.\n", tierOptions.tier);
439                 failed = true;
440             }
441         }
442 #endif
443
444         JSGlobalContextRelease(context);
445
446         Options::setOptions(savedOptionsBuilder.toString().ascii().data());
447     }
448     
449     return failed;
450 }