/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=8 sts=2 et sw=2 tw=80:
* 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/Compiler.h"
#include "mozilla/Sprintf.h"
#include <iterator>
#include <stdarg.h>
#include "util/GetPidProvider.h" // getpid()
#ifdef MOZ_CALLGRIND
# include <valgrind/callgrind.h>
#endif
#ifdef __APPLE__
# ifdef MOZ_INSTRUMENTS
# include
"devtools/Instruments.h"
# endif
#endif
#include "js/CharacterEncoding.h"
#include "js/PropertyAndElement.h" // JS_DefineFunctions
#include "js/PropertySpec.h"
#include "js/Utility.h"
#include "util/Text.h"
#include "vm/Probes.h"
#include "vm/JSContext-inl.h"
using namespace js;
/* 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
ok = js_DumpCallgrind(outfile);
#endif
return ok;
}
#ifdef MOZ_PROFILING
static UniqueChars RequiredStringArg(JSContext* cx,
const CallArgs& args,
size_t argi,
const char* caller) {
if (args.length() <= argi) {
JS_ReportErrorASCII(cx,
"%s: not enough arguments", caller);
return nullptr;
}
if (!args[argi].isString()) {
JS_ReportErrorASCII(cx,
"%s: invalid arguments (string expected)", caller);
return nullptr;
}
return JS_EncodeStringToLatin1(cx, args[argi].toString());
}
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;
}
UniqueChars profileName = RequiredStringArg(cx, args, 0,
"startProfiling");
if (!profileName) {
return false;
}
if (args.length() == 1) {
args.rval().setBoolean(JS_StartProfiling(profileName.get(), 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.get(), 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;
}
UniqueChars profileName = RequiredStringArg(cx, args, 0,
"stopProfiling");
if (!profileName) {
return false;
}
args.rval().setBoolean(JS_StopProfiling(profileName.get()));
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;
}
UniqueChars profileName = RequiredStringArg(cx, args, 0,
"pauseProfiling");
if (!profileName) {
return false;
}
args.rval().setBoolean(JS_PauseProfilers(profileName.get()));
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;
}
UniqueChars profileName = RequiredStringArg(cx, args, 0,
"resumeProfiling");
if (!profileName) {
return false;
}
args.rval().setBoolean(JS_ResumeProfilers(profileName.get()));
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 {
UniqueChars filename = RequiredStringArg(cx, args, 0,
"dumpProfile");
if (!filename) {
return false;
}
if (args.length() == 1) {
ret = JS_DumpProfile(filename.get(), nullptr);
}
else {
UniqueChars profileName = RequiredStringArg(cx, args, 1,
"dumpProfile");
if (!profileName) {
return false;
}
ret = JS_DumpProfile(filename.get(), profileName.get());
}
}
args.rval().setBoolean(ret);
return true;
}
static bool GetMaxGCPauseSinceClear(JSContext* cx,
unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
args.rval().setNumber(
cx->runtime()->gc.stats().getMaxGCPauseSinceClear().ToMicroseconds());
return true;
}
static bool ClearMaxGCPauseAccumulator(JSContext* cx,
unsigned argc,
Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
args.rval().setNumber(
cx->runtime()->gc.stats().clearMaxGCPauseAccumulator().ToMicroseconds());
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;
}
UniqueChars outFile = RequiredStringArg(cx, args, 0,
"dumpCallgrind");
if (!outFile) {
return false;
}
args.rval().setBoolean(js_DumpCallgrind(outFile.get()));
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) {
cx->check(obj);
#ifdef MOZ_PROFILING
return JS_DefineFunctions(cx, obj, profiling_functions);
#else
return true;
#endif
}
#ifdef MOZ_CALLGRIND
/* Wrapper for various macros to stop warnings coming from their expansions. */
# if defined(__clang__)
# define JS_SILENCE_UNUSED_VALUE_IN_EXPR(expr) \
JS_BEGIN_MACRO \
_Pragma(
"clang diagnostic push")
/* If these _Pragmas cause warnings \
for you, try disabling ccache. */
_Pragma(
"clang diagnostic ignored \"-Wunused-value\
"") { \
expr; \
} \
_Pragma(
"clang diagnostic pop") \
JS_END_MACRO
# elif MOZ_IS_GCC
# define JS_SILENCE_UNUSED_VALUE_IN_EXPR(expr) \
JS_BEGIN_MACRO \
_Pragma(
"GCC diagnostic push") \
_Pragma(
"GCC diagnostic ignored \"-Wunused-but-set-variable\
"") \
expr; \
_Pragma(
"GCC diagnostic pop") \
JS_END_MACRO
# endif
# if !
defined(JS_SILENCE_UNUSED_VALUE_IN_EXPR)
# define JS_SILENCE_UNUSED_VALUE_IN_EXPR(expr) \
JS_BEGIN_MACRO \
expr; \
JS_END_MACRO
# endif
JS_PUBLIC_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_PUBLIC_API
bool js_StopCallgrind() {
JS_SILENCE_UNUSED_VALUE_IN_EXPR(CALLGRIND_STOP_INSTRUMENTATION);
return true;
}
JS_PUBLIC_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, std::size(defaultArgs))) {
return false;
}
const char* flags = getenv(
"MOZ_PROFILE_PERF_FLAGS");
if (!flags) {
flags =
"--call-graph";
}
UniqueChars flags2 = DuplicateString(flags);
if (!flags2) {
return false;
}
// 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__ */