/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 * vim: set ts=8 sw=4 et tw=99:
 */

#ifdef JS_THREADSAFE

#include "tests.h"
#include "prthread.h"

#include "jscntxt.h"

/*
 * We test that if a GC callback cancels the GC on a child thread the GC can
 * still proceed on the main thread even if the child thread continue to
 * run uninterrupted.
 */

struct SharedData {
    enum ChildState {
        CHILD_STARTING,
        CHILD_RUNNING,
        CHILD_DONE,
        CHILD_ERROR
    };

    JSRuntime   *const runtime;
    PRThread    *const mainThread;
    PRLock      *const lock;
    PRCondVar   *const signal;
    ChildState  childState;
    bool        childShouldStop;
    JSContext   *childContext;

    SharedData(JSRuntime *rt, bool *ok)
      : runtime(rt),
        mainThread(PR_GetCurrentThread()),
        lock(PR_NewLock()),
        signal(lock ? PR_NewCondVar(lock) : NULL),
        childState(CHILD_STARTING),
        childShouldStop(false),
        childContext(NULL)
    {
        JS_ASSERT(!*ok);
        *ok = !!signal;
    }

    ~SharedData() {
        if (signal)
            PR_DestroyCondVar(signal);
        if (lock)
            PR_DestroyLock(lock);
    }
};

static SharedData *shared;

static JSBool
CancelNonMainThreadGCCallback(JSContext *cx, JSGCStatus status)
{
    return status != JSGC_BEGIN || PR_GetCurrentThread() == shared->mainThread;
}

static JSBool
StopChildOperationCallback(JSContext *cx)
{
    bool shouldStop;
    PR_Lock(shared->lock);
    shouldStop = shared->childShouldStop;
    PR_Unlock(shared->lock);
    return !shouldStop;
}

static JSBool
NotifyMainThreadAboutBusyLoop(JSContext *cx, uintN argc, jsval *vp)
{
    PR_Lock(shared->lock);
    JS_ASSERT(shared->childState == SharedData::CHILD_STARTING);
    shared->childState = SharedData::CHILD_RUNNING;
    shared->childContext = cx;
    PR_NotifyCondVar(shared->signal);
    PR_Unlock(shared->lock);

    return true;
}

static void
ChildThreadMain(void *arg)
{
    JS_ASSERT(!arg);
    bool error = true;
    JSContext *cx = JS_NewContext(shared->runtime, 8192);
    if (cx) {
        JS_SetOperationCallback(cx, StopChildOperationCallback);
        JSAutoRequest ar(cx);
        JSObject *global = JS_NewCompartmentAndGlobalObject(cx, JSAPITest::basicGlobalClass(),
                                                            NULL);
        if (global) {
            JS_SetGlobalObject(cx, global);
            if (JS_InitStandardClasses(cx, global) &&
                JS_DefineFunction(cx, global, "notify", NotifyMainThreadAboutBusyLoop, 0, 0)) {

                jsval rval;
                static const char code[] = "var i = 0; notify(); for (var i = 0; ; ++i);";
                JSBool ok = JS_EvaluateScript(cx, global, code, strlen(code),
                                              __FILE__, __LINE__, &rval);
                if (!ok && !JS_IsExceptionPending(cx)) {
                    /* Evaluate should only return via the callback cancellation. */
                    error = false;
                }
            }
        }
    }

    PR_Lock(shared->lock);
    shared->childState = error ? SharedData::CHILD_DONE : SharedData::CHILD_ERROR;
    shared->childContext = NULL;
    PR_NotifyCondVar(shared->signal);
    PR_Unlock(shared->lock);

    if (cx)
        JS_DestroyContextNoGC(cx);
}

BEGIN_TEST(testThreadGC_bug590533)
    {
        /*
         * Test the child thread busy running while the current thread calls
         * the GC both with JSRuntime->gcIsNeeded set and unset.
         */
        bool ok = TestChildThread(true);
        CHECK(ok);
        ok = TestChildThread(false);
        CHECK(ok);
        return ok;
    }

    bool TestChildThread(bool setGCIsNeeded)
    {
        bool ok = false;
        shared = new SharedData(rt, &ok);
        CHECK(ok);

        JSGCCallback oldGCCallback = JS_SetGCCallback(cx, CancelNonMainThreadGCCallback);

        PRThread *thread =
            PR_CreateThread(PR_USER_THREAD, ChildThreadMain, NULL,
                            PR_PRIORITY_NORMAL, PR_LOCAL_THREAD, PR_JOINABLE_THREAD, 0);
        if (!thread)
            return false;

        PR_Lock(shared->lock);
        while (shared->childState == SharedData::CHILD_STARTING)
            PR_WaitCondVar(shared->signal, PR_INTERVAL_NO_TIMEOUT);
        JS_ASSERT(shared->childState != SharedData::CHILD_DONE);
        ok = (shared->childState == SharedData::CHILD_RUNNING);
        PR_Unlock(shared->lock);

        CHECK(ok);

        if (setGCIsNeeded) {
            /*
             * Use JS internal API to set the GC trigger flag after we know
             * that the child is in a request and is about to run an infinite
             * loop. Then run the GC with JSRuntime->gcIsNeeded flag set.
             */
            js::AutoLockGC lock(rt);
            js::TriggerGC(rt);
        }

        JS_GC(cx);

        PR_Lock(shared->lock);
        shared->childShouldStop = true;
        while (shared->childState == SharedData::CHILD_RUNNING) {
            JS_TriggerOperationCallback(shared->childContext);
            PR_WaitCondVar(shared->signal, PR_INTERVAL_NO_TIMEOUT);
        }
        JS_ASSERT(shared->childState != SharedData::CHILD_STARTING);
        ok = (shared->childState == SharedData::CHILD_DONE);
        PR_Unlock(shared->lock);

        JS_SetGCCallback(cx, oldGCCallback);

        PR_JoinThread(thread);

        delete shared;
        shared = NULL;

        return true;
    }


END_TEST(testThreadGC_bug590533)

#endif
