Improve single thread stepping

Let me say what I thought this was going to do, and see if we can converge on something…

One simplification I want to make here is that these “one thread timeouts” are really about preventing stalls. So I think it’s fine that every time we restart we use the same timeout. After all, we want to keep on this thread “as long as it is making progress”. We aren’t saying “I guarantee I will run in one thread state for X seconds”. We’re saying “I take a stall of X seconds to mean this one thread isn’t going to make progress on its own”. So we don’t need to keep track of a decrementing timer or anything global like that. We only need to know that we’re still using the timer.

Suppose the user did a “next”. Then if we were going to use the single thread timeout, we would push a ThreadPlanSingleThreadTimeOut as the leaf node, and continue with only that one thread running. Then in lldb, we’d set ourselves a timer for the one thread interval - if it fires we will send an “interrupt” to the process.

There are two cases now:

1: The process stops before our timer expires:

So ThreadPlanSingleThreadTimeout should always say it explains the stop. Saying you explain the stop doesn’t mean you are the only one that will get to handle the stop. It just means “I have some work to do on this stop”.
ThreadPlanSingleThreadTimeout always has work to do: noting whether the timer has expired or not. If you do this, then MischiefManaged → true should cause the plan to get popped. Then the stop will get propagated up the thread plan stack to the next plan that also explained the stop.

Now there are three sub-cases:

(a) There’s a plan that explained the stop which is a controlling plan and is done with its job:

We wipe our “I was timeout-stepping this thread” info: we aren’t doing that anymore because we’re returning control to the user; and pop the plan as per usual.

(b) There’s an explaining plan but it has more work to do. After all, it may take a series of steps to make our way through a complicated source line. Suppose for instance, we stopped because we instruction single-stepped over a branch, but we the instruction we stepped to is still in the line range we’re next-ing over. Then we have to consult the step next plan to see what to do. It will get asked what to do next in the ordinary course of things, and will set the appropriate breakpoint for the next stage of this step.

Then when we go to continue, the Thread which is getting to run solo will return true for StopOthers and push a ThreadPlanSingleThreadTimeout and continue the process again with a timeout.

(c) The stop reason was not handled by the current controlling plan (e.g. lldb did a next over a function that the user had a breakpoint in). In that case we’re going to stop to return control to the user, so we zero out our timer and stop. I think that’s the most natural behavior. This whole feature is about how thread stepping happens when the user isn’t in control. So it also makes sense to return control over the timeout behavior to them as well. They can choose how to do the next continue of the process.

2: The one thread timer has expired.

We will mark that thread as now in “run all threads” mode. But again, we have to consult the parent plans, because we might accidentally have hit our interrupt timeout when the thread plan was also getting to one of its end stages. For instance, it’s not impossible that lldb sends the interrupt, but before that gets to the debug stub the process already had stopped at one of the controlling plan’s breakpoints.

In this case, we have:

(a) The stop reason was just the interrupt we sent, so we know we should just continue with all threads running. The other plans won’t explain the Interrupt stop reason, so that will propagate to the base thread plan, which can recognize this timer interrupt stop and continue.

(b) We sent the interrupt but got another stop reason back and there’s a controlling plan that explains this and has more work to do, it will continue the process as normal.

(c) Otherwise the unexplained stop reason will make its way to the Base thread plan, which will stop appropriately.

The rule here is that any time we return control to the user, we unset our decision about whether we were timeout-single stepping this thread.

There is one bit you’ll have to watch out for: Normally I try not to starve threads, so if more than one thread says it wants to StopOthers I randomly choose the one that gets to run. But if we are in the mode where Thread A won the right to run solo with a timeout, it needs to keep winning that random choice till it accomplishes its task.

Does this make any sense?