This is an archive of the discontinued LLVM Phabricator instance.

[clangd] Build ASTs only with fresh preambles or after building a new preamble
ClosedPublic

Authored by kadircet on Mar 24 2020, 12:30 PM.

Details

Summary

This is another step for out-of-order preamble builds. To keep the
diagnostic behavior same, we only build ASTs either with "reusable" preambles,
the ones that are fully applicable to a given ParseInput, or after building a
new preamble. Which is the same behaviour as what we do today.

ASTs built through preamble callbacks are not cached as they are built on a
different thread and ASTWorker heavily relies on being the only thread updating
cached ASTs. This results in possibly building some ASTs twice (when there's an
immediate read after a preamble built without any write in between).

Diff Detail

Event Timeline

kadircet created this revision.Mar 24 2020, 12:30 PM

So I think we need a careful description of the new model somewhere. Not so much on specific changes/constraints parts of the code operate under, but what it's trying to do.

My best understanding is:

  • In general, read requests are processed in order by the ASTWorker, and get exactly the version of the file when the request was received. That is, the ASTworker queue interleaves updates and reads in the same order as LSP.
  • Preambles are built on the PreambleWorker for a certain version of the file, and may be compatible or incompatible with subsequent versions. If a read is "picky" it may block the ASTWorker queue waiting for a compatible preamble, otherwise it will "patch up" an incompatible one.
  • Diagnostics and indexes are updated opportunistically using onMainAST when an AST is built, but only if the used preamble is compatible. Publishing diagnostics from patched preambles is not allowed, but we also do not block waiting for an up-to-date preamble in order to generate publishable diagnostics. (Exception: if WantDiagnostics=Yes, we block).
  • To ensure forward progress with diagnostics if the queue is full, when preamble is built we immediately build a "golden" AST from that version and publish diagnostics. To ensure diagnostics do not "go backwards", opportunistic diagnostics are suppressed if they don't use the latest preamble (the one with the last golden AST). This means the sequence of ASTs producing diagnostics is neatly partitioned into "epochs" of the preambles, the first in the epoch is always the golden version. (Exception: if WantDiagnostics=no, diagnostics are not emitted for the golden AST). Because these versions are built out-of-order (not interleaved with reads per LSP), the golden AST is not cached and reused for reads.

Is this about right?

Based on this, I do think the model would be much easier to reason about and verify if the golden AST was built on the ASTWorker thread in a queued task, rather than on the PreambleWorker thread in a callback. This means we understand allowed sequencing by looking at the queue rather than reasoning about mutexes/threads with multiple possible interleavings. By pushing the golden AST task to the front of the queue, it's more explicit that this is a priorities/scheduling decision. Following what happened in logs is probably easier due to less interleaving.
(The callback could enqueue the task, or the PreambleWorker could just enqueue the task directly at that point - not sure the callback indirection buys anything).

One thing that seems complicated in the model is that the AST build needs to decide whether to block on a compatible preamble, but it's the following read that determines whether it needs to. In the worst case, the picky read hasn't even been scheduled yet. I think picky reads probably need to block on the target preamble and build their AST themselves if the cached one isn't suitable. If they "miss" their preamble (i.e. preambleVersion >= readVersion but the preamble isn't compatible) then I think we should fail the request (i.e. picky requests may be invalidated by subsequent preamble edits).

So I think we need a careful description of the new model somewhere. Not so much on specific changes/constraints parts of the code operate under, but what it's trying to do.

My best understanding is:

  • In general, read requests are processed in order by the ASTWorker, and get exactly the version of the file when the request was received. That is, the ASTworker queue interleaves updates and reads in the same order as LSP.
  • Preambles are built on the PreambleWorker for a certain version of the file, and may be compatible or incompatible with subsequent versions. If a read is "picky" it may block the ASTWorker queue waiting for a compatible preamble, otherwise it will "patch up" an incompatible one.
  • Diagnostics and indexes are updated opportunistically using onMainAST when an AST is built, but only if the used preamble is compatible. Publishing diagnostics from patched preambles is not allowed, but we also do not block waiting for an up-to-date preamble in order to generate publishable diagnostics. (Exception: if WantDiagnostics=Yes, we block).
  • To ensure forward progress with diagnostics if the queue is full, when preamble is built we immediately build a "golden" AST from that version and publish diagnostics. To ensure diagnostics do not "go backwards", opportunistic diagnostics are suppressed if they don't use the latest preamble (the one with the last golden AST). This means the sequence of ASTs producing diagnostics is neatly partitioned into "epochs" of the preambles, the first in the epoch is always the golden version. (Exception: if WantDiagnostics=no, diagnostics are not emitted for the golden AST). Because these versions are built out-of-order (not interleaved with reads per LSP), the golden AST is not cached and reused for reads.

Is this about right?

Yes that's most of what this patch does. Tried to explain/sum up those in file comments.

Based on this, I do think the model would be much easier to reason about and verify if the golden AST was built on the ASTWorker thread in a queued task, rather than on the PreambleWorker thread in a callback. This means we understand allowed sequencing by looking at the queue rather than reasoning about mutexes/threads with multiple possible interleavings. By pushing the golden AST task to the front of the queue, it's more explicit that this is a priorities/scheduling decision. Following what happened in logs is probably easier due to less interleaving.
(The callback could enqueue the task, or the PreambleWorker could just enqueue the task directly at that point - not sure the callback indirection buys anything).

Done. It is still the callback that enqueues the task, but moved storage of latest build preamble from PreambleThread to ASTWorker to prevent a possible race between currently running update in astworker and build of preamble in preamblethread, as discussed offline.
This also enabled us to cache golden ASTs in case ASTWorker hasn't received any updates in between which gets rid of the additional AST build cost.

One thing that seems complicated in the model is that the AST build needs to decide whether to block on a compatible preamble, but it's the following read that determines whether it needs to. In the worst case, the picky read hasn't even been scheduled yet. I think picky reads probably need to block on the target preamble and build their AST themselves if the cached one isn't suitable. If they "miss" their preamble (i.e. preambleVersion >= readVersion but the preamble isn't compatible) then I think we should fail the request (i.e. picky requests may be invalidated by subsequent preamble edits).

Agreed, but this is not in the scope of this patch, will address that in the upcoming patches after dropping the synchronization between preamble and ast thread. In addition to that i am planning to ensure updates with WantDiagnostics::Yes gets build by making them non-overwritable in PreambleThread. That is any subsequent build requests will block (astworker thread) until update with WantDiags::Yes starts building.

kadircet updated this revision to Diff 252633.Mar 25 2020, 11:24 AM
  • Build golden asts in ASTWorker thread, rather than preamble thread.
  • Explain new model in file comments.
kadircet updated this revision to Diff 252841.Mar 26 2020, 7:24 AM
  • Invalidate cached AST in case of preamble/input changes when building golden ASTs
sammccall added inline comments.Mar 26 2020, 9:26 AM
clang-tools-extra/clangd/TUScheduler.cpp
7–8

This is explaining a pretty complicated thing, so I think it's particularly important to clearly organize the explanation, use consistent terminology, and avoid including unneccesary details.

I'd suggest introducing with paragraphs in this order:

  • TUScheduler and the idea of a worker per TU.
  • ASTWorker thread, the queue of interleaved updates and reads, elision of dead updates.
  • Compatibility of preambles and updates, picky reads.
  • PreambleWorker thread, elision of preambles, golden ASTs.
  • Published ASTs, epochs/monotonicity.
10

nit: name the other thread (there is a second PreambleWorker thread ...)

11

"issues updates" is vague. What about "... enqueues rebuilds on the PreambleWorker thread as preamble becomes stale. Currently, it then immediately blocks on that request."

11–12

I'm not sure why this paragraph is here rather than near the description of the ASTWorker queue.

16–18

I'm not sure what "serving preambles" means.

18

You haven't defined "compatible" anywhere, and it's not obvious what it means.

19–20

I think this no longer belongs here - merge with TUScheduler::Options::AsyncThreadsCount or just drop

22

The wording here (particularly the word "phases") implies sequencing: an update results in a preamble build followed by an ast build in sequence, when in fact it usually results in an ast build and preamble build in parallel (the former usually finishing first) with a second ast build sequenced after the preamble build.

24

This isn't true, an AST is built for an update if it is needed (a read is based on it, wantdiagnostics=yes, it changed the preamble, or wantdiagnostics=maybe and the debounce expired)

28

I'm not sure what compatible means here, but we will certainly build ASTs for incompatible preambles.
(Or is this describing the intermediate state as of this patch, with the plan to rewrite it all next patch? I think we should rather describe the new model, and then document the current deviations from it)

28

I'm not sure what "immediately following" is relative to: if it's after the update() call, it's not immediate - it has to wait in the queue.

28–29

This section is basically obsolete - elision of dead updates is (should be) mentioned above, and the details are stale (e.g. it refers to debounce as a possibility) and documented in-line.

32

"and *the* index *are* updated"... "building *the* AST"

32

you're using "write" and "update" interchangeably here, it would be clearer to be consistent

32

I think "as a result of" is just "after"?

33

Now we're using "updates" to mean something different...

35

It's not clear what this sentence is saying: some code is doing this partitioning?

(I know you're describing a property of the output of the process, but it's not clear from the text)

212

This never varies, why use a callback rather than just calling a function on the ASTWorker?
(I think I asked this in the previous version, but didn't see an answer)

235–237

Took me a while to understand what this comment is saying (why is it talking about ASTWorker queue state?)

Maybe:
// If we're shutting down, don't bother building preambles, reads will be cancelled.

312–313

this seems to never reuse?

312–313

That sounds racy to me - what happens if a new request arrives after build() invalidates but before build() returns?

Seems much more robust for PreambleWorker to hold the last built preamble, and ASTWorker to hold the last usable preamble. It's just a shared_ptr, what's the harm :)

332

storing this separately from the latest preamble (i.e. separately from where the decision is made to build/rebuild) seems a bit suspect (not actually wrong, but points towards storing the latest built preamble here too)

580

this function is getting too long/monolithic, we should find a way to split it up.
ASTTask should be a member function, I think.

580

I also lost track of the control flow halfway through the function, and can't work it out.
I don't know really what to concretely advise, but it needs to be a lot simpler, maybe by finding some better abstractions or just better understanding conceptually what this is doing.

628

You've called this a task, but it never gets scheduled on a queue, just called in various places. This seems like a helper function, and needs a better name.

642

Didn't we decide to make this available only after building the golden AST and publishing diagnostics?

kadircet updated this revision to Diff 253347.Mar 28 2020, 8:27 AM
kadircet marked 26 inline comments as done.
  • Update description and address comments
kadircet added inline comments.Mar 28 2020, 8:28 AM
clang-tools-extra/clangd/TUScheduler.cpp
580

ASTTask should be a member function, I think.

Moved it into a meber named publishDiagnostics

642

as discussed offline, this is harmless since it is being executed on ASTWorker thread and makes new preamble available to code completion/signature help faster.

kadircet updated this revision to Diff 253552.Mar 30 2020, 4:37 AM
  • Make use of a separate queue for golden ASTs to prevent any races that might

occur due PreambleThread finishing multiple preambles before ASTWorker gets to
process others.

kadircet updated this revision to Diff 253556.Mar 30 2020, 4:55 AM
  • Add assertion to explicitly spell out scheduling for golden ASTs.

Had some discussion offline.

  • having ASTWorker not worry about preamble validity before dispatching to preambleworker seems like a win
  • for this, preambleworker needs to call preambleReady whether it's new or not, so ASTWorker can produce diagnostics
  • AST reuse from diagnostics->request seems much more useful than the other way around (e.g. it reduces request latency), so don't bother with the latter. (And we can drop diagnostic computation in some cases)

This yields pseudocode like:

ASTWorker::update(): enqueue({
  currentInputs = inputs
  preambleworker::update(inputs)
})
ASTWorker::runWithAST(): enqueue({
  ast = cache.get()
  if (!ast) patch preamble and build ast
  action(ast)
  cache.put(ast)
})
PreambleWorker::update(): enqueue({
  if (!preamble.compatible(inputs))
    build preamble
  ASTWorker::preambleReady(preamble)
})
ASTWorker::preambleReady(): enqueue({
  preamble = p
  build ast
  publish ast.diagnostics
  if (inputs == currentInputs) cache.put(ast)
  else if (preamble != oldPreamble) cache.get() // force next read to use this preamble
})

(I'm not sure how simple the actual code can be. I do think defining the methods in that order may help readability)

clang-tools-extra/clangd/TUScheduler.cpp
8–10

nit: just "This ASTWorker manages updates..."

8–10

uber-nit: I'd say the scheduler *manages* workers and the worker *processes* updates.
(Just to avoid mixing metaphors)

9

nit: space before open parens (not a big deal, but occurs several times)

16–18

changes latest inputs -> replaces the current parser inputs

("changes" suggests some tricky mutation, and inputs isn't defined here)

17

You need to mention "building an AST" here, as you reference it below.

29–47

nit: we use "main file" or "main-file" much more often than "mainfile".

30

it it isn't -> until?

33

I don't think "usable" is a good name for this, because we *use* preambles that are not "usable".

I think "compatible" or "up-to-date" or "fresh" or so would have enough semantic distance to make this less confusing.

41

Nit: first and third sentences in this paragraph say the same thing, drop the third?

47

This gets a bit far into implementation details (uses of queues etc) and is hard to follow. Maybe:

When a new preamble is built, a "golden" AST is immediately built from that version of the file.
This ensures diagnostics get updated even if the queue is full.

52

This is talking about the subtleties of ordering/version guarantees - not really interesting from the outside, and not enough detail here to avoid reading the code. I'd suggest moving these to preambleReady or so.

221–222

Some params get copied explicitly, others get passed by value. Pick one strategy?

223

nit: WantDiags is an enum, no move

245–246

The prefix "PreambleThread" implies that there's something in the code called PreambleThread, and that there's one such thing, neither are true.

(I also think there's a risk in adding ad-hoc prefixes to things that it adds noise and becomes less readable to people with little context, though at dlog that's less of an issue)

313–315

this sentence doesn't parse :-)

333

Please don't use initialisms like this for members, they're hard to follow out of context.
I'd suggest ASTPeer (and PreamblePeer) or just Peer, to indicate these threads are tightly-coupled partners.

402

nit: move somewhere more appropriate (near getCurrentPreamble or update if modelling as public, else into the private section?)

402

nit: use a verb for mutating operations (updatePreamble)?

404

This name is confusing, almost all of its implementation is building AST.

generateDiagnostics?

470

PreambleRequests or GoldenASTRequests or so? The queue doens't actually contain preambles.

530

Placing this between ASTWorker and its implementation seems confusing - move it down?

543

This is going to trigger a rebuild of the fileindex - is it really necessary?

583

"Golden AST from preamble"?
Current text is not terribly descriptive...

597

incomplete comment

628

this isn't a safe assert - it does IO and could transiently become true/false

kadircet updated this revision to Diff 254439.Apr 2 2020, 12:21 AM
kadircet marked 31 inline comments as done.
  • Update comments and re-order functions

preambleReady part is a little bit different than you've described, it looks more like:

ASTWorker::preambleReady(): enqueue({
  if(preamble!=lastPreamble) {
     lastPreamble = preamble
     cache.get(); // force next read to use this preamble
  }
  build ast
  publish ast.diagnostics
  if (inputs == currentInputs) cache.put(ast)
})

also the last 3 steps are on a different function called generateDiagnostics since the code is a little bit long.
it immediately follows the preambleReady(now named updatePreamble).

clang-tools-extra/clangd/TUScheduler.cpp
17

just saying "it will issue a build for new inputs", which is explained in details in the next paragraph.

33

using compatible.

333

PW was for PeerWorker :P

543

astworker won't trigger that if inputs are the same.

583

this is not necessarily a golden ast now. just changing it to "Build AST"

628

right, thanks for catching it!

the one above is also not true (assert(LatestPreamble)) as preamble build might've failed. I suppose we would still want to move forward so that we can surface diagnostics saying why it failed.

sammccall accepted this revision.Apr 6 2020, 3:38 AM

Thanks for your patience!
This is very nice, only +100 lines on TUScheduler.cpp in the end!

clang-tools-extra/clangd/TUScheduler.cpp
221–222

comment is now obsolete, remove it and consider inlining Req

323–325

this is no longer locked or concurrently accessed right?

378

nit: last sentence is a fragment, I think you want to swap the dot and comma:

...through this callback. This ensures... of the file, as the callback...

408

Unused, I think?

784

I'd suggest InputsAreLatest or something that hints at *why* they might differ

801

This is reusing the AST built for a read if the file (and preamble) hasn't changed, right?

I think it's helpful to explain where teh cache data might be coming from.

IIRC this is the case we thought wasn't so important. It competes with a different optimization: skipping clang-tidy, warning analysis, etc when building ASTs because we know their diagnostics will never be used.

This might be a good place to leave a comment like "FIXME: maybe we're better off never reusing this AST, so queued AST builds aren't required to produce diagnostics" or so

853

This explains the what but not the why.
(We may be building an older version of the source, as the queue raced ahead while we were waiting on the preamble. In that case the queue can't reuse the AST we built.)
Comment could go here or on InputsAreTheSame

This revision is now accepted and ready to land.Apr 6 2020, 3:38 AM
kadircet updated this revision to Diff 255425.Apr 6 2020, 11:54 AM
kadircet marked 10 inline comments as done.
  • Address comments and rebase
kadircet marked an inline comment as done.Apr 6 2020, 11:54 AM
kadircet added inline comments.
clang-tools-extra/clangd/TUScheduler.cpp
221–222

i would rather keep it out-of-line since it is used in two places.

323–325

right, deleted annottation and added a comment.

This revision was automatically updated to reflect the committed changes.