85c272c4437677573e0e750aaac5639ece842d7f
[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 #include <chrono>
34 #include <wtf/CurrentTime.h>
35 #include <wtf/text/StringBuilder.h>
36
37 using namespace std::chrono;
38 using JSC::Options;
39
40 static JSGlobalContextRef context = nullptr;
41
42 static JSValueRef currentCPUTimeAsJSFunctionCallback(JSContextRef ctx, JSObjectRef functionObject, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
43 {
44     UNUSED_PARAM(functionObject);
45     UNUSED_PARAM(thisObject);
46     UNUSED_PARAM(argumentCount);
47     UNUSED_PARAM(arguments);
48     UNUSED_PARAM(exception);
49     
50     ASSERT(JSContextGetGlobalContext(ctx) == context);
51     return JSValueMakeNumber(ctx, currentCPUTime().count() / 1000000.);
52 }
53
54 bool shouldTerminateCallbackWasCalled = false;
55 static bool shouldTerminateCallback(JSContextRef, void*)
56 {
57     shouldTerminateCallbackWasCalled = true;
58     return true;
59 }
60
61 bool cancelTerminateCallbackWasCalled = false;
62 static bool cancelTerminateCallback(JSContextRef, void*)
63 {
64     cancelTerminateCallbackWasCalled = true;
65     return false;
66 }
67
68 int extendTerminateCallbackCalled = 0;
69 static bool extendTerminateCallback(JSContextRef ctx, void*)
70 {
71     extendTerminateCallbackCalled++;
72     if (extendTerminateCallbackCalled == 1) {
73         JSContextGroupRef contextGroup = JSContextGetGroup(ctx);
74         JSContextGroupSetExecutionTimeLimit(contextGroup, .200f, extendTerminateCallback, 0);
75         return false;
76     }
77     return true;
78 }
79
80 struct TierOptions {
81     const char* tier;
82     unsigned timeLimitAdjustmentMillis;
83     const char* optionsStr;
84 };
85
86 static void testResetAfterTimeout(bool& failed)
87 {
88     JSValueRef v = nullptr;
89     JSValueRef exception = nullptr;
90     const char* reentryScript = "100";
91     JSStringRef script = JSStringCreateWithUTF8CString(reentryScript);
92     v = JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
93     if (exception) {
94         printf("FAIL: Watchdog timeout was not reset.\n");
95         failed = true;
96     } else if (!JSValueIsNumber(context, v) || JSValueToNumber(context, v, nullptr) != 100) {
97         printf("FAIL: Script result is not as expected.\n");
98         failed = true;
99     }
100 }
101
102 int testExecutionTimeLimit()
103 {
104     static const TierOptions tierOptionsList[] = {
105         { "LLINT",    0,   "--useConcurrentJIT=false --useLLInt=true --useJIT=false" },
106         { "Baseline", 0,   "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=false" },
107         { "DFG",      0,   "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=true --useFTLJIT=false" },
108         { "FTL",      200, "--useConcurrentJIT=false --useLLInt=true --useJIT=true --useDFGJIT=true --useFTLJIT=true" },
109     };
110     
111     bool failed = false;
112
113     JSC::initializeThreading();
114     Options::initialize(); // Ensure options is initialized first.
115
116     for (auto tierOptions : tierOptionsList) {
117         StringBuilder savedOptionsBuilder;
118         Options::dumpAllOptionsInALine(savedOptionsBuilder);
119
120         Options::setOptions(tierOptions.optionsStr);
121         
122         unsigned tierAdjustmentMillis = tierOptions.timeLimitAdjustmentMillis;
123         double timeLimit;
124
125         context = JSGlobalContextCreateInGroup(nullptr, nullptr);
126
127         JSContextGroupRef contextGroup = JSContextGetGroup(context);
128         JSObjectRef globalObject = JSContextGetGlobalObject(context);
129         ASSERT(JSValueIsObject(context, globalObject));
130
131         JSValueRef exception = nullptr;
132
133         JSStringRef currentCPUTimeStr = JSStringCreateWithUTF8CString("currentCPUTime");
134         JSObjectRef currentCPUTimeFunction = JSObjectMakeFunctionWithCallback(context, currentCPUTimeStr, currentCPUTimeAsJSFunctionCallback);
135         JSObjectSetProperty(context, globalObject, currentCPUTimeStr, currentCPUTimeFunction, kJSPropertyAttributeNone, nullptr);
136         JSStringRelease(currentCPUTimeStr);
137         
138         /* Test script timeout: */
139         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
140         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, shouldTerminateCallback, 0);
141         {
142             unsigned timeAfterWatchdogShouldHaveFired = 300 + tierAdjustmentMillis;
143
144             StringBuilder scriptBuilder;
145             scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
146             scriptBuilder.appendNumber(timeAfterWatchdogShouldHaveFired / 1000.0);
147             scriptBuilder.appendLiteral(") break; } } foo();");
148
149             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
150             exception = nullptr;
151             shouldTerminateCallbackWasCalled = false;
152             auto startTime = currentCPUTime();
153             JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
154             auto endTime = currentCPUTime();
155
156             if (((endTime - startTime) < milliseconds(timeAfterWatchdogShouldHaveFired)) && shouldTerminateCallbackWasCalled)
157                 printf("PASS: %s script timed out as expected.\n", tierOptions.tier);
158             else {
159                 if ((endTime - startTime) >= milliseconds(timeAfterWatchdogShouldHaveFired))
160                     printf("FAIL: %s script did not time out as expected.\n", tierOptions.tier);
161                 if (!shouldTerminateCallbackWasCalled)
162                     printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
163                 failed = true;
164             }
165             
166             if (!exception) {
167                 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
168                 failed = true;
169             }
170
171             testResetAfterTimeout(failed);
172         }
173
174         /* Test script timeout with tail calls: */
175         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
176         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, shouldTerminateCallback, 0);
177         {
178             unsigned timeAfterWatchdogShouldHaveFired = 300 + tierAdjustmentMillis;
179
180             StringBuilder scriptBuilder;
181             scriptBuilder.appendLiteral("var startTime = currentCPUTime();"
182                                  "function recurse(i) {"
183                                      "'use strict';"
184                                      "if (i % 1000 === 0) {"
185                                         "if (currentCPUTime() - startTime >");
186             scriptBuilder.appendNumber(timeAfterWatchdogShouldHaveFired / 1000.0);
187             scriptBuilder.appendLiteral("       ) { return; }");
188             scriptBuilder.appendLiteral("    }");
189             scriptBuilder.appendLiteral("    return recurse(i + 1); }");
190             scriptBuilder.appendLiteral("recurse(0);");
191
192             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
193             exception = nullptr;
194             shouldTerminateCallbackWasCalled = false;
195             auto startTime = currentCPUTime();
196             JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
197             auto endTime = currentCPUTime();
198
199             if (((endTime - startTime) < milliseconds(timeAfterWatchdogShouldHaveFired)) && shouldTerminateCallbackWasCalled)
200                 printf("PASS: %s script with infinite tail calls timed out as expected .\n", tierOptions.tier);
201             else {
202                 if ((endTime - startTime) >= milliseconds(timeAfterWatchdogShouldHaveFired))
203                     printf("FAIL: %s script with infinite tail calls did not time out as expected.\n", tierOptions.tier);
204                 if (!shouldTerminateCallbackWasCalled)
205                     printf("FAIL: %s script with infinite tail calls' timeout callback was not called.\n", tierOptions.tier);
206                 failed = true;
207             }
208             
209             if (!exception) {
210                 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
211                 failed = true;
212             }
213
214             testResetAfterTimeout(failed);
215         }
216
217         /* Test the script timeout's TerminatedExecutionException should NOT be catchable: */
218         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
219         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, shouldTerminateCallback, 0);
220         {
221             unsigned timeAfterWatchdogShouldHaveFired = 300 + tierAdjustmentMillis;
222             
223             StringBuilder scriptBuilder;
224             scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); try { while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
225             scriptBuilder.appendNumber(timeAfterWatchdogShouldHaveFired / 1000.0);
226             scriptBuilder.appendLiteral(") break; } } catch(e) { } } foo();");
227
228             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
229             exception = nullptr;
230             shouldTerminateCallbackWasCalled = false;
231
232             auto startTime = currentCPUTime();
233             JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
234             auto endTime = currentCPUTime();
235             
236             if (((endTime - startTime) >= milliseconds(timeAfterWatchdogShouldHaveFired)) || !shouldTerminateCallbackWasCalled) {
237                 if (!((endTime - startTime) < milliseconds(timeAfterWatchdogShouldHaveFired)))
238                     printf("FAIL: %s script did not time out as expected.\n", tierOptions.tier);
239                 if (!shouldTerminateCallbackWasCalled)
240                     printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
241                 failed = true;
242             }
243             
244             if (exception)
245                 printf("PASS: %s TerminatedExecutionException was not catchable as expected.\n", tierOptions.tier);
246             else {
247                 printf("FAIL: %s TerminatedExecutionException was caught.\n", tierOptions.tier);
248                 failed = true;
249             }
250
251             testResetAfterTimeout(failed);
252         }
253         
254         /* Test script timeout with no callback: */
255         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
256         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, 0, 0);
257         {
258             unsigned timeAfterWatchdogShouldHaveFired = 300 + tierAdjustmentMillis;
259             
260             StringBuilder scriptBuilder;
261             scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
262             scriptBuilder.appendNumber(timeAfterWatchdogShouldHaveFired / 1000.0);
263             scriptBuilder.appendLiteral(") break; } } foo();");
264             
265             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
266             exception = nullptr;
267             shouldTerminateCallbackWasCalled = false;
268
269             auto startTime = currentCPUTime();
270             JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
271             auto endTime = currentCPUTime();
272             
273             if (((endTime - startTime) < milliseconds(timeAfterWatchdogShouldHaveFired)) && !shouldTerminateCallbackWasCalled)
274                 printf("PASS: %s script timed out as expected when no callback is specified.\n", tierOptions.tier);
275             else {
276                 if ((endTime - startTime) >= milliseconds(timeAfterWatchdogShouldHaveFired))
277                     printf("FAIL: %s script did not time out as expected when no callback is specified.\n", tierOptions.tier);
278                 else
279                     printf("FAIL: %s script called stale callback function.\n", tierOptions.tier);
280                 failed = true;
281             }
282             
283             if (!exception) {
284                 printf("FAIL: %s TerminatedExecutionException was not thrown.\n", tierOptions.tier);
285                 failed = true;
286             }
287
288             testResetAfterTimeout(failed);
289         }
290         
291         /* Test script timeout cancellation: */
292         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
293         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, cancelTerminateCallback, 0);
294         {
295             unsigned timeAfterWatchdogShouldHaveFired = 300 + tierAdjustmentMillis;
296             
297             StringBuilder scriptBuilder;
298             scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
299             scriptBuilder.appendNumber(timeAfterWatchdogShouldHaveFired / 1000.0);
300             scriptBuilder.appendLiteral(") break; } } foo();");
301
302             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
303             exception = nullptr;
304             cancelTerminateCallbackWasCalled = false;
305
306             auto startTime = currentCPUTime();
307             JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
308             auto endTime = currentCPUTime();
309             
310             if (((endTime - startTime) >= milliseconds(timeAfterWatchdogShouldHaveFired)) && cancelTerminateCallbackWasCalled && !exception)
311                 printf("PASS: %s script timeout was cancelled as expected.\n", tierOptions.tier);
312             else {
313                 if (((endTime - startTime) < milliseconds(timeAfterWatchdogShouldHaveFired)) || exception)
314                     printf("FAIL: %s script timeout was not cancelled.\n", tierOptions.tier);
315                 if (!cancelTerminateCallbackWasCalled)
316                     printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
317                 failed = true;
318             }
319             
320             if (exception) {
321                 printf("FAIL: %s Unexpected TerminatedExecutionException thrown.\n", tierOptions.tier);
322                 failed = true;
323             }
324         }
325         
326         /* Test script timeout extension: */
327         timeLimit = (100 + tierAdjustmentMillis) / 1000.0;
328         JSContextGroupSetExecutionTimeLimit(contextGroup, timeLimit, extendTerminateCallback, 0);
329         {
330             unsigned timeBeforeExtendedDeadline = 250 + tierAdjustmentMillis;
331             unsigned timeAfterExtendedDeadline = 600 + tierAdjustmentMillis;
332             unsigned maxBusyLoopTime = 750 + tierAdjustmentMillis;
333
334             StringBuilder scriptBuilder;
335             scriptBuilder.appendLiteral("function foo() { var startTime = currentCPUTime(); while (true) { for (var i = 0; i < 1000; i++); if (currentCPUTime() - startTime > ");
336             scriptBuilder.appendNumber(maxBusyLoopTime / 1000.0); // in seconds.
337             scriptBuilder.appendLiteral(") break; } } foo();");
338
339             JSStringRef script = JSStringCreateWithUTF8CString(scriptBuilder.toString().utf8().data());
340             exception = nullptr;
341             extendTerminateCallbackCalled = 0;
342
343             auto startTime = currentCPUTime();
344             JSEvaluateScript(context, script, nullptr, nullptr, 1, &exception);
345             auto endTime = currentCPUTime();
346             auto deltaTime = endTime - startTime;
347             
348             if ((deltaTime >= milliseconds(timeBeforeExtendedDeadline)) && (deltaTime < milliseconds(timeAfterExtendedDeadline)) && (extendTerminateCallbackCalled == 2) && exception)
349                 printf("PASS: %s script timeout was extended as expected.\n", tierOptions.tier);
350             else {
351                 if (deltaTime < milliseconds(timeBeforeExtendedDeadline))
352                     printf("FAIL: %s script timeout was not extended as expected.\n", tierOptions.tier);
353                 else if (deltaTime >= milliseconds(timeAfterExtendedDeadline))
354                     printf("FAIL: %s script did not timeout.\n", tierOptions.tier);
355                 
356                 if (extendTerminateCallbackCalled < 1)
357                     printf("FAIL: %s script timeout callback was not called.\n", tierOptions.tier);
358                 if (extendTerminateCallbackCalled < 2)
359                     printf("FAIL: %s script timeout callback was not called after timeout extension.\n", tierOptions.tier);
360                 
361                 if (!exception)
362                     printf("FAIL: %s TerminatedExecutionException was not thrown during timeout extension test.\n", tierOptions.tier);
363                 
364                 failed = true;
365             }
366         }
367
368         JSGlobalContextRelease(context);
369
370         Options::setOptions(savedOptionsBuilder.toString().ascii().data());
371     }
372     
373     return failed;
374 }