Arquivos
hhvm/hphp/runtime/debugger/cmd/cmd_next.cpp
T
Mike Magruder 4c13ecfd6c Update debugger to track async changes
This diff updates the debugger to be able to debug async functions again, after the most excellent optimizations introduced in D946914. Minor updates to be able to identify an async func outside of the emitter, and minor adjustments to get breakpoints on async function names working again. Async stacks were fine, the test just needed to be updated. Stepping over await/yield used to decode the instruction stream around OpContSuspend to determine where execution would continue, but that was annoying and brittle. I recently gave c_Continuation the ability to predict its next execution offset, so I enhanced that a bit and switched to using that, so the debugger no longer cares about the codegen around OpContSuspend and doesn't care about anything around OpCreateAsync. This fixes stepping over suspends within a continuation. To get across the first await, we now notice a OpCreateAsync, step over that operation, then grab the c_Continuation off the top of the operand stack and ask it where the heck it's going.

Reviewed By: @hermanventer

Differential Revision: D965577
2013-09-13 12:32:30 -07:00

294 linhas
12 KiB
C++

/*
+----------------------------------------------------------------------+
| HipHop for PHP |
+----------------------------------------------------------------------+
| Copyright (c) 2010-2013 Facebook, Inc. (http://www.facebook.com) |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| http://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/
#include "hphp/runtime/debugger/cmd/cmd_next.h"
#include "hphp/runtime/vm/debugger-hook.h"
#include "hphp/runtime/vm/runtime.h"
#include "hphp/runtime/ext/ext_continuation.h"
namespace HPHP { namespace Eval {
///////////////////////////////////////////////////////////////////////////////
TRACE_SET_MOD(debuggerflow);
void CmdNext::help(DebuggerClient& client) {
client.helpTitle("Next Command");
client.helpCmds(
"[n]ext {count=1}", "steps over lines of code",
nullptr
);
client.helpBody(
"Use this command at break to step over lines of code. Specify a "
"count to step over more than one line of code."
);
}
void CmdNext::onSetup(DebuggerProxy& proxy, CmdInterrupt& interrupt) {
TRACE(2, "CmdNext::onSetup\n");
assert(!m_complete); // Complete cmds should not be asked to do work.
m_stackDepth = proxy.getStackDepth();
m_vmDepth = g_vmContext->m_nesting;
m_loc = interrupt.getFileLine();
ActRec *fp = g_vmContext->getFP();
if (!fp) {
// If we have no frame just wait for the next instruction to be interpreted.
m_needsVMInterrupt = true;
return;
}
PC pc = g_vmContext->getPC();
stepCurrentLine(interrupt, fp, pc);
}
void CmdNext::onBeginInterrupt(DebuggerProxy& proxy, CmdInterrupt& interrupt) {
TRACE(2, "CmdNext::onBeginInterrupt\n");
assert(!m_complete); // Complete cmds should not be asked to do work.
ActRec *fp = g_vmContext->getFP();
if (!fp) {
// If we have no frame just wait for the next instruction to be interpreted.
m_needsVMInterrupt = true;
return;
}
PC pc = g_vmContext->getPC();
Unit* unit = fp->m_func->unit();
Offset offset = unit->offsetOf(pc);
TRACE(2, "CmdNext: pc %p, opcode %s at '%s' offset %d\n",
pc, opcodeToName(toOp(*pc)), fp->m_func->fullName()->data(), offset);
int currentVMDepth = g_vmContext->m_nesting;
int currentStackDepth = proxy.getStackDepth();
TRACE(2, "CmdNext: original depth %d:%d, current depth %d:%d\n",
m_vmDepth, m_stackDepth, currentVMDepth, currentStackDepth);
// Where are we on the stack now vs. when we started? Breaking the answer down
// into distinct variables helps the clarity of the algorithm below.
bool deeper = false;
bool originalDepth = false;
if ((currentVMDepth == m_vmDepth) && (currentStackDepth == m_stackDepth)) {
originalDepth = true;
} else if ((currentVMDepth > m_vmDepth) ||
((currentVMDepth == m_vmDepth) &&
(currentStackDepth > m_stackDepth))) {
deeper = true;
}
m_needsVMInterrupt = false; // Will be set again below if still needed.
// First consider if we've got internal breakpoints setup. These are used when
// we can make an accurate prediction of where execution should flow,
// eventually, and when we want to let the program run normally until we get
// there.
if (hasStepOuts() || hasStepCont()) {
TRACE(2, "CmdNext: checking internal breakpoint(s)\n");
if (atStepOutOffset(unit, offset)) {
if (deeper) return; // Recursion
TRACE(2, "CmdNext: hit step-out\n");
} else if (atStepContOffset(unit, offset)) {
if (m_stepContTag != getContinuationTag(fp)) return;
TRACE(2, "CmdNext: hit step-cont\n");
// We're in the continuation we expect. This may be at a
// different stack depth, though, especially if we've moved from
// the original function to the continuation. Update the depth
// accordingly.
if (!originalDepth) {
m_vmDepth = currentVMDepth;
m_stackDepth = currentStackDepth;
deeper = false;
originalDepth = true;
}
} else if (interrupt.getInterruptType() == ExceptionHandler) {
// Entering an exception handler may take us someplace we weren't
// expecting. Adjust internal breakpoints accordingly. First case is easy.
if (deeper) {
TRACE(2, "CmdNext: exception handler, deeper\n");
return;
}
// For step-conts, we ignore handlers at the original level if we're not
// in the original continuation. We don't care about exception handlers
// in continuations being driven at the same level.
if (hasStepCont() && originalDepth &&
(m_stepContTag != getContinuationTag(fp))) {
TRACE(2, "CmdNext: exception handler, original depth, wrong cont\n");
return;
}
// Sometimes we have handlers in generated code, i.e., Continuation::next.
// These just help propagate exceptions so ignore those.
if (fp->m_func->line1() == 0) {
TRACE(2, "CmdNext: exception handler, ignoring func with no source\n");
return;
}
TRACE(2, "CmdNext: exception handler altering expected flow\n");
} else {
// We have internal breakpoints setup, but we haven't hit one yet. Keep
// running until we reach one.
TRACE(2, "CmdNext: waiting to hit internal breakpoint...\n");
return;
}
// We've hit one internal breakpoint at a useful place, or decided we don't,
// need them, so we can remove them all now.
cleanupStepOuts();
cleanupStepCont();
}
if (interrupt.getInterruptType() == ExceptionHandler) {
// If we're about to enter an exception handler we turn interrupts on to
// ensure we stop when control reaches the handler. The normal logic below
// will decide if we're done at that point or not.
TRACE(2, "CmdNext: exception handler\n");
removeLocationFilter();
m_needsVMInterrupt = true;
return;
}
if (m_skippingCreateAsync) {
m_skippingCreateAsync = false;
stepAfterCreateAsync();
return;
}
if (deeper) {
TRACE(2, "CmdNext: deeper, setup step out to get back to original line\n");
setupStepOuts();
// We can nuke the entire location filter here since we'll re-install it
// when we get back to the old level. Keeping it installed may be more
// efficient if we were on a large line, but there is a penalty for every
// opcode executed while it's installed and that's bad if there's a lot of
// code called from that line.
removeLocationFilter();
return;
}
if (originalDepth && (m_loc == interrupt.getFileLine())) {
TRACE(2, "CmdNext: not complete, still on same line\n");
stepCurrentLine(interrupt, fp, pc);
return;
}
TRACE(2, "CmdNext: operation complete.\n");
m_complete = (decCount() == 0);
if (!m_complete) {
TRACE(2, "CmdNext: repeat count > 0, start fresh.\n");
onSetup(proxy, interrupt);
}
}
void CmdNext::stepCurrentLine(CmdInterrupt& interrupt, ActRec* fp, PC pc) {
// Special handling for yields from generators and awaits from
// async. The destination of these instructions is somewhat counter
// intuitive so we take care to ensure that we step to the most
// appropriate place. For yields, we want to land on the next
// statement when driven from a C++ iterator like ASIO. If the
// generator is driven directly from PHP (i.e., a loop calling
// send($foo)) then we'll land back at the callsite of send(). For
// returns from generators, we follow the execution stack for now,
// and end up at the caller of ASIO or send(). For async functions
// stepping over an await, we land on the next statement.
auto op = toOp(*pc);
if (fp->m_func->isGenerator() &&
(op == OpContSuspend || op == OpContSuspendK || op == OpContRetC)) {
TRACE(2, "CmdNext: encountered yield, await or return from generator\n");
// Patch the projected return point(s) in both cases for
// generators, to catch if we exit the the asio iterator or if we
// are being iterated directly by PHP.
if ((op == OpContRetC) || !fp->m_func->isAsync()) setupStepOuts();
op = toOp(*pc);
if (op == OpContSuspend || op == OpContSuspendK) {
// Patch the next normal execution point so we can pickup the stepping
// from there if the caller is C++.
setupStepCont(fp, pc);
}
removeLocationFilter();
return;
} else if (fp->m_func->hasGeneratorAsBody() && (op == OpCreateAsync)) {
// We need to step over this opcode, then grab the continuation
// and setup continuation stepping like we do for OpContSuspend.
TRACE(2, "CmdNext: encountered create async\n");
m_skippingCreateAsync = true;
m_needsVMInterrupt = true;
removeLocationFilter();
return;
}
installLocationFilterForLine(interrupt.getSite());
m_needsVMInterrupt = true;
}
bool CmdNext::hasStepCont() {
return m_stepCont.valid();
}
bool CmdNext::atStepContOffset(Unit* unit, Offset o) {
return m_stepCont.at(unit, o);
}
// A ContSuspend marks a return point from a generator or async
// function. Execution will resume at this function later, and the
// Continuation associated with this function can predict where.
void CmdNext::setupStepCont(ActRec* fp, PC pc) {
// ContSuspend is followed by the label where execution will continue.
DEBUG_ONLY auto ops = reinterpret_cast<const Op*>(pc);
assert(ops[0] == OpContSuspend || ops[0] == OpContSuspendK);
++pc;
int32_t label = decodeVariableSizeImm(&pc);
c_Continuation* cont = frame_continuation(fp);
Offset nextInst = cont->getExecutionOffset(label);
assert(nextInst != InvalidAbsoluteOffset);
m_stepContTag = cont;
TRACE(2, "CmdNext: patch for cont step at '%s' offset %d\n",
fp->m_func->fullName()->data(), nextInst);
m_stepCont = StepDestination(fp->m_func->unit(), nextInst);
}
// A CreateAsync is used in the codegen for an async function to setup
// a Continuation and return a wait handle so execution can continue
// later. We have just completed a CreateAsync, so the new
// Continuation is available, and it can predict where execution will
// resume.
void CmdNext::stepAfterCreateAsync() {
auto topObj = g_vmContext->getStack().topTV()->m_data.pobj;
assert(topObj->instanceof(c_Continuation::s_cls));
auto cont = static_cast<c_Continuation*>(topObj);
auto func = cont->actRec()->m_func;
Offset nextInst = cont->getNextExecutionOffset();
assert(nextInst != InvalidAbsoluteOffset);
m_stepContTag = cont;
TRACE(2, "CmdNext: patch for cont step after CreateAsync at '%s' offset %d\n",
func->fullName()->data(), nextInst);
m_stepCont = StepDestination(func->unit(), nextInst);
}
void CmdNext::cleanupStepCont() {
if (m_stepCont.valid()) {
m_stepCont = StepDestination();
m_stepContTag = nullptr;
}
}
// Use the address of the c_Continuation object as a tag for this stepping
// operation, to ensure we only stop once we're back to the same continuation.
// Since we'll either stop when we get out of whatever is driving this
// continuation, or we'll stop when we get back into it, we know the object
// will remain alive.
void* CmdNext::getContinuationTag(ActRec* fp) {
c_Continuation* cont = frame_continuation(fp);
TRACE(2, "CmdNext: continuation tag %p for %s\n", cont,
cont->t_getorigfuncname()->data());
return cont;
}
///////////////////////////////////////////////////////////////////////////////
}}