Streamline JSRetainPtr, fix leaks of JSString and JSGlobalContext
[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     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
119 int 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.appendNumber(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.appendNumber(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.appendNumber(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.appendNumber(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.appendNumber(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.appendNumber(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.appendNumber(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 }