/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 * vim: set ts=8 sts=4 et sw=4 tw=99:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* Profiling-related API */

#include "builtin/Profilers.h"

#include "mozilla/Sprintf.h"

#include <stdarg.h>

#ifdef MOZ_CALLGRIND
# include <valgrind/callgrind.h>
#endif

#ifdef __APPLE__
#ifdef MOZ_INSTRUMENTS
# include "devtools/Instruments.h"
#endif
#endif

#ifdef XP_WIN
# include <process.h>
# define getpid _getpid
#endif

#include "vm/Probes.h"

#include "jscntxtinlines.h"

using namespace js;

using mozilla::ArrayLength;

/* Thread-unsafe error management */

static char gLastError[2000];

#if defined(__APPLE__) || defined(__linux__) || defined(MOZ_CALLGRIND)
static void
MOZ_FORMAT_PRINTF(1, 2)
UnsafeError(const char* format, ...)
{
    va_list args;
    va_start(args, format);
    (void) VsprintfLiteral(gLastError, format, args);
    va_end(args);
}
#endif

JS_PUBLIC_API(const char*)
JS_UnsafeGetLastProfilingError()
{
    return gLastError;
}

#ifdef __APPLE__
static bool
StartOSXProfiling(const char* profileName, pid_t pid)
{
    bool ok = true;
    const char* profiler = nullptr;
#ifdef MOZ_INSTRUMENTS
    ok = Instruments::Start(pid);
    profiler = "Instruments";
#endif
    if (!ok) {
        if (profileName)
            UnsafeError("Failed to start %s for %s", profiler, profileName);
        else
            UnsafeError("Failed to start %s", profiler);
        return false;
    }
    return true;
}
#endif

JS_PUBLIC_API(bool)
JS_StartProfiling(const char* profileName, pid_t pid)
{
    bool ok = true;
#ifdef __APPLE__
    ok = StartOSXProfiling(profileName, pid);
#endif
#ifdef __linux__
    if (!js_StartPerf())
        ok = false;
#endif
    return ok;
}

JS_PUBLIC_API(bool)
JS_StopProfiling(const char* profileName)
{
    bool ok = true;
#ifdef __APPLE__
#ifdef MOZ_INSTRUMENTS
    Instruments::Stop(profileName);
#endif
#endif
#ifdef __linux__
    if (!js_StopPerf())
        ok = false;
#endif
    return ok;
}

/*
 * Start or stop whatever platform- and configuration-specific profiling
 * backends are available.
 */
static bool
ControlProfilers(bool toState)
{
    bool ok = true;

    if (! probes::ProfilingActive && toState) {
#ifdef __APPLE__
#if defined(MOZ_INSTRUMENTS)
        const char* profiler;
#ifdef MOZ_INSTRUMENTS
        ok = Instruments::Resume();
        profiler = "Instruments";
#endif
        if (!ok) {
            UnsafeError("Failed to start %s", profiler);
        }
#endif
#endif
#ifdef MOZ_CALLGRIND
        if (! js_StartCallgrind()) {
            UnsafeError("Failed to start Callgrind");
            ok = false;
        }
#endif
    } else if (probes::ProfilingActive && ! toState) {
#ifdef __APPLE__
#ifdef MOZ_INSTRUMENTS
        Instruments::Pause();
#endif
#endif
#ifdef MOZ_CALLGRIND
        if (! js_StopCallgrind()) {
            UnsafeError("failed to stop Callgrind");
            ok = false;
        }
#endif
    }

    probes::ProfilingActive = toState;

    return ok;
}

/*
 * Pause/resume whatever profiling mechanism is currently compiled
 * in, if applicable. This will not affect things like dtrace.
 *
 * Do not mix calls to these APIs with calls to the individual
 * profilers' pause/resume functions, because only overall state is
 * tracked, not the state of each profiler.
 */
JS_PUBLIC_API(bool)
JS_PauseProfilers(const char* profileName)
{
    return ControlProfilers(false);
}

JS_PUBLIC_API(bool)
JS_ResumeProfilers(const char* profileName)
{
    return ControlProfilers(true);
}

JS_PUBLIC_API(bool)
JS_DumpProfile(const char* outfile, const char* profileName)
{
    bool ok = true;
#ifdef MOZ_CALLGRIND
    js_DumpCallgrind(outfile);
#endif
    return ok;
}

#ifdef MOZ_PROFILING

struct RequiredStringArg {
    JSContext* mCx;
    char* mBytes;
    RequiredStringArg(JSContext* cx, const CallArgs& args, size_t argi, const char* caller)
        : mCx(cx), mBytes(nullptr)
    {
        if (args.length() <= argi) {
            JS_ReportErrorASCII(cx, "%s: not enough arguments", caller);
        } else if (!args[argi].isString()) {
            JS_ReportErrorASCII(cx, "%s: invalid arguments (string expected)", caller);
        } else {
            mBytes = JS_EncodeString(cx, args[argi].toString());
        }
    }
    operator void*() {
        return (void*) mBytes;
    }
    ~RequiredStringArg() {
        js_free(mBytes);
    }
};

static bool
StartProfiling(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);
    if (args.length() == 0) {
        args.rval().setBoolean(JS_StartProfiling(nullptr, getpid()));
        return true;
    }

    RequiredStringArg profileName(cx, args, 0, "startProfiling");
    if (!profileName)
        return false;

    if (args.length() == 1) {
        args.rval().setBoolean(JS_StartProfiling(profileName.mBytes, getpid()));
        return true;
    }

    if (!args[1].isInt32()) {
        JS_ReportErrorASCII(cx, "startProfiling: invalid arguments (int expected)");
        return false;
    }
    pid_t pid = static_cast<pid_t>(args[1].toInt32());
    args.rval().setBoolean(JS_StartProfiling(profileName.mBytes, pid));
    return true;
}

static bool
StopProfiling(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);
    if (args.length() == 0) {
        args.rval().setBoolean(JS_StopProfiling(nullptr));
        return true;
    }

    RequiredStringArg profileName(cx, args, 0, "stopProfiling");
    if (!profileName)
        return false;
    args.rval().setBoolean(JS_StopProfiling(profileName.mBytes));
    return true;
}

static bool
PauseProfilers(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);
    if (args.length() == 0) {
        args.rval().setBoolean(JS_PauseProfilers(nullptr));
        return true;
    }

    RequiredStringArg profileName(cx, args, 0, "pauseProfiling");
    if (!profileName)
        return false;
    args.rval().setBoolean(JS_PauseProfilers(profileName.mBytes));
    return true;
}

static bool
ResumeProfilers(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);
    if (args.length() == 0) {
        args.rval().setBoolean(JS_ResumeProfilers(nullptr));
        return true;
    }

    RequiredStringArg profileName(cx, args, 0, "resumeProfiling");
    if (!profileName)
        return false;
    args.rval().setBoolean(JS_ResumeProfilers(profileName.mBytes));
    return true;
}

/* Usage: DumpProfile([filename[, profileName]]) */
static bool
DumpProfile(JSContext* cx, unsigned argc, Value* vp)
{
    bool ret;
    CallArgs args = CallArgsFromVp(argc, vp);
    if (args.length() == 0) {
        ret = JS_DumpProfile(nullptr, nullptr);
    } else {
        RequiredStringArg filename(cx, args, 0, "dumpProfile");
        if (!filename)
            return false;

        if (args.length() == 1) {
            ret = JS_DumpProfile(filename.mBytes, nullptr);
        } else {
            RequiredStringArg profileName(cx, args, 1, "dumpProfile");
            if (!profileName)
                return false;

            ret = JS_DumpProfile(filename.mBytes, profileName.mBytes);
        }
    }

    args.rval().setBoolean(ret);
    return true;
}

static bool
GetMaxGCPauseSinceClear(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);
    args.rval().setNumber(uint32_t(cx->runtime()->gc.stats.getMaxGCPauseSinceClear()));
    return true;
}

static bool
ClearMaxGCPauseAccumulator(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);
    args.rval().setNumber(uint32_t(cx->runtime()->gc.stats.clearMaxGCPauseAccumulator()));
    return true;
}

#if defined(MOZ_INSTRUMENTS)

static bool
IgnoreAndReturnTrue(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);
    args.rval().setBoolean(true);
    return true;
}

#endif

#ifdef MOZ_CALLGRIND
static bool
StartCallgrind(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);
    args.rval().setBoolean(js_StartCallgrind());
    return true;
}

static bool
StopCallgrind(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);
    args.rval().setBoolean(js_StopCallgrind());
    return true;
}

static bool
DumpCallgrind(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);
    if (args.length() == 0) {
        args.rval().setBoolean(js_DumpCallgrind(nullptr));
        return true;
    }

    RequiredStringArg outFile(cx, args, 0, "dumpCallgrind");
    if (!outFile)
        return false;

    args.rval().setBoolean(js_DumpCallgrind(outFile.mBytes));
    return true;
}
#endif

static const JSFunctionSpec profiling_functions[] = {
    JS_FN("startProfiling",  StartProfiling,      1,0),
    JS_FN("stopProfiling",   StopProfiling,       1,0),
    JS_FN("pauseProfilers",  PauseProfilers,      1,0),
    JS_FN("resumeProfilers", ResumeProfilers,     1,0),
    JS_FN("dumpProfile",     DumpProfile,         2,0),
    JS_FN("getMaxGCPauseSinceClear",    GetMaxGCPauseSinceClear,    0, 0),
    JS_FN("clearMaxGCPauseAccumulator", ClearMaxGCPauseAccumulator, 0, 0),
#if defined(MOZ_INSTRUMENTS)
    /* Keep users of the old shark API happy. */
    JS_FN("connectShark",    IgnoreAndReturnTrue, 0,0),
    JS_FN("disconnectShark", IgnoreAndReturnTrue, 0,0),
    JS_FN("startShark",      StartProfiling,      0,0),
    JS_FN("stopShark",       StopProfiling,       0,0),
#endif
#ifdef MOZ_CALLGRIND
    JS_FN("startCallgrind", StartCallgrind,       0,0),
    JS_FN("stopCallgrind",  StopCallgrind,        0,0),
    JS_FN("dumpCallgrind",  DumpCallgrind,        1,0),
#endif
    JS_FS_END
};

#endif

JS_PUBLIC_API(bool)
JS_DefineProfilingFunctions(JSContext* cx, HandleObject obj)
{
    assertSameCompartment(cx, obj);
#ifdef MOZ_PROFILING
    return JS_DefineFunctions(cx, obj, profiling_functions);
#else
    return true;
#endif
}

#ifdef MOZ_CALLGRIND

JS_FRIEND_API(bool)
js_StartCallgrind()
{
    JS_SILENCE_UNUSED_VALUE_IN_EXPR(CALLGRIND_START_INSTRUMENTATION);
    JS_SILENCE_UNUSED_VALUE_IN_EXPR(CALLGRIND_ZERO_STATS);
    return true;
}

JS_FRIEND_API(bool)
js_StopCallgrind()
{
    JS_SILENCE_UNUSED_VALUE_IN_EXPR(CALLGRIND_STOP_INSTRUMENTATION);
    return true;
}

JS_FRIEND_API(bool)
js_DumpCallgrind(const char* outfile)
{
    if (outfile) {
        JS_SILENCE_UNUSED_VALUE_IN_EXPR(CALLGRIND_DUMP_STATS_AT(outfile));
    } else {
        JS_SILENCE_UNUSED_VALUE_IN_EXPR(CALLGRIND_DUMP_STATS);
    }

    return true;
}

#endif /* MOZ_CALLGRIND */

#ifdef __linux__

/*
 * Code for starting and stopping |perf|, the Linux profiler.
 *
 * Output from profiling is written to mozperf.data in your cwd.
 *
 * To enable, set MOZ_PROFILE_WITH_PERF=1 in your environment.
 *
 * To pass additional parameters to |perf record|, provide them in the
 * MOZ_PROFILE_PERF_FLAGS environment variable.  If this variable does not
 * exist, we default it to "--call-graph".  (If you don't want --call-graph but
 * don't want to pass any other args, define MOZ_PROFILE_PERF_FLAGS to the empty
 * string.)
 *
 * If you include --pid or --output in MOZ_PROFILE_PERF_FLAGS, you're just
 * asking for trouble.
 *
 * Our split-on-spaces logic is lame, so don't expect MOZ_PROFILE_PERF_FLAGS to
 * work if you pass an argument which includes a space (e.g.
 * MOZ_PROFILE_PERF_FLAGS="-e 'foo bar'").
 */

#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

static bool perfInitialized = false;
static pid_t perfPid = 0;

bool js_StartPerf()
{
    const char* outfile = "mozperf.data";

    if (perfPid != 0) {
        UnsafeError("js_StartPerf: called while perf was already running!\n");
        return false;
    }

    // Bail if MOZ_PROFILE_WITH_PERF is empty or undefined.
    if (!getenv("MOZ_PROFILE_WITH_PERF") ||
        !strlen(getenv("MOZ_PROFILE_WITH_PERF"))) {
        return true;
    }

    /*
     * Delete mozperf.data the first time through -- we're going to append to it
     * later on, so we want it to be clean when we start out.
     */
    if (!perfInitialized) {
        perfInitialized = true;
        unlink(outfile);
        char cwd[4096];
        printf("Writing perf profiling data to %s/%s\n",
               getcwd(cwd, sizeof(cwd)), outfile);
    }

    pid_t mainPid = getpid();

    pid_t childPid = fork();
    if (childPid == 0) {
        /* perf record --pid $mainPID --output=$outfile $MOZ_PROFILE_PERF_FLAGS */

        char mainPidStr[16];
        SprintfLiteral(mainPidStr, "%d", mainPid);
        const char* defaultArgs[] = {"perf", "record", "--pid", mainPidStr, "--output", outfile};

        Vector<const char*, 0, SystemAllocPolicy> args;
        if (!args.append(defaultArgs, ArrayLength(defaultArgs)))
            return false;

        const char* flags = getenv("MOZ_PROFILE_PERF_FLAGS");
        if (!flags) {
            flags = "--call-graph";
        }

        UniqueChars flags2((char*)js_malloc(strlen(flags) + 1));
        if (!flags2)
            return false;
        strcpy(flags2.get(), flags);

        // Split |flags2| on spaces.
        char* toksave;
        char* tok = strtok_r(flags2.get(), " ", &toksave);
        while (tok) {
            if (!args.append(tok))
                return false;
            tok = strtok_r(nullptr, " ", &toksave);
        }

        if (!args.append((char*) nullptr))
            return false;

        execvp("perf", const_cast<char**>(args.begin()));

        /* Reached only if execlp fails. */
        fprintf(stderr, "Unable to start perf.\n");
        exit(1);
    }
    if (childPid > 0) {
        perfPid = childPid;

        /* Give perf a chance to warm up. */
        usleep(500 * 1000);
        return true;
    }
    UnsafeError("js_StartPerf: fork() failed\n");
    return false;
}

bool js_StopPerf()
{
    if (perfPid == 0) {
        UnsafeError("js_StopPerf: perf is not running.\n");
        return true;
    }

    if (kill(perfPid, SIGINT)) {
        UnsafeError("js_StopPerf: kill failed\n");

        // Try to reap the process anyway.
        waitpid(perfPid, nullptr, WNOHANG);
    }
    else {
        waitpid(perfPid, nullptr, 0);
    }

    perfPid = 0;
    return true;
}

#endif /* __linux__ */