mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 781979 - Part 2: Track the line number for profiling in IonMonkey. r=nbp,djvj
This commit is contained in:
parent
f3cf0ef464
commit
30c4756c19
@ -748,9 +748,11 @@ CodeGenerator::visitCallGeneric(LCallGeneric *call)
|
||||
|
||||
// Finally call the function in objreg.
|
||||
masm.bind(&makeCall);
|
||||
masm.leaveBeforeCall();
|
||||
masm.callIon(objreg);
|
||||
if (!markSafepoint(call))
|
||||
return false;
|
||||
masm.reenterAfterCall();
|
||||
|
||||
// Increment to remove IonFramePrefix; decrement to fill FrameSizeClass.
|
||||
// The return address has already been removed from the Ion frame.
|
||||
@ -819,11 +821,13 @@ CodeGenerator::visitCallKnown(LCallKnown *call)
|
||||
masm.Push(Imm32(call->numActualArgs()));
|
||||
masm.Push(calleereg);
|
||||
masm.Push(Imm32(descriptor));
|
||||
masm.leaveBeforeCall();
|
||||
|
||||
// Finally call the function in objreg.
|
||||
masm.callIon(objreg);
|
||||
if (!markSafepoint(call))
|
||||
return false;
|
||||
masm.reenterAfterCall();
|
||||
|
||||
// Increment to remove IonFramePrefix; decrement to fill FrameSizeClass.
|
||||
// The return address has already been removed from the Ion frame.
|
||||
@ -1075,11 +1079,13 @@ CodeGenerator::visitApplyArgsGeneric(LApplyArgsGeneric *apply)
|
||||
}
|
||||
|
||||
masm.bind(&rejoin);
|
||||
masm.leaveBeforeCall();
|
||||
|
||||
// Finally call the function in objreg, as assigned by one of the paths above.
|
||||
masm.callIon(objreg);
|
||||
if (!markSafepoint(apply))
|
||||
return false;
|
||||
masm.reenterAfterCall();
|
||||
|
||||
// Recover the number of arguments from the frame descriptor.
|
||||
masm.movePtr(Address(StackPointer, 0), copyreg);
|
||||
@ -4041,54 +4047,79 @@ CodeGenerator::visitSetDOMProperty(LSetDOMProperty *ins)
|
||||
}
|
||||
|
||||
bool
|
||||
CodeGenerator::visitProfilingEnter(LProfilingEnter *lir)
|
||||
CodeGenerator::visitFunctionBoundary(LFunctionBoundary *lir)
|
||||
{
|
||||
#if 0
|
||||
SPSProfiler *profiler = &gen->compartment->rt->spsProfiler;
|
||||
JS_ASSERT(profiler->enabled());
|
||||
Register temp = ToRegister(lir->temp()->output());
|
||||
|
||||
const char *string = lir->profileString();
|
||||
switch (lir->type()) {
|
||||
case MFunctionBoundary::Inline_Enter:
|
||||
// Multiple scripts can be inlined at one depth, but there is only
|
||||
// one Inline_Exit node to signify this. To deal with this, if we
|
||||
// reach the entry of another inline script on the same level, then
|
||||
// just reset the sps metadata about the frame. We must balance
|
||||
// calls to leave()/reenter(), so perform the balance without
|
||||
// emitting any instrumentation. Technically the previous inline
|
||||
// call at this same depth has reentered, but the instrumentation
|
||||
// will be emitted at the common join point for all inlines at the
|
||||
// same depth.
|
||||
if (sps.inliningDepth() == lir->inlineLevel()) {
|
||||
sps.leaveInlineFrame();
|
||||
sps.skipNextReenter();
|
||||
sps.reenter(masm, temp);
|
||||
}
|
||||
|
||||
Register size = ToRegister(lir->temp1()->output());
|
||||
Register base = ToRegister(lir->temp2()->output());
|
||||
sps.leave(lastPC, masm, temp);
|
||||
if (!sps.enterInlineFrame())
|
||||
return false;
|
||||
// fallthrough
|
||||
|
||||
// Check if there's still space on the stack
|
||||
masm.movePtr(ImmWord(profiler->sizePointer()), size);
|
||||
masm.load32(Address(size, 0), size);
|
||||
Label stackFull;
|
||||
masm.branch32(Assembler::GreaterThanOrEqual, size, Imm32(profiler->maxSize()),
|
||||
&stackFull);
|
||||
case MFunctionBoundary::Enter:
|
||||
if (sps.slowAssertions()) {
|
||||
typedef bool(*pf)(JSContext *, HandleScript);
|
||||
static const VMFunction SPSEnterInfo = FunctionInfo<pf>(SPSEnter);
|
||||
|
||||
// With room, store our string onto the stack
|
||||
masm.movePtr(ImmWord(profiler->stack()), base);
|
||||
JS_STATIC_ASSERT(sizeof(ProfileEntry) == 2 * sizeof(void*));
|
||||
masm.lshiftPtr(Imm32(sizeof(void*) == 4 ? 3 : 4), size);
|
||||
masm.addPtr(size, base);
|
||||
saveLive(lir);
|
||||
pushArg(ImmGCPtr(lir->script()));
|
||||
if (!callVM(SPSEnterInfo, lir))
|
||||
return false;
|
||||
restoreLive(lir);
|
||||
sps.pushManual(lir->script(), masm, temp);
|
||||
return true;
|
||||
}
|
||||
|
||||
masm.storePtr(ImmWord(string), Address(base, offsetof(ProfileEntry, string)));
|
||||
masm.storePtr(ImmWord((uintptr_t) 0), Address(base, offsetof(ProfileEntry, sp)));
|
||||
return sps.push(GetIonContext()->cx, lir->script(), masm, temp);
|
||||
|
||||
// Always increment the stack size (paired with a decrement in pop)
|
||||
masm.bind(&stackFull);
|
||||
masm.movePtr(ImmWord(profiler->sizePointer()), size);
|
||||
Address addr(size, 0);
|
||||
masm.add32(Imm32(1), addr);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
case MFunctionBoundary::Inline_Exit:
|
||||
// all inline returns were covered with ::Exit, so we just need to
|
||||
// maintain the state of inline frames currently active and then
|
||||
// reenter the caller
|
||||
sps.leaveInlineFrame();
|
||||
sps.reenter(masm, temp);
|
||||
return true;
|
||||
|
||||
bool
|
||||
CodeGenerator::visitProfilingExit(LProfilingExit *exit)
|
||||
{
|
||||
#if 0
|
||||
SPSProfiler *profiler = &gen->compartment->rt->spsProfiler;
|
||||
JS_ASSERT(profiler->enabled());
|
||||
Register temp = ToRegister(exit->temp());
|
||||
masm.movePtr(ImmWord(profiler->sizePointer()), temp);
|
||||
Address addr(temp, 0);
|
||||
masm.add32(Imm32(-1), addr);
|
||||
#endif
|
||||
return true;
|
||||
case MFunctionBoundary::Exit:
|
||||
if (sps.slowAssertions()) {
|
||||
typedef bool(*pf)(JSContext *, HandleScript);
|
||||
static const VMFunction SPSExitInfo = FunctionInfo<pf>(SPSExit);
|
||||
|
||||
saveLive(lir);
|
||||
pushArg(ImmGCPtr(lir->script()));
|
||||
// Once we've exited, then we shouldn't emit instrumentation for
|
||||
// the corresponding reenter() because we no longer have a
|
||||
// frame.
|
||||
sps.skipNextReenter();
|
||||
if (!callVM(SPSExitInfo, lir))
|
||||
return false;
|
||||
restoreLive(lir);
|
||||
return true;
|
||||
}
|
||||
|
||||
sps.pop(masm, temp);
|
||||
return true;
|
||||
|
||||
default:
|
||||
JS_NOT_REACHED("invalid LFunctionBoundary type");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ion
|
||||
|
@ -163,8 +163,7 @@ class CodeGenerator : public CodeGeneratorSpecific
|
||||
bool emitInstanceOf(LInstruction *ins, Register rhs);
|
||||
bool visitInstanceOfO(LInstanceOfO *ins);
|
||||
bool visitInstanceOfV(LInstanceOfV *ins);
|
||||
bool visitProfilingEnter(LProfilingEnter *lir);
|
||||
bool visitProfilingExit(LProfilingExit *lir);
|
||||
bool visitFunctionBoundary(LFunctionBoundary *lir);
|
||||
bool visitGetDOMProperty(LGetDOMProperty *lir);
|
||||
bool visitSetDOMProperty(LSetDOMProperty *lir);
|
||||
bool visitCallDOMNative(LCallDOMNative *lir);
|
||||
|
@ -289,13 +289,8 @@ IonBuilder::build()
|
||||
|
||||
// Emit the start instruction, so we can begin real instructions.
|
||||
current->makeStart(MStart::New(MStart::StartType_Default));
|
||||
if (instrumentedProfiling()) {
|
||||
SPSProfiler *profiler = &cx->runtime->spsProfiler;
|
||||
const char *string = profiler->profileString(cx, script, script->function());
|
||||
if (!string)
|
||||
return false;
|
||||
current->add(MProfilingEnter::New(string));
|
||||
}
|
||||
if (instrumentedProfiling())
|
||||
current->add(MFunctionBoundary::New(script, MFunctionBoundary::Enter));
|
||||
|
||||
// Parameters have been checked to correspond to the typeset, now we unbox
|
||||
// what we can in an infallible manner.
|
||||
@ -407,18 +402,19 @@ IonBuilder::buildInline(IonBuilder *callerBuilder, MResumePoint *callerResumePoi
|
||||
|
||||
current->setCallerResumePoint(callerResumePoint);
|
||||
|
||||
// Flag the entry into an inlined function with a special MStart block
|
||||
if (instrumentedProfiling()) {
|
||||
SPSProfiler *profiler = &cx->runtime->spsProfiler;
|
||||
const char *string = profiler->profileString(cx, script, script->function());
|
||||
if (!string)
|
||||
return false;
|
||||
current->add(MProfilingEnter::New(string));
|
||||
}
|
||||
|
||||
// Connect the entrance block to the last block in the caller's graph.
|
||||
MBasicBlock *predecessor = callerBuilder->current;
|
||||
JS_ASSERT(predecessor == callerResumePoint->block());
|
||||
|
||||
// All further instructions generated in from this scope should be
|
||||
// considered as part of the function that we're inlining. We also need to
|
||||
// keep track of the inlining depth because all scripts inlined on the same
|
||||
// level contiguously have only one Inline_Exit node.
|
||||
if (instrumentedProfiling())
|
||||
predecessor->add(MFunctionBoundary::New(script,
|
||||
MFunctionBoundary::Inline_Enter,
|
||||
inliningDepth));
|
||||
|
||||
predecessor->end(MGoto::New(current));
|
||||
if (!current->addPredecessorWithoutPhis(predecessor))
|
||||
return false;
|
||||
@ -2561,7 +2557,7 @@ IonBuilder::processReturn(JSOp op)
|
||||
}
|
||||
|
||||
if (instrumentedProfiling())
|
||||
current->add(MProfilingExit::New());
|
||||
current->add(MFunctionBoundary::New(script, MFunctionBoundary::Exit));
|
||||
MReturn *ret = MReturn::New(def);
|
||||
current->end(ret);
|
||||
|
||||
@ -3229,6 +3225,12 @@ IonBuilder::inlineScriptedCall(AutoObjectVector &targets, uint32 argc, bool cons
|
||||
RootedFunction target(cx, func);
|
||||
if (!jsop_call_inline(target, argc, constructing, constFun, bottom, retvalDefns))
|
||||
return false;
|
||||
|
||||
// The Inline_Enter node is handled by buildInline, we're responsible
|
||||
// for the Inline_Exit node (mostly for the case below)
|
||||
if (instrumentedProfiling())
|
||||
bottom->add(MFunctionBoundary::New(NULL, MFunctionBoundary::Inline_Exit));
|
||||
|
||||
} else {
|
||||
// In the polymorphic case, we end the current block with a MPolyInlineDispatch instruction.
|
||||
|
||||
@ -3254,6 +3256,20 @@ IonBuilder::inlineScriptedCall(AutoObjectVector &targets, uint32 argc, bool cons
|
||||
}
|
||||
top->end(disp);
|
||||
|
||||
// If profiling is enabled, then we need a clear-cut boundary of all of
|
||||
// the inlined functions which is distinct from the fallback path where
|
||||
// no inline functions are entered. In the case that there's a fallback
|
||||
// path and a set of inline functions, we create a new block as a join
|
||||
// point for all of the inline paths which will then go to the real end
|
||||
// block: 'bottom'. This 'inlineBottom' block is never different from
|
||||
// 'bottom' except for this one case where profiling is turned on.
|
||||
MBasicBlock *inlineBottom = bottom;
|
||||
if (instrumentedProfiling() && disp->inlinePropertyTable()) {
|
||||
inlineBottom = newBlock(NULL, pc);
|
||||
if (inlineBottom == NULL)
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < disp->numCallees(); i++) {
|
||||
// Do the inline function build.
|
||||
MConstant *constFun = disp->getFunctionConstant(i);
|
||||
@ -3261,8 +3277,46 @@ IonBuilder::inlineScriptedCall(AutoObjectVector &targets, uint32 argc, bool cons
|
||||
MBasicBlock *block = disp->getSuccessor(i);
|
||||
graph().moveBlockToEnd(block);
|
||||
current = block;
|
||||
|
||||
if (!jsop_call_inline(target, argc, constructing, constFun, bottom, retvalDefns))
|
||||
|
||||
if (!jsop_call_inline(target, argc, constructing, constFun, inlineBottom, retvalDefns))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Regardless of whether inlineBottom != bottom, demarcate these exits
|
||||
// with an Inline_Exit instruction signifying that the inlined functions
|
||||
// on this level have all ceased running.
|
||||
if (instrumentedProfiling())
|
||||
inlineBottom->add(MFunctionBoundary::New(NULL, MFunctionBoundary::Inline_Exit));
|
||||
|
||||
// In the case where we had to create a new block, all of the returns of
|
||||
// the inline functions need to be merged together with a phi node. This
|
||||
// phi node resident in the 'inlineBottom' block is then an input to the
|
||||
// phi node for this entire call sequence in the 'bottom' block.
|
||||
if (inlineBottom != bottom) {
|
||||
graph().moveBlockToEnd(inlineBottom);
|
||||
inlineBottom->inheritSlots(top);
|
||||
if (!inlineBottom->initEntrySlots())
|
||||
return false;
|
||||
|
||||
// Only need to phi returns together if there's more than one
|
||||
if (retvalDefns.length() > 1) {
|
||||
// This is the same depth as the phi node of the 'bottom' block
|
||||
// after all of the 'pops' happen (see pop() sequence below)
|
||||
MPhi *phi = MPhi::New(inlineBottom->stackDepth() - argc - 2);
|
||||
inlineBottom->addPhi(phi);
|
||||
|
||||
for (MDefinition **it = retvalDefns.begin(), **end = retvalDefns.end(); it != end; ++it) {
|
||||
if (!phi->addInput(*it))
|
||||
return false;
|
||||
}
|
||||
// retvalDefns should become a singleton vector of 'phi'
|
||||
retvalDefns.clear();
|
||||
if (!retvalDefns.append(phi))
|
||||
return false;
|
||||
}
|
||||
|
||||
inlineBottom->end(MGoto::New(bottom));
|
||||
if (!bottom->addPredecessorWithoutPhis(inlineBottom))
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -183,10 +183,6 @@ class IonBuilder : public MIRGenerator
|
||||
return js_IonOptions.inlining;
|
||||
}
|
||||
|
||||
bool instrumentedProfiling() {
|
||||
return cx->runtime->spsProfiler.enabled();
|
||||
}
|
||||
|
||||
JSFunction *getSingleCallTarget(uint32 argc, jsbytecode *pc);
|
||||
unsigned getPolyCallTargets(uint32 argc, jsbytecode *pc,
|
||||
AutoObjectVector &targets, uint32_t maxTargets);
|
||||
|
@ -24,6 +24,10 @@
|
||||
|
||||
namespace js {
|
||||
namespace ion {
|
||||
|
||||
class MacroAssembler;
|
||||
|
||||
typedef SPSInstrumentation<MacroAssembler, Register> IonInstrumentation;
|
||||
|
||||
// The public entrypoint for emitting assembly. Note that a MacroAssembler can
|
||||
// use cx->lifoAlloc, so take care not to interleave masm use with other
|
||||
@ -55,10 +59,16 @@ class MacroAssembler : public MacroAssemblerSpecific
|
||||
Maybe<AutoIonContextAlloc> alloc_;
|
||||
bool enoughMemory_;
|
||||
|
||||
private:
|
||||
IonInstrumentation *sps;
|
||||
jsbytecode **pc_;
|
||||
|
||||
public:
|
||||
MacroAssembler()
|
||||
MacroAssembler(IonInstrumentation *sps = NULL, jsbytecode **pc = NULL)
|
||||
: autoRooter_(GetIonContext()->cx, thisFromCtor()),
|
||||
enoughMemory_(true)
|
||||
enoughMemory_(true),
|
||||
sps(sps),
|
||||
pc_(pc)
|
||||
{
|
||||
if (!GetIonContext()->temp)
|
||||
alloc_.construct(GetIonContext()->cx);
|
||||
@ -71,7 +81,9 @@ class MacroAssembler : public MacroAssemblerSpecific
|
||||
// (for example, Trampoline-$(ARCH).cpp).
|
||||
MacroAssembler(JSContext *cx)
|
||||
: autoRooter_(cx, thisFromCtor()),
|
||||
enoughMemory_(true)
|
||||
enoughMemory_(true),
|
||||
sps(NULL),
|
||||
pc_(NULL)
|
||||
{
|
||||
ionContext_.construct(cx, cx->compartment, (js::ion::TempAllocator *)NULL);
|
||||
alloc_.construct(cx);
|
||||
@ -465,6 +477,110 @@ class MacroAssembler : public MacroAssemblerSpecific
|
||||
|
||||
// Generates code used to complete a bailout.
|
||||
void generateBailoutTail(Register scratch);
|
||||
|
||||
// These functions exist as small wrappers around sites where execution can
|
||||
// leave the currently running stream of instructions. They exist so that
|
||||
// instrumentation may be put in place around them if necessary and the
|
||||
// instrumentation is enabled.
|
||||
|
||||
void callWithABI(void *fun, Result result = GENERAL) {
|
||||
leaveBeforeCall();
|
||||
MacroAssemblerSpecific::callWithABI(fun, result);
|
||||
reenterAfterCall();
|
||||
}
|
||||
|
||||
void handleException() {
|
||||
// Re-entry code is irrelevant because the exception will leave the
|
||||
// running function and never come back
|
||||
if (sps)
|
||||
sps->skipNextReenter();
|
||||
leaveBeforeCall();
|
||||
MacroAssemblerSpecific::handleException();
|
||||
// Doesn't actually emit code, but balances the leave()
|
||||
if (sps)
|
||||
sps->reenter(*this, InvalidReg);
|
||||
}
|
||||
|
||||
private:
|
||||
void spsProfileEntryAddress(SPSProfiler *p, int offset, Register temp,
|
||||
Label *full)
|
||||
{
|
||||
movePtr(ImmWord(p->sizePointer()), temp);
|
||||
load32(Address(temp, 0), temp);
|
||||
if (offset != 0)
|
||||
add32(Imm32(offset), temp);
|
||||
branch32(Assembler::GreaterThanOrEqual, temp, Imm32(p->maxSize()), full);
|
||||
|
||||
// 4 * sizeof(void*) * idx = idx << (2 + log(sizeof(void*)))
|
||||
JS_STATIC_ASSERT(sizeof(ProfileEntry) == 4 * sizeof(void*));
|
||||
lshiftPtr(Imm32(2 + (sizeof(void*) == 4 ? 2 : 3)), temp);
|
||||
addPtr(ImmWord(p->stack()), temp);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
// These functions are needed by the IonInstrumentation interface defined in
|
||||
// vm/SPSProfiler.h. They will modify the pseudostack provided to SPS to
|
||||
// perform the actual instrumentation.
|
||||
|
||||
void spsUpdatePCIdx(SPSProfiler *p, int32_t idx, Register temp) {
|
||||
Label stackFull;
|
||||
spsProfileEntryAddress(p, -1, temp, &stackFull);
|
||||
store32(Imm32(idx), Address(temp, ProfileEntry::offsetOfPCIdx()));
|
||||
bind(&stackFull);
|
||||
}
|
||||
|
||||
void spsPushFrame(SPSProfiler *p, const char *str, JSScript *s, Register temp) {
|
||||
Label stackFull;
|
||||
spsProfileEntryAddress(p, 0, temp, &stackFull);
|
||||
|
||||
storePtr(ImmWord(str), Address(temp, ProfileEntry::offsetOfString()));
|
||||
storePtr(ImmGCPtr(s), Address(temp, ProfileEntry::offsetOfScript()));
|
||||
storePtr(ImmWord((void*) NULL),
|
||||
Address(temp, ProfileEntry::offsetOfStackAddress()));
|
||||
store32(Imm32(ProfileEntry::NullPCIndex),
|
||||
Address(temp, ProfileEntry::offsetOfPCIdx()));
|
||||
|
||||
/* Always increment the stack size, tempardless if we actually pushed */
|
||||
bind(&stackFull);
|
||||
movePtr(ImmWord(p->sizePointer()), temp);
|
||||
add32(Imm32(1), Address(temp, 0));
|
||||
}
|
||||
|
||||
void spsPopFrame(SPSProfiler *p, Register temp) {
|
||||
movePtr(ImmWord(p->sizePointer()), temp);
|
||||
add32(Imm32(-1), Address(temp, 0));
|
||||
}
|
||||
|
||||
// These two functions are helpers used around call sites throughout the
|
||||
// assembler. They are mainly called from the call wrappers above, but can
|
||||
// also be found within callVM and around callIon.
|
||||
|
||||
void leaveBeforeCall() {
|
||||
if (!sps || !sps->enabled())
|
||||
return;
|
||||
GeneralRegisterSet regs(Registers::TempMask);
|
||||
Register r = regs.getAny();
|
||||
push(r);
|
||||
sps->leave(*pc_, *this, r);
|
||||
pop(r);
|
||||
}
|
||||
|
||||
void reenterAfterCall() {
|
||||
if (!sps || !sps->enabled())
|
||||
return;
|
||||
GeneralRegisterSet regs(Registers::TempMask & ~Registers::JSCallMask &
|
||||
~Registers::CallMask);
|
||||
if (regs.empty()) {
|
||||
regs = GeneralRegisterSet(Registers::TempMask);
|
||||
Register r = regs.getAny();
|
||||
push(r);
|
||||
sps->reenter(*this, r);
|
||||
pop(r);
|
||||
} else {
|
||||
sps->reenter(*this, regs.getAny());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ion
|
||||
|
@ -2979,41 +2979,30 @@ class LInstanceOfV : public LInstructionHelper<1, BOX_PIECES+1, 2>
|
||||
static const size_t RHS = BOX_PIECES;
|
||||
};
|
||||
|
||||
class LProfilingEnter : public LInstructionHelper<0, 0, 2>
|
||||
class LFunctionBoundary : public LInstructionHelper<0, 0, 1>
|
||||
{
|
||||
public:
|
||||
LIR_HEADER(ProfilingEnter);
|
||||
LIR_HEADER(FunctionBoundary);
|
||||
|
||||
LProfilingEnter(const LDefinition &temp1, const LDefinition &temp2) {
|
||||
setTemp(0, temp1);
|
||||
setTemp(1, temp2);
|
||||
}
|
||||
|
||||
const LDefinition *temp1() {
|
||||
return getTemp(0);
|
||||
}
|
||||
|
||||
const LDefinition *temp2() {
|
||||
return getTemp(1);
|
||||
}
|
||||
|
||||
const char *profileString() const {
|
||||
return mir_->toProfilingEnter()->profileString();
|
||||
}
|
||||
};
|
||||
|
||||
class LProfilingExit : public LInstructionHelper<0, 0, 1>
|
||||
{
|
||||
public:
|
||||
LIR_HEADER(ProfilingExit);
|
||||
|
||||
LProfilingExit(const LDefinition &temp) {
|
||||
LFunctionBoundary(const LDefinition &temp) {
|
||||
setTemp(0, temp);
|
||||
}
|
||||
|
||||
const LDefinition *temp() {
|
||||
return getTemp(0);
|
||||
}
|
||||
|
||||
JSScript *script() {
|
||||
return mir_->toFunctionBoundary()->script();
|
||||
}
|
||||
|
||||
MFunctionBoundary::Type type() {
|
||||
return mir_->toFunctionBoundary()->type();
|
||||
}
|
||||
|
||||
unsigned inlineLevel() {
|
||||
return mir_->toFunctionBoundary()->inlineLevel();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
@ -657,26 +657,27 @@ class LInstruction
|
||||
|
||||
class LInstructionVisitor
|
||||
{
|
||||
#ifdef TRACK_SNAPSHOTS
|
||||
LInstruction *ins_;
|
||||
|
||||
protected:
|
||||
jsbytecode *lastPC;
|
||||
|
||||
LInstruction *instruction() {
|
||||
return ins_;
|
||||
}
|
||||
#endif
|
||||
|
||||
public:
|
||||
void setInstruction(LInstruction *ins) {
|
||||
#ifdef TRACK_SNAPSHOTS
|
||||
ins_ = ins;
|
||||
#endif
|
||||
if (ins->mirRaw()) {
|
||||
lastPC = ins->mirRaw()->trackedPc();
|
||||
JS_ASSERT(lastPC != NULL);
|
||||
}
|
||||
}
|
||||
|
||||
LInstructionVisitor()
|
||||
#ifdef TRACK_SNAPSHOTS
|
||||
: ins_(NULL)
|
||||
#endif
|
||||
: ins_(NULL),
|
||||
lastPC(NULL)
|
||||
{}
|
||||
|
||||
public:
|
||||
|
@ -161,8 +161,7 @@
|
||||
_(InstanceOfO) \
|
||||
_(InstanceOfV) \
|
||||
_(InterruptCheck) \
|
||||
_(ProfilingEnter) \
|
||||
_(ProfilingExit) \
|
||||
_(FunctionBoundary) \
|
||||
_(GetDOMProperty) \
|
||||
_(SetDOMProperty) \
|
||||
_(CallDOMNative)
|
||||
|
@ -1808,15 +1808,15 @@ LIRGenerator::visitInstanceOf(MInstanceOf *ins)
|
||||
}
|
||||
|
||||
bool
|
||||
LIRGenerator::visitProfilingEnter(MProfilingEnter *ins)
|
||||
LIRGenerator::visitFunctionBoundary(MFunctionBoundary *ins)
|
||||
{
|
||||
return add(new LProfilingEnter(temp(), temp()), ins);
|
||||
}
|
||||
|
||||
bool
|
||||
LIRGenerator::visitProfilingExit(MProfilingExit *ins)
|
||||
{
|
||||
return add(new LProfilingExit(temp()), ins);
|
||||
LFunctionBoundary *lir = new LFunctionBoundary(temp());
|
||||
if (!add(lir, ins))
|
||||
return false;
|
||||
// If slow assertions are enabled, then this node will result in a callVM
|
||||
// out to a C++ function for the assertions, so we will need a safepoint.
|
||||
return !gen->compartment->rt->spsProfiler.slowAssertionsEnabled() ||
|
||||
assignSafepoint(lir, ins);
|
||||
}
|
||||
|
||||
bool
|
||||
|
@ -176,8 +176,7 @@ class LIRGenerator : public LIRGeneratorSpecific
|
||||
bool visitGetArgument(MGetArgument *ins);
|
||||
bool visitThrow(MThrow *ins);
|
||||
bool visitInstanceOf(MInstanceOf *ins);
|
||||
bool visitProfilingEnter(MProfilingEnter *ins);
|
||||
bool visitProfilingExit(MProfilingExit *ins);
|
||||
bool visitFunctionBoundary(MFunctionBoundary *ins);
|
||||
bool visitSetDOMProperty(MSetDOMProperty *ins);
|
||||
bool visitGetDOMProperty(MGetDOMProperty *ins);
|
||||
};
|
||||
|
@ -239,6 +239,10 @@ class MDefinition : public MNode
|
||||
uint32 virtualRegister_; // Used by lowering to map definitions to virtual registers.
|
||||
};
|
||||
|
||||
// Track bailouts by storing the current pc in MIR instruction. Also used
|
||||
// for profiling and keeping track of what the last known pc was.
|
||||
jsbytecode *trackedPc_;
|
||||
|
||||
private:
|
||||
enum Flag {
|
||||
None = 0,
|
||||
@ -262,21 +266,6 @@ class MDefinition : public MNode
|
||||
flags_ |= flags;
|
||||
}
|
||||
|
||||
#ifdef TRACK_SNAPSHOTS
|
||||
// Track bailouts by storing the current pc in MIR instruction.
|
||||
jsbytecode *trackedPc_;
|
||||
|
||||
public:
|
||||
void setTrackedPc(jsbytecode *pc) {
|
||||
if (!trackedPc_)
|
||||
trackedPc_ = pc;
|
||||
}
|
||||
|
||||
jsbytecode *trackedPc() {
|
||||
return trackedPc_;
|
||||
}
|
||||
#endif
|
||||
|
||||
public:
|
||||
MDefinition()
|
||||
: id_(0),
|
||||
@ -284,10 +273,8 @@ class MDefinition : public MNode
|
||||
range_(),
|
||||
resultType_(MIRType_None),
|
||||
flags_(0),
|
||||
dependency_(NULL)
|
||||
#ifdef TRACK_SNAPSHOTS
|
||||
, trackedPc_(NULL)
|
||||
#endif
|
||||
dependency_(NULL),
|
||||
trackedPc_(NULL)
|
||||
{ }
|
||||
|
||||
virtual Opcode op() const = 0;
|
||||
@ -295,6 +282,14 @@ class MDefinition : public MNode
|
||||
static void PrintOpcodeName(FILE *fp, Opcode op);
|
||||
virtual void printOpcode(FILE *fp);
|
||||
|
||||
void setTrackedPc(jsbytecode *pc) {
|
||||
trackedPc_ = pc;
|
||||
}
|
||||
|
||||
jsbytecode *trackedPc() {
|
||||
return trackedPc_;
|
||||
}
|
||||
|
||||
Range *range() {
|
||||
return &range_;
|
||||
}
|
||||
@ -5292,44 +5287,50 @@ class MNewCallObject : public MUnaryInstruction
|
||||
// Node that represents that a script has begun executing. This comes at the
|
||||
// start of the function and is called once per function (including inline
|
||||
// ones)
|
||||
class MProfilingEnter : public MNullaryInstruction
|
||||
class MFunctionBoundary : public MNullaryInstruction
|
||||
{
|
||||
const char *string_;
|
||||
public:
|
||||
enum Type {
|
||||
Enter, // a function has begun executing and it is not inline
|
||||
Exit, // any function has exited (inlined or normal)
|
||||
Inline_Enter, // an inline function has begun executing
|
||||
|
||||
MProfilingEnter(const char *string) : string_(string) {
|
||||
JS_ASSERT(string != NULL);
|
||||
Inline_Exit // all instructions of an inline function are done, a
|
||||
// return from the inline function could have occurred
|
||||
// before this boundary
|
||||
};
|
||||
|
||||
private:
|
||||
JSScript *script_;
|
||||
Type type_;
|
||||
unsigned inlineLevel_;
|
||||
|
||||
MFunctionBoundary(JSScript *script, Type type, unsigned inlineLevel)
|
||||
: script_(script), type_(type), inlineLevel_(inlineLevel)
|
||||
{
|
||||
JS_ASSERT_IF(type != Inline_Exit, script != NULL);
|
||||
JS_ASSERT_IF(type == Inline_Enter, inlineLevel != 0);
|
||||
setGuard();
|
||||
}
|
||||
|
||||
public:
|
||||
INSTRUCTION_HEADER(ProfilingEnter);
|
||||
INSTRUCTION_HEADER(FunctionBoundary);
|
||||
|
||||
static MProfilingEnter *New(const char *string) {
|
||||
return new MProfilingEnter(string);
|
||||
static MFunctionBoundary *New(JSScript *script, Type type,
|
||||
unsigned inlineLevel = 0) {
|
||||
return new MFunctionBoundary(script, type, inlineLevel);
|
||||
}
|
||||
|
||||
const char *profileString() {
|
||||
return string_;
|
||||
JSScript *script() {
|
||||
return script_;
|
||||
}
|
||||
|
||||
AliasSet getAliasSet() const {
|
||||
return AliasSet::None();
|
||||
}
|
||||
};
|
||||
|
||||
// Pairing of MProfilingEnter. Each Enter is paired with an eventual
|
||||
// Exit. This includes inline functions.
|
||||
class MProfilingExit : public MNullaryInstruction
|
||||
{
|
||||
MProfilingExit() {
|
||||
setGuard();
|
||||
Type type() {
|
||||
return type_;
|
||||
}
|
||||
|
||||
public:
|
||||
INSTRUCTION_HEADER(ProfilingExit);
|
||||
|
||||
static MProfilingExit *New() {
|
||||
return new MProfilingExit();
|
||||
unsigned inlineLevel() {
|
||||
return inlineLevel_;
|
||||
}
|
||||
|
||||
AliasSet getAliasSet() const {
|
||||
|
@ -60,6 +60,10 @@ class MIRGenerator
|
||||
return error_;
|
||||
}
|
||||
|
||||
bool instrumentedProfiling() {
|
||||
return compartment->rt->spsProfiler.enabled();
|
||||
}
|
||||
|
||||
public:
|
||||
JSCompartment *compartment;
|
||||
|
||||
|
@ -132,10 +132,8 @@ MBasicBlock::MBasicBlock(MIRGraph &graph, CompileInfo &info, jsbytecode *pc, Kin
|
||||
mark_(false),
|
||||
immediateDominator_(NULL),
|
||||
numDominated_(0),
|
||||
loopHeader_(NULL)
|
||||
#ifdef TRACK_SNAPSHOTS
|
||||
, trackedPc_(pc)
|
||||
#endif
|
||||
loopHeader_(NULL),
|
||||
trackedPc_(pc)
|
||||
{
|
||||
}
|
||||
|
||||
@ -485,9 +483,7 @@ MBasicBlock::insertBefore(MInstruction *at, MInstruction *ins)
|
||||
ins->setBlock(this);
|
||||
graph().allocDefinitionId(ins);
|
||||
instructions_.insertBefore(at, ins);
|
||||
#ifdef TRACK_SNAPSHOTS
|
||||
ins->setTrackedPc(at->trackedPc());
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
@ -496,9 +492,7 @@ MBasicBlock::insertAfter(MInstruction *at, MInstruction *ins)
|
||||
ins->setBlock(this);
|
||||
graph().allocDefinitionId(ins);
|
||||
instructions_.insertAfter(at, ins);
|
||||
#ifdef TRACK_SNAPSHOTS
|
||||
ins->setTrackedPc(at->trackedPc());
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
@ -508,9 +502,7 @@ MBasicBlock::add(MInstruction *ins)
|
||||
ins->setBlock(this);
|
||||
graph().allocDefinitionId(ins);
|
||||
instructions_.pushBack(ins);
|
||||
#ifdef TRACK_SNAPSHOTS
|
||||
ins->setTrackedPc(trackedPc_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -386,13 +386,15 @@ class MBasicBlock : public TempObject, public InlineListNode<MBasicBlock>
|
||||
|
||||
void dumpStack(FILE *fp);
|
||||
|
||||
#ifdef TRACK_SNAPSHOTS
|
||||
// Track bailouts by storing the current pc in MIR instruction added at this
|
||||
// cycle.
|
||||
// cycle. This is also used for tracking calls when profiling.
|
||||
void updateTrackedPc(jsbytecode *pc) {
|
||||
trackedPc_ = pc;
|
||||
}
|
||||
#endif
|
||||
|
||||
jsbytecode *trackedPc() {
|
||||
return trackedPc_;
|
||||
}
|
||||
|
||||
private:
|
||||
MIRGraph &graph_;
|
||||
@ -422,11 +424,7 @@ class MBasicBlock : public TempObject, public InlineListNode<MBasicBlock>
|
||||
size_t numDominated_;
|
||||
MBasicBlock *loopHeader_;
|
||||
|
||||
#ifdef TRACK_SNAPSHOTS
|
||||
// Track bailouts by storing the current pc in MIR instruction added at this
|
||||
// cycle.
|
||||
jsbytecode *trackedPc_;
|
||||
#endif
|
||||
};
|
||||
|
||||
typedef InlineListIterator<MBasicBlock> MBasicBlockIterator;
|
||||
|
@ -127,8 +127,7 @@ namespace ion {
|
||||
_(Round) \
|
||||
_(InstanceOf) \
|
||||
_(InterruptCheck) \
|
||||
_(ProfilingEnter) \
|
||||
_(ProfilingExit) \
|
||||
_(FunctionBoundary) \
|
||||
_(GetDOMProperty) \
|
||||
_(SetDOMProperty)
|
||||
|
||||
|
@ -358,5 +358,16 @@ NewCallObject(JSContext *cx, HandleShape shape, HandleTypeObject type, HeapSlot
|
||||
return CallObject::create(cx, shape, type, slots, global);
|
||||
}
|
||||
|
||||
bool SPSEnter(JSContext *cx, HandleScript script)
|
||||
{
|
||||
return cx->runtime->spsProfiler.enter(cx, script, script->function());
|
||||
}
|
||||
|
||||
bool SPSExit(JSContext *cx, HandleScript script)
|
||||
{
|
||||
cx->runtime->spsProfiler.exit(cx, script, script->function());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ion
|
||||
} // namespace js
|
||||
|
@ -439,6 +439,9 @@ HeapSlot *NewSlots(JSRuntime *rt, unsigned nslots);
|
||||
JSObject *NewCallObject(JSContext *cx, HandleShape shape, HandleTypeObject type, HeapSlot *slots,
|
||||
HandleObject global);
|
||||
|
||||
bool SPSEnter(JSContext *cx, HandleScript script);
|
||||
bool SPSExit(JSContext *cx, HandleScript script);
|
||||
|
||||
|
||||
} // namespace ion
|
||||
} // namespace js
|
||||
|
@ -132,7 +132,8 @@ class Registers
|
||||
|
||||
// Registers returned from a JS -> C call.
|
||||
static const uint32 CallMask =
|
||||
(1 << Registers::r0);
|
||||
(1 << Registers::r0) |
|
||||
(1 << Registers::r1); // used for double-size returns
|
||||
|
||||
static const uint32 AllocatableMask = AllMask & ~NonAllocatableMask;
|
||||
};
|
||||
|
@ -990,6 +990,9 @@ class MacroAssemblerARMCompat : public MacroAssemblerARM
|
||||
void subPtr(Imm32 imm, const Register dest);
|
||||
void addPtr(Imm32 imm, const Register dest);
|
||||
void addPtr(Imm32 imm, const Address &dest);
|
||||
void addPtr(ImmWord imm, const Register dest) {
|
||||
addPtr(Imm32(imm.value), dest);
|
||||
}
|
||||
|
||||
void setStackArg(const Register ®, uint32 arg);
|
||||
|
||||
|
@ -19,13 +19,17 @@ namespace js {
|
||||
namespace ion {
|
||||
|
||||
CodeGeneratorShared::CodeGeneratorShared(MIRGenerator *gen, LIRGraph &graph)
|
||||
: gen(gen),
|
||||
: oolIns(NULL),
|
||||
masm(&sps, &lastPC),
|
||||
gen(gen),
|
||||
graph(graph),
|
||||
current(NULL),
|
||||
deoptTable_(NULL),
|
||||
#ifdef DEBUG
|
||||
pushedArgs_(0),
|
||||
#endif
|
||||
lastOsiPointOffset_(0),
|
||||
sps(&gen->compartment->rt->spsProfiler),
|
||||
osrEntryOffset_(0),
|
||||
frameDepth_(graph.localSlotCount() * sizeof(STACK_SLOT_SIZE) +
|
||||
graph.argumentSlotCount() * sizeof(Value))
|
||||
@ -40,11 +44,15 @@ CodeGeneratorShared::generateOutOfLineCode()
|
||||
if (!gen->temp().ensureBallast())
|
||||
return false;
|
||||
masm.setFramePushed(outOfLineCode_[i]->framePushed());
|
||||
lastPC = outOfLineCode_[i]->pc();
|
||||
sps.setPushed(outOfLineCode_[i]->script());
|
||||
outOfLineCode_[i]->bind(&masm);
|
||||
|
||||
oolIns = outOfLineCode_[i];
|
||||
if (!outOfLineCode_[i]->generate(this))
|
||||
return false;
|
||||
}
|
||||
oolIns = NULL;
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -53,6 +61,13 @@ bool
|
||||
CodeGeneratorShared::addOutOfLineCode(OutOfLineCode *code)
|
||||
{
|
||||
code->setFramePushed(masm.framePushed());
|
||||
// If an OOL instruction adds another OOL instruction, then use the original
|
||||
// instruction's script/pc instead of the basic block's that we're on
|
||||
// because they're probably not relevant any more.
|
||||
if (oolIns)
|
||||
code->setSource(oolIns->script(), oolIns->pc());
|
||||
else
|
||||
code->setSource(current ? current->mir()->info().script() : NULL, lastPC);
|
||||
return outOfLineCode_.append(code);
|
||||
}
|
||||
|
||||
@ -343,6 +358,7 @@ CodeGeneratorShared::callVM(const VMFunction &fun, LInstruction *ins, const Regi
|
||||
IonCode *wrapper = ion->generateVMWrapper(cx, fun);
|
||||
if (!wrapper)
|
||||
return false;
|
||||
masm.leaveBeforeCall();
|
||||
|
||||
// Call the wrapper function. The wrapper is in charge to unwind the stack
|
||||
// when returning from the call. Failures are handled with exceptions based
|
||||
@ -355,6 +371,7 @@ CodeGeneratorShared::callVM(const VMFunction &fun, LInstruction *ins, const Regi
|
||||
|
||||
if (!markSafepoint(ins))
|
||||
return false;
|
||||
masm.reenterAfterCall();
|
||||
|
||||
// Remove rest of the frame left on the stack. We remove the return address
|
||||
// which is implicitly poped when returning.
|
||||
|
@ -33,6 +33,7 @@ class OutOfLineTruncateSlow;
|
||||
class CodeGeneratorShared : public LInstructionVisitor
|
||||
{
|
||||
js::Vector<OutOfLineCode *, 0, SystemAllocPolicy> outOfLineCode_;
|
||||
OutOfLineCode *oolIns;
|
||||
|
||||
protected:
|
||||
MacroAssembler masm;
|
||||
@ -64,6 +65,8 @@ class CodeGeneratorShared : public LInstructionVisitor
|
||||
// List of stack slots that have been pushed as arguments to an MCall.
|
||||
js::Vector<uint32, 0, SystemAllocPolicy> pushedArgumentSlots_;
|
||||
|
||||
IonInstrumentation sps;
|
||||
|
||||
protected:
|
||||
// The offset of the first instruction of the OSR entry block from the
|
||||
// beginning of the code buffer.
|
||||
@ -290,10 +293,14 @@ class OutOfLineCode : public TempObject
|
||||
Label entry_;
|
||||
Label rejoin_;
|
||||
uint32 framePushed_;
|
||||
jsbytecode *pc_;
|
||||
JSScript *script_;
|
||||
|
||||
public:
|
||||
OutOfLineCode()
|
||||
: framePushed_(0)
|
||||
: framePushed_(0),
|
||||
pc_(NULL),
|
||||
script_(NULL)
|
||||
{ }
|
||||
|
||||
virtual bool generate(CodeGeneratorShared *codegen) = 0;
|
||||
@ -313,6 +320,16 @@ class OutOfLineCode : public TempObject
|
||||
uint32 framePushed() const {
|
||||
return framePushed_;
|
||||
}
|
||||
void setSource(JSScript *script, jsbytecode *pc) {
|
||||
script_ = script;
|
||||
pc_ = pc;
|
||||
}
|
||||
jsbytecode *pc() {
|
||||
return pc_;
|
||||
}
|
||||
JSScript *script() {
|
||||
return script_;
|
||||
}
|
||||
};
|
||||
|
||||
// For OOL paths that want a specific-typed code generator.
|
||||
|
@ -53,7 +53,7 @@ bool
|
||||
LIRGeneratorX86Shared::visitInterruptCheck(MInterruptCheck *ins)
|
||||
{
|
||||
LInterruptCheck *lir = new LInterruptCheck();
|
||||
if (!add(lir))
|
||||
if (!add(lir, ins))
|
||||
return false;
|
||||
if (!assignSafepoint(lir, ins))
|
||||
return false;
|
||||
|
@ -351,6 +351,11 @@ class MacroAssemblerX64 : public MacroAssemblerX86Shared
|
||||
void addPtr(Imm32 imm, const Address &dest) {
|
||||
addq(imm, Operand(dest));
|
||||
}
|
||||
void addPtr(ImmWord imm, const Register &dest) {
|
||||
JS_ASSERT(dest != ScratchReg);
|
||||
movq(imm, ScratchReg);
|
||||
addq(ScratchReg, dest);
|
||||
}
|
||||
void subPtr(Imm32 imm, const Register &dest) {
|
||||
subq(imm, dest);
|
||||
}
|
||||
|
@ -381,6 +381,9 @@ class MacroAssemblerX86 : public MacroAssemblerX86Shared
|
||||
void addPtr(Imm32 imm, const Register &dest) {
|
||||
addl(imm, dest);
|
||||
}
|
||||
void addPtr(ImmWord imm, const Register &dest) {
|
||||
addl(Imm32(imm.value), dest);
|
||||
}
|
||||
void addPtr(Imm32 imm, const Address &dest) {
|
||||
addl(imm, Operand(dest));
|
||||
}
|
||||
|
@ -297,41 +297,6 @@ class SPSInstrumentation
|
||||
FrameState *frame;
|
||||
|
||||
public:
|
||||
/*
|
||||
* SPS instruments calls to track leaving and reentering a function. A call
|
||||
* is one made to C++ other other JS. This is a helper class to assist with
|
||||
* the calls to leave()/reenter() to an SPSInstrumentation instance. It uses
|
||||
* RAII to invoke leave() on construction and reenter() on destruction.
|
||||
*/
|
||||
class CallScope
|
||||
{
|
||||
SPSInstrumentation *sps;
|
||||
jsbytecode *pc;
|
||||
Assembler &masm;
|
||||
Register reg;
|
||||
JS_DECL_USE_GUARD_OBJECT_NOTIFIER
|
||||
public:
|
||||
|
||||
/*
|
||||
* Each parameter will be passed along to the instrumentation's
|
||||
* leave()/reenter() methods. The given instrumentation can be NULL, in
|
||||
* which case this object will do nothing.
|
||||
*/
|
||||
CallScope(SPSInstrumentation *sps, jsbytecode *pc, Assembler &masm,
|
||||
Register reg JS_GUARD_OBJECT_NOTIFIER_PARAM)
|
||||
: sps(sps), pc(pc), masm(masm), reg(reg)
|
||||
{
|
||||
JS_GUARD_OBJECT_NOTIFIER_INIT;
|
||||
if (sps)
|
||||
sps->leave(pc, masm, reg);
|
||||
}
|
||||
|
||||
~CallScope() {
|
||||
if (sps)
|
||||
sps->reenter(masm, reg);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Creates instrumentation which writes information out the the specified
|
||||
* profiler's stack and constituent fields.
|
||||
@ -403,7 +368,6 @@ class SPSInstrumentation
|
||||
void setPushed(JSScript *script) {
|
||||
if (!enabled())
|
||||
return;
|
||||
JS_ASSERT(frame->script == NULL);
|
||||
JS_ASSERT(frame->left == 0);
|
||||
frame->script = script;
|
||||
}
|
||||
@ -444,8 +408,11 @@ class SPSInstrumentation
|
||||
* corresponding reenter() actually emits instrumentation.
|
||||
*/
|
||||
void leave(jsbytecode *pc, Assembler &masm, Register scratch) {
|
||||
if (enabled() && frame->script && frame->left++ == 0)
|
||||
if (enabled() && frame->script && frame->left++ == 0) {
|
||||
JS_ASSERT(frame->script->code <= pc &&
|
||||
pc < frame->script->code + frame->script->length);
|
||||
masm.spsUpdatePCIdx(profiler_, pc - frame->script->code, scratch);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
Loading…
Reference in New Issue
Block a user