Bug 1050500: Add SpiderMonkey API for reporting JavaScript entry points. r=shu

This commit is contained in:
Jim Blandy 2015-05-07 11:34:03 -07:00
parent 47669df31d
commit f89382e8da
16 changed files with 403 additions and 10 deletions

View File

@ -318,6 +318,48 @@ onPromiseSettled(JSContext* cx, HandleObject promise);
JS_PUBLIC_API(bool)
IsDebugger(JS::Value val);
// Hooks for reporting where JavaScript execution began.
//
// Our performance tools would like to be able to label blocks of JavaScript
// execution with the function name and source location where execution began:
// the event handler, the callback, etc.
//
// Construct an instance of this class on the stack, providing a JSContext
// belonging to the runtime in which execution will occur. Each time we enter
// JavaScript --- specifically, each time we push a JavaScript stack frame that
// has no older JS frames younger than this AutoEntryMonitor --- we will
// call the appropriate |Entry| member function to indicate where we've begun
// execution.
class MOZ_STACK_CLASS AutoEntryMonitor {
JSRuntime* runtime_;
AutoEntryMonitor* savedMonitor_;
public:
explicit AutoEntryMonitor(JSContext* cx);
~AutoEntryMonitor();
// SpiderMonkey reports the JavaScript entry points occuring within this
// AutoEntryMonitor's scope to the following member functions, which the
// embedding is expected to override.
// We have begun executing |function|. Note that |function| may not be the
// actual closure we are running, but only the canonical function object to
// which the script refers.
virtual void Entry(JSContext* cx, JSFunction* function) = 0;
// Execution has begun at the entry point of |script|, which is not a
// function body. (This is probably being executed by 'eval' or some
// JSAPI equivalent.)
virtual void Entry(JSContext* cx, JSScript* script) = 0;
// Execution of the function or script has ended.
virtual void Exit(JSContext* cx) { }
};
} // namespace dbg
} // namespace JS

View File

@ -744,8 +744,12 @@ CallAsmJS(JSContext* cx, unsigned argc, Value* vp)
// that the optimized asm.js-to-Ion FFI call path (which we want to be
// very fast) can avoid doing so. The JitActivation is marked as
// inactive so stack iteration will skip over it.
//
// We needn't provide an entry script pointer; that's only used for
// reporting entry points to performance-monitoring tools, and asm.js ->
// Ion calls will never be entry points.
AsmJSActivation activation(cx, module);
JitActivation jitActivation(cx, /* active */ false);
JitActivation jitActivation(cx, /* entryScript */ nullptr, /* active */ false);
// Call the per-exported-function trampoline created by GenerateEntry.
AsmJSModule::CodePtr enter = module.entryTrampoline(func);

View File

@ -0,0 +1,50 @@
// AutoEntryMonitor should catch all entry points into JavaScript.
load(libdir + 'array-compare.js');
function cold_and_warm(f, params, expected) {
print(uneval(params));
print(uneval(entryPoints(params)));
assertEq(arraysEqual(entryPoints(params), expected), true);
// Warm up the function a bit, so the JITs will compile it, and try again.
if (f)
for (i = 0; i < 10; i++)
f();
assertEq(arraysEqual(entryPoints(params), expected), true);
}
function entry1() { }
cold_and_warm(entry1, { function: entry1 }, [ "entry1" ]);
var getx = { get x() { } };
cold_and_warm(Object.getOwnPropertyDescriptor(getx, 'x').get,
{ object: getx, property: 'x' }, [ "getx.x" ]);
var sety = { set y(v) { } };
cold_and_warm(Object.getOwnPropertyDescriptor(sety, 'y').set,
{ object: sety, property: 'y', value: 'glerk' }, [ "sety.y" ]);
cold_and_warm(Object.prototype.toString, { ToString: {} }, []);
var toS = { toString: function myToString() { return "string"; } };
cold_and_warm(toS.toString, { ToString: toS }, [ "myToString" ]);
cold_and_warm(undefined, { ToNumber: {} }, []);
var vOf = { valueOf: function myValueOf() { return 42; } };
cold_and_warm(vOf.valueOf, { ToNumber: vOf }, [ "myValueOf" ]);
var toSvOf = { toString: function relations() { return Object; },
valueOf: function wallpaper() { return 17; } };
cold_and_warm(() => { toSvOf.toString(); toSvOf.valueOf(); },
{ ToString: toSvOf }, [ "relations", "wallpaper" ]);
var vOftoS = { toString: function ettes() { return "6-inch lengths"; },
valueOf: function deathBy() { return Math; } };
cold_and_warm(() => { vOftoS.valueOf(); vOftoS.toString(); },
{ ToNumber: vOftoS }, [ "deathBy", "ettes" ]);
cold_and_warm(() => 0, { eval: "Math.atan2(1,1);" }, [ "eval:entryPoint eval" ]);

View File

@ -0,0 +1,12 @@
// Nested uses of AutoEntryMonitor should behave with decorum.
load(libdir + 'array-compare.js');
function Cobb() {
var twoShot = { toString: function Saito() { return Object; },
valueOf: function Fischer() { return "Ariadne"; } };
assertEq(arraysEqual(entryPoints({ ToString: twoShot }),
[ "Saito", "Fischer" ]), true);
}
assertEq(arraysEqual(entryPoints({ function: Cobb }), ["Cobb"]), true);

View File

@ -115,7 +115,7 @@ EnterBaseline(JSContext* cx, EnterJitData& data)
data.result.setInt32(data.numActualArgs);
{
AssertCompartmentUnchanged pcc(cx);
JitActivation activation(cx);
JitActivation activation(cx, data.calleeToken);
if (data.osrFrame)
data.osrFrame->setRunningInJit();

View File

@ -2384,7 +2384,7 @@ EnterIon(JSContext* cx, EnterJitData& data)
data.result.setInt32(data.numActualArgs);
{
AssertCompartmentUnchanged pcc(cx);
JitActivation activation(cx);
JitActivation activation(cx, data.calleeToken);
CALL_GENERATED_CODE(enter, data.jitcode, data.maxArgc, data.maxArgv, /* osrFrame = */nullptr, data.calleeToken,
/* scopeChain = */ nullptr, 0, data.result.address());
@ -2481,14 +2481,15 @@ jit::FastInvoke(JSContext* cx, HandleFunction fun, CallArgs& args)
{
JS_CHECK_RECURSION(cx, return JitExec_Error);
IonScript* ion = fun->nonLazyScript()->ionScript();
RootedScript script(cx, fun->nonLazyScript());
IonScript* ion = script->ionScript();
JitCode* code = ion->method();
void* jitcode = code->raw();
MOZ_ASSERT(jit::IsIonEnabled(cx));
MOZ_ASSERT(!ion->bailoutExpected());
JitActivation activation(cx);
JitActivation activation(cx, CalleeToToken(script));
EnterJitCode enter = cx->runtime()->jitRuntime()->enterIon();
void* calleeToken = CalleeToToken(fun, /* constructing = */ false);

View File

@ -23,6 +23,8 @@ namespace js {
namespace js {
namespace jit {
typedef void * CalleeToken;
enum FrameType
{
// A JS frame is analagous to a js::InterpreterFrame, representing one scripted

View File

@ -18,8 +18,6 @@
namespace js {
namespace jit {
typedef void * CalleeToken;
enum CalleeTokenTag
{
CalleeToken_Function = 0x0, // untagged

View File

@ -4512,6 +4512,205 @@ ReflectTrackedOptimizations(JSContext* cx, unsigned argc, Value* vp)
return true;
}
namespace shell {
class ShellAutoEntryMonitor : JS::dbg::AutoEntryMonitor {
Vector<UniqueChars, 1, js::SystemAllocPolicy> log;
bool oom;
bool enteredWithoutExit;
public:
explicit ShellAutoEntryMonitor(JSContext *cx)
: AutoEntryMonitor(cx),
oom(false),
enteredWithoutExit(false)
{ }
~ShellAutoEntryMonitor() {
MOZ_ASSERT(!enteredWithoutExit);
}
void Entry(JSContext* cx, JSFunction* function) override {
MOZ_ASSERT(!enteredWithoutExit);
enteredWithoutExit = true;
RootedString displayId(cx, JS_GetFunctionDisplayId(function));
if (displayId) {
UniqueChars displayIdStr(JS_EncodeStringToUTF8(cx, displayId));
oom = !displayIdStr || !log.append(mozilla::Move(displayIdStr));
return;
}
oom = !log.append(make_string_copy("anonymous"));
}
void Entry(JSContext* cx, JSScript* script) override {
MOZ_ASSERT(!enteredWithoutExit);
enteredWithoutExit = true;
UniqueChars label(JS_smprintf("eval:%s", JS_GetScriptFilename(script)));
oom = !label || !log.append(mozilla::Move(label));
}
void Exit(JSContext* cx) {
MOZ_ASSERT(enteredWithoutExit);
enteredWithoutExit = false;
}
bool buildResult(JSContext *cx, MutableHandleValue resultValue) {
if (oom) {
JS_ReportOutOfMemory(cx);
return false;
}
RootedObject result(cx, JS_NewArrayObject(cx, log.length()));
if (!result)
return false;
for (size_t i = 0; i < log.length(); i++) {
char *name = log[i].get();
RootedString string(cx, Atomize(cx, name, strlen(name)));
if (!string)
return false;
RootedValue value(cx, StringValue(string));
if (!JS_SetElement(cx, result, i, value))
return false;
}
resultValue.setObject(*result.get());
return true;
}
};
} // namespace shell
static bool
EntryPoints(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1) {
JS_ReportError(cx, "Wrong number of arguments");
return false;
}
RootedObject opts(cx, ToObject(cx, args[0]));
if (!opts)
return false;
// { function: f } --- Call f.
{
RootedValue fun(cx), dummy(cx);
if (!JS_GetProperty(cx, opts, "function", &fun))
return false;
if (!fun.isUndefined()) {
shell::ShellAutoEntryMonitor sarep(cx);
if (!Call(cx, UndefinedHandleValue, fun, JS::HandleValueArray::empty(), &dummy))
return false;
return sarep.buildResult(cx, args.rval());
}
}
// { object: o, property: p, value: v } --- Fetch o[p], or if
// v is present, assign o[p] = v.
{
RootedValue objectv(cx), propv(cx), valuev(cx);
if (!JS_GetProperty(cx, opts, "object", &objectv) ||
!JS_GetProperty(cx, opts, "property", &propv))
return false;
if (!objectv.isUndefined() && !propv.isUndefined()) {
RootedObject object(cx, ToObject(cx, objectv));
if (!object)
return false;
RootedString string(cx, ToString(cx, propv));
if (!string)
return false;
RootedId id(cx);
if (!JS_StringToId(cx, string, &id))
return false;
if (!JS_GetProperty(cx, opts, "value", &valuev))
return false;
shell::ShellAutoEntryMonitor sarep(cx);
if (!valuev.isUndefined()) {
if (!JS_SetPropertyById(cx, object, id, valuev))
return false;
} else {
if (!JS_GetPropertyById(cx, object, id, &valuev))
return false;
}
return sarep.buildResult(cx, args.rval());
}
}
// { ToString: v } --- Apply JS::ToString to v.
{
RootedValue v(cx);
if (!JS_GetProperty(cx, opts, "ToString", &v))
return false;
if (!v.isUndefined()) {
shell::ShellAutoEntryMonitor sarep(cx);
if (!JS::ToString(cx, v))
return false;
return sarep.buildResult(cx, args.rval());
}
}
// { ToNumber: v } --- Apply JS::ToNumber to v.
{
RootedValue v(cx);
double dummy;
if (!JS_GetProperty(cx, opts, "ToNumber", &v))
return false;
if (!v.isUndefined()) {
shell::ShellAutoEntryMonitor sarep(cx);
if (!JS::ToNumber(cx, v, &dummy))
return false;
return sarep.buildResult(cx, args.rval());
}
}
// { eval: code } --- Apply ToString and then Evaluate to code.
{
RootedValue code(cx), dummy(cx);
if (!JS_GetProperty(cx, opts, "eval", &code))
return false;
if (!code.isUndefined()) {
RootedString codeString(cx, ToString(cx, code));
if (!codeString || !codeString->ensureFlat(cx))
return false;
AutoStableStringChars stableChars(cx);
if (!stableChars.initTwoByte(cx, codeString))
return false;
const char16_t* chars = stableChars.twoByteRange().start().get();
size_t length = codeString->length();
CompileOptions options(cx);
options.setIntroductionType("entryPoint eval")
.setFileAndLine("entryPoint eval", 1);
shell::ShellAutoEntryMonitor sarep(cx);
if (!JS::Evaluate(cx, options, chars, length, &dummy))
return false;
return sarep.buildResult(cx, args.rval());
}
}
JS_ReportError(cx, "bad 'params' object");
return false;
}
static const JSFunctionSpecWithHelp shell_functions[] = {
JS_FN_HELP("version", Version, 0, 0,
"version([number])",
@ -4867,6 +5066,30 @@ static const JSFunctionSpecWithHelp shell_functions[] = {
" Return an int32 value which corresponds to the offset of the latest stack\n"
" pointer, such that one can take the differences of 2 to estimate a frame-size."),
JS_FN_HELP("entryPoints", EntryPoints, 1, 0,
"entryPoints(params)",
"Carry out some JSAPI operation as directed by |params|, and return an array of\n"
"objects describing which JavaScript entry points were invoked as a result.\n"
"|params| is an object whose properties indicate what operation to perform. Here\n"
"are the recognized groups of properties:\n"
"\n"
"{ function }: Call the object |params.function| with no arguments.\n"
"\n"
"{ object, property }: Fetch the property named |params.property| of\n"
"|params.object|.\n"
"\n"
"{ ToString }: Apply JS::ToString to |params.toString|.\n"
"\n"
"{ ToNumber }: Apply JS::ToNumber to |params.toNumber|.\n"
"\n"
"{ eval }: Apply JS::Evaluate to |params.eval|.\n"
"\n"
"The return value is an array of strings, with one element for each\n"
"JavaScript invocation that occurred as a result of the given\n"
"operation. Each element is the name of the function invoked, or the\n"
"string 'eval:FILENAME' if the code was invoked by 'eval' or something\n"
"similar.\n"),
JS_FS_HELP_END
};

View File

@ -39,6 +39,7 @@
using namespace js;
using JS::dbg::AutoEntryMonitor;
using JS::dbg::Builder;
using js::frontend::IsIdentifier;
using mozilla::ArrayLength;
@ -7801,6 +7802,21 @@ Builder::newObject(JSContext* cx)
return Object(cx, *this, obj);
}
/*** JS::dbg::AutoEntryMonitor ******************************************************************/
AutoEntryMonitor::AutoEntryMonitor(JSContext* cx)
: runtime_(cx->runtime()),
savedMonitor_(cx->runtime()->entryMonitor)
{
runtime_->entryMonitor = this;
}
AutoEntryMonitor::~AutoEntryMonitor()
{
runtime_->entryMonitor = savedMonitor_;
}
/*** Glue ****************************************************************************************/

View File

@ -191,7 +191,7 @@ DebuggerMemory::drainAllocationsLog(JSContext* cx, unsigned argc, Value* vp)
return false;
// Don't pop the AllocationSite yet. The queue's links are followed by
// the GC to find the AllocationSite, but are not barried, so we must
// the GC to find the AllocationSite, but are not barriered, so we must
// edit them with great care. Use the queue entry in place, and then
// pop and delete together.
Debugger::AllocationSite* allocSite = dbg->allocationsLog.getFirst();

View File

@ -126,6 +126,7 @@ JSRuntime::JSRuntime(JSRuntime* parentRuntime)
asmJSActivationStack_(nullptr),
asyncStackForNewActivations(nullptr),
asyncCauseForNewActivations(nullptr),
entryMonitor(nullptr),
parentRuntime(parentRuntime),
interrupt_(false),
telemetryCallback(nullptr),

View File

@ -680,6 +680,9 @@ struct JSRuntime : public JS::shadow::Runtime,
*/
JSString* asyncCauseForNewActivations;
/* If non-null, report JavaScript entry points to this monitor. */
JS::dbg::AutoEntryMonitor* entryMonitor;
js::Activation* const* addressOfActivation() const {
return &activation_;
}

View File

@ -16,6 +16,7 @@
#include "jit/BaselineFrame.h"
#include "jit/RematerializedFrame.h"
#include "js/Debug.h"
#include "vm/GeneratorObject.h"
#include "vm/ScopeObject.h"
@ -846,10 +847,12 @@ Activation::Activation(JSContext* cx, Kind kind)
hideScriptedCallerCount_(0),
asyncStack_(cx, cx->runtime_->asyncStackForNewActivations),
asyncCause_(cx, cx->runtime_->asyncCauseForNewActivations),
entryMonitor_(cx->runtime_->entryMonitor),
kind_(kind)
{
cx->runtime_->asyncStackForNewActivations = nullptr;
cx->runtime_->asyncCauseForNewActivations = nullptr;
cx->runtime_->entryMonitor = nullptr;
cx->runtime_->activation_ = this;
}
@ -859,6 +862,7 @@ Activation::~Activation()
MOZ_ASSERT(cx_->runtime_->activation_ == this);
MOZ_ASSERT(hideScriptedCallerCount_ == 0);
cx_->runtime_->activation_ = prev_;
cx_->runtime_->entryMonitor = entryMonitor_;
cx_->runtime_->asyncCauseForNewActivations = asyncCause_;
cx_->runtime_->asyncStackForNewActivations = asyncStack_;
}
@ -896,6 +900,13 @@ InterpreterActivation::InterpreterActivation(RunState& state, JSContext* cx,
regs_.prepareToRun(*entryFrame, state.script());
MOZ_ASSERT(regs_.pc == state.script()->code());
MOZ_ASSERT_IF(entryFrame_->isEvalFrame(), state.script()->isActiveEval());
if (entryMonitor_) {
if (entryFrame->isFunctionFrame())
entryMonitor_->Entry(cx_, entryFrame->fun());
else
entryMonitor_->Entry(cx_, entryFrame->script());
}
}
InterpreterActivation::~InterpreterActivation()
@ -908,6 +919,9 @@ InterpreterActivation::~InterpreterActivation()
MOZ_ASSERT(oldFrameCount_ == cx->runtime()->interpreterStack().frameCount_);
MOZ_ASSERT_IF(oldFrameCount_ == 0, cx->runtime()->interpreterStack().allocator_.used() == 0);
if (entryMonitor_)
entryMonitor_->Exit(cx_);
if (entryFrame_)
cx->runtime()->interpreterStack().releaseFrame(entryFrame_);
}

View File

@ -1388,7 +1388,7 @@ AbstractFramePtr::hasPushedSPSFrame() const
return false;
}
jit::JitActivation::JitActivation(JSContext* cx, bool active)
jit::JitActivation::JitActivation(JSContext* cx, CalleeToken entryPoint, bool active)
: Activation(cx, Jit),
active_(active),
isLazyLinkExitFrame_(false),
@ -1411,10 +1411,22 @@ jit::JitActivation::JitActivation(JSContext* cx, bool active)
prevJitJSContext_ = nullptr;
prevJitActivation_ = nullptr;
}
if (entryMonitor_) {
MOZ_ASSERT(entryPoint);
if (CalleeTokenIsFunction(entryPoint))
entryMonitor_->Entry(cx_, CalleeTokenToFunction(entryPoint));
else
entryMonitor_->Entry(cx_, CalleeTokenToScript(entryPoint));
}
}
jit::JitActivation::~JitActivation()
{
if (entryMonitor_)
entryMonitor_->Exit(cx_);
if (active_) {
if (isProfiling())
unregisterProfiling();

View File

@ -21,6 +21,12 @@
struct JSCompartment;
namespace JS {
namespace dbg {
class AutoEntryMonitor;
}
}
namespace js {
class ArgumentsObject;
@ -1091,6 +1097,11 @@ class Activation
// Value of asyncCause to be attached to asyncStack_.
RootedString asyncCause_;
// The entry point monitor that was set on cx_->runtime() when this
// Activation was created. Subclasses should report their entry frame's
// function or script here.
JS::dbg::AutoEntryMonitor* entryMonitor_;
enum Kind { Interpreter, Jit, AsmJS };
Kind kind_;
@ -1340,7 +1351,11 @@ class JitActivation : public Activation
#endif
public:
explicit JitActivation(JSContext* cx, bool active = true);
// If non-null, |entryScript| should be the script we're about to begin
// executing, for the benefit of performance tooling. We can pass null for
// entryScript when we know we couldn't possibly be entering JS directly
// from the JSAPI: OSR, asm.js -> Ion transitions, and so on.
explicit JitActivation(JSContext* cx, CalleeToken entryPoint, bool active = true);
~JitActivation();
bool isActive() const {