Page MenuHomePhabricator

[NFC} Move ThreadPlans stacks into their own class, store it in Process by TID

Authored by jingham on Mar 9 2020, 3:50 PM.



This is the third in the series of patches that move the ThreadPlan stacks out of the Threads and lets the process manage them instead. The first two are:

This patch just moves the three thread plan stacks - current, discarded and completed - out of ThreadPlan and into the ThreadPlanStack class. This is a nice cleanup on its own as it gets all the thread plan manipulation logic out of Thread. Then the ThreadPlanStacks move out of Thread to Process where they are managed in a map ordered by TID.

Diff Detail

Event Timeline

jingham created this revision.Mar 9 2020, 3:50 PM
Herald added a project: Restricted Project. · View Herald TranscriptMar 9 2020, 3:50 PM
jingham updated this revision to Diff 249257.Mar 9 2020, 5:51 PM

As we've seen, I don't know much about thread plans so I don't have much to say on the higher-level aspects of this patch.

But I see a bunch of opportunities to make this more consistent with llvm code style.


the include guards were recently normalized to LLDB_TARGET_THREADPLANSTACK_H


superfluous semicolon


It's not clear to me what is the value of this class. Most of it is just about wrapping the underlying container in a slightly different way. If the ThreadDestroyed method can be called in the ThreadPlanStack destructor (which appears to be possible and would be a nice clean up regardless) then RemoveTID would boil down to container.erase(tid). At that point the only non-trivial piece of functionality is the Update function (which this patch doesn't even implement), which could just as well be implemented outside of the class.


This error message doesn't really make sense for a function like this. What option is it talking about?

As the only reason this can fail is because there are no plans for the given tid, maybe you don't actually need the Expected, and can signal that with a simple nullptr?


What's the reason for the introduction of the "checkpoint" indirection? Does it have something to do with needing to update the checkpointed plans when the thread disappears?


You don't modify the callers shared pointer => const ThreadPlanSP & or a ThreadPlanSP with a PushPlan(std::move(thread_plan_sp))`.




maybe return cantFail(GetProcess()->FindThreadPlans(GetID()));


The usual way to write that is assert(result == m_completed_plan_store.end() && "Asked for a checkpoint that didn't exist"). llvm_unreachable is used where an existing control flow path needs to be declared invalid. We don't introduce new control flow just for the sake of using it.


Please put the actual type (ThreadPlanSP ?) instead of auto.


assert instead of branch-to-unreachable


This is the usual place where one would add a llvm_unreachable. Some compilers do not consider the above switch to be fully covered (and if we are to be fully pedantic, it isn't) and will complain about falling off of the end of a non-void function.

jingham updated this revision to Diff 249515.Mar 10 2020, 4:20 PM
jingham marked 3 inline comments as done.

Addressed Pavel's review comments.

jingham marked 9 inline comments as done.Mar 10 2020, 4:24 PM

Addressed Pavel's review comments.


Mostly because if not here then all this logic goes in Process and that is already pretty huge. This isn't a class that we will turn around and wire the methods through process, so I think it helps keep a little more organization.

The update task is trivial at this point because there aren't actually any plan stacks that outlive their threads, but it will grow as we decide when we want to do about this.

I also plan (though I haven't done it yet) to move thread plan listing/deleting/etc to go through here. Right now the "thread plan list" & Co. commands go through the thread list to individual threads, but that means you can't get at thread plan stacks for unreported threads, which doesn't seem right. That belongs to the map of threads.


I originally had this as a pointer that returns NULL when not found, but I thought we were going towards Expected's for everything that might not produce a value. I'm happier with it as just a pointer, so I put it back to that.



This is actually a feature that was present but more or less buried in the old way of doing things. The problem it addresses is:

  1. You stop at the end of a step over
  2. You run an expression, which also stops because it hit the breakpoint at _start
  3. The user does "thread list".

What do you show as the stop reason for the thread? The answer is "step over" not "expression completed". This is not as simple as it seems, since the expression evaluation could push lots of plans on the stack. For instance, the expression evaluation could hit a breakpoint, the user could step around a bit, run some more function, etc. But when the expression evaluation eventually unwinds, we need to restore the original "step over" as the completed plan that gets reported as the "completed plan for this stop."

This part of the job (you also needed to update a few other things) used to be managed in the Thread separate from the rest of the SavedState. That wasn't very clean, and since Threads might go away is also not safe. I moved it into the saved state first because you have to if the Thread might go away, but also because it makes more sense where it is.


I added a default, but what do you mean by "if we are to be fully pedantic, it isn't"? It's an enum with 3 cases, all of which are listed. How is that not fully covered?

jingham updated this revision to Diff 249525.Mar 10 2020, 5:20 PM

Fix some clang-format nits. I didn't change:

condition1 && condition2

&& condition3


condition1 && condition2 &&


as it suggests. We use that all over the place in lldb, and I much prefer it. It makes it clear that you are looking at a continuation of a logical expression, which scanning to the end of the line to find the && is much harder to see.

We use that all over the place in lldb, and I much prefer it.

I'm afraid that's no longer true. I count 30 instances of && or || at the beginning of a line versus 2343 instances of it being at the end. One of the main advantages of having a tool like clang-format is that you don't have to argue about the right way to format something. It may not produce the nicest possible layout all the time, but it will be close enough most of the time. And it will be consistent...

Overall, I'd just recommend getting into the habit of running clang-format before submitting a patch. I noticed a bunch of inconsistencies in the formatting of this patch. Right now, I only highlighted those that are definitely inferior to what clang-format would produce.


this should be indented


inconsistent indentation


here too


I don't think we want to use Expected everywhere. We definitely trying to avoid returning default-constructed objects and checking them with IsValid or something. And if I am creating a new class, I try to make it so that it does not have an "invalid" state and use Optionals or Expecteds to signal its absence. But pointers are existing type with a well known "invalid" value, so I don't see a problem with using it. Furthermore, with pointers it is possible to express that something is always valid (by using a reference), which is something that isn't doable with a class with an IsValid method.

Expecteds are mostly useful when you actually want to return an error from a function (instead of a Status + default value combo), for instance when there are multiple ways it can fail, and you want to user to know what happened. Or if you think that it is dangerous to ignore possible errors from the function and you want to make sure the user checks for them. For simple getter-like functions, I don't think any of this applies.


I meant assert(thread_plan_sp && "Don't push an empty thread plan.")




The first sentence of cppreference enum definition could be helpful here:

An enumeration is a distinct type whose value is restricted to a range of values (see below for details), which may include several explicitly named constants ("enumerators").

What exactly is the "range of values" of an enum is a complex topic, but suffice to say it usually includes more than the named constants one has mentioned in the enum definition (imagine bitfield enums).
Clang takes the "common sense" approach and assumes that if one does a switch over an enum, he expects it to always have one of the named constants. Gcc follows a stricter interpretation and concludes that there are execution flows which do not return a value from this function and issues a warning. Which behavior is more correct is debatable...

Unfortunately adding a default case is not the right solution here (though I can see how that could be understood from my comment -- sorry), because clang will then warn you about putting a default statement in what it considers to be a fully covered switch. The right solution is to put the llvm_unreachable call *after* the switch statement, which is the layout that will make everyone happy. You don't need (or shouldn't) put the return statement because the compilers know that the macro terminates the progream.

I'm sorry that this turned into such a lesson, but since you were using the macro in an atypical way, I wanted to illustrate what is the more common use of it.

jingham updated this revision to Diff 249674.EditedMar 11 2020, 10:24 AM

Ran clang-format over the new files. Fixed up the weird switch llvm_unreachable construct.

jingham marked an inline comment as done.Mar 11 2020, 10:57 AM
jingham added inline comments.

The "may" in that sentence is apparently the crucial bit, but it didn't explain why an enum specified only by named constants might have other possible values. I read a little further on, but didn't get to the part where it explains that. Seems to me in this case the compiler shouldn't have to to guess about the extent of the valid values of this enum and then be conservative or not in handling the results of its guess. I don't see the point of an enum if a legally constructed enum value (not cast from some random value) isn't exhausted by the constants used to define it. That just seems weird.

But arguing with C++ compiler diagnostics isn't a productive use of time, and I feel like too deep an understanding of the C++ type system's mysteries is not good for one's mental stability. So I'm more than content to cargo cult this one...

labath accepted this revision.Mar 12 2020, 5:25 AM

This looks good to me, though I can't say I'm that familiar with thread plan intricacies.


Delete the dangling declaration as the current patch does not implement this.


Any chance of calling ThreadDestroyed from ThreadPlanStack destructor, so this can just be m_plan_list.erase(tid) ?


The rough rule is that for enums without a fixed underlying type, the range of valid values is those that are representable by the smallest bitfield capable of holding all enumerators of that enum. For a fixed underlying type, the range is the whole type.

This revision is now accepted and ready to land.Mar 12 2020, 5:25 AM
jingham updated this revision to Diff 250025.Mar 12 2020, 12:29 PM
jingham marked 6 inline comments as done.

Removed ThreadPlanStackMap::Update, fixed a bug in PushPlan (can't std::move the ThreadPlan, THEN log it...)

jingham marked an inline comment as done.Mar 12 2020, 12:31 PM

Removed Update till the next patch, and fixed a bug in PushPlan that preparing the next patch uncovered.


I dislike postponing things that might do needed work (like remove tell all the plans in the stack to remove their breakpoints) till a destructor happens to run. That makes lldb's state harder to reason about.

This revision was automatically updated to reflect the committed changes.