This is an archive of the discontinued LLVM Phabricator instance.

[clangd] Add hot-reload of compile_commands.json and compile_flags.txt
ClosedPublic

Authored by sammccall on Dec 4 2020, 9:23 AM.

Details

Summary

When querying the CDB, we stat the underlying file to check it hasn't changed.
We don't do this every time, but only if we didn't check within 5 seconds.

This behavior only exists for compile_commands.json and compile_flags.txt.
The CDB plugin system doesn't expose enough information to handle others.

Slight behavior change: we now only look for build/compile_commands.json
rather than trying every CDB strategy under build subdirectories.

Diff Detail

Event Timeline

sammccall created this revision.Dec 4 2020, 9:23 AM
sammccall requested review of this revision.Dec 4 2020, 9:23 AM

The compilationdatabase changes won't be landed as part of this patch, they've been sent separately for review.
I can try the git gymnastics to get them out of this patch, but if you don't mind ignoring them that's easier :-)

There might be a sensible way to split this further, but I'm not seeing it for some reason - definitely open to suggestions.

njames93 added inline comments.
clang-tools-extra/clangd/ClangdLSPServer.cpp
712

Any reason for re-specifying compile_commands.json here?

clang-tools-extra/clangd/GlobalCompilationDatabase.cpp
256

this mostly LGTM. there are some changes to the existing behavior; like discovery order and not looking for other plugins under build/, but they seem like non-harmful changes to me.

my biggest question is, have we considered updating CompilationDatabasePlugin interface to provide a loadFromBuffer and relativeFileName virtual methods. I think loading logic could've been a lot simpler and more uniform that way.
i think we discussed this option but i don't remember the outcome, sorry :(.

clang-tools-extra/clangd/GlobalCompilationDatabase.cpp
201

nit: inline MayCache

220

what about symlinks ? i think users usually have cc.json -> obscure_build_dir/cc.json as a symlink.

239

also update cached mtime ?

282

nit: can we have a functor(probably llvm::function_ref) rather than a function pointer?

345

it is not crucial but we used to try to load these from Path + "/build" as well

483

why not log instead of vlog? this shouldn't be too noisy anyway.

clang-tools-extra/clangd/GlobalCompilationDatabase.h
133

why not directly store Options ?

clang-tools-extra/clangd/unittests/GlobalCompilationDatabaseTests.cpp
393

nit: maybe a MATCHER_P instead ?

sammccall marked 7 inline comments as done.Dec 9 2020, 9:47 AM

Thanks for the review!

my biggest question is, have we considered updating CompilationDatabasePlugin interface to provide a loadFromBuffer and relativeFileName virtual methods. I think loading logic could've been a lot simpler and more uniform that way.

I thought about it, not sure if we discussed it. TL;DR: I feel like we can do a good job of the concrete cases, but don't feel ready to generalize.

It's a larger, complicated design problem and the costs of getting it wrong (either making it too limiting *or* too flexible) are much higher than the benefits we'd get here. Apart from JSON/Fixed CDBs, we know there are downstream CDBs and this is a key extension point. What assumptions should we bake in?

  • CDB state is derived from a single file with known path?
  • CDBs should be thrown away and reloaded, rather than updated, when data changes?
  • CDBs are not invalidated by any other events?

I like the idea of extending the interface at some point. But I'm not sure the model from this patch is the right one to codify.

Meanwhile, most clang tools are batch, so the audience for this is... clangd and ccls? I don't think it's a pressing need.

clang-tools-extra/clangd/GlobalCompilationDatabase.cpp
220

RealFileSystem::status() is stat() rather than lstat() (it uses the default follow=true for sys::fs::status) so it will follow symlinks and report "regular file".

345

Yeah, this is on purpose, I think it's better only to do this for compile_commands.json. Now that we're periodically stat'ing we should think a little about how many files are necessary.

The build/ heuristic is basically targetting the "standard cmake setup" which is always compile_commands.json.

I think it's unlikely this is a significant regression for anyone. In a standard build it only affects build/compile_flags.txt which AFAIK is always hand-authored, and I'd expect authors to put it in the project root (we never documented the build/ behavior, and other clang tools don't have it).

FWIW, the old implementation looked in build/ for all plugins only because I wanted to stick to the high-level API rather than piercing the abstraction.


As an aside, I am slightly worried this hot-reload feature will make us wary of supporting more build systems that require more IO, because both more periodic stats and having some CDBs only scanned once would be unpalatable options.

Still, given how dominant compile_commands.json is today, we can't let the extensibility argument get in the way of an important feature like this.

483

Basically because "broacdasting" per se isn't meaningful to users.
This log line is going to be sandwiched between "Loaded compilation database from {0}" and "Enqueueing {0} commands for indexing", which tell them what they need to know.

I'm not sure it adds much, and maybe we should just remove it entirely? (But as a developer I do find it a nice reminder of what the flow is)

clang-tools-extra/clangd/unittests/GlobalCompilationDatabaseTests.cpp
393

Well, I find functions easier to reason about than the MATCHER_P factories...

Here it's pretty ugly: if we want to delegate to the two-arg version from the body, we have to pass through the result_listener, so something like

MATCHER_P(HasFlag, Flag, "") {
  return HasFlag(Flag, "dummy.cc").MatchAndExplain(arg, result_listener);
}

however the MATCHER_P macros produce *polymorphic* matchers (which is why we don't need to provide the arg type), so we need something more like

return Matcher<decltype(arg)>(HasFlag(Flag, "dummy.cc")).MatchAndExplain(arg, result_listener);

(Not sure if that's quite right)
I don't think that does a great job of communicating "this is the same HasFlag matcher, but the Path arg is dummy.cc".

sammccall updated this revision to Diff 310574.Dec 9 2020, 10:00 AM
sammccall marked an inline comment as done.

Address review comments

adamcz added inline comments.Dec 15 2020, 9:30 AM
clang-tools-extra/clangd/GlobalCompilationDatabase.cpp
220

Here's a fun thought: what if this is a network or otherwise magical filesystem (like a FUSE) and may be temporarily unavailable (due to expiring credentials, network issues, maybe your machine just woke up from suspend and needs to reconnect).

Do you think it makes sense to check for things like ENOACCES, ENOKEY, ESTALE and treat them as transient?

231

Why throw away the information we have? Better keep the cached metadata around, transient error here could mean the build system is re-creating the file, but it may end up being identical to what we already read, it's just that someone re-run cmake or something.

clang-tools-extra/clangd/unittests/GlobalCompilationDatabaseTests.cpp
393

lint warning here. Either rename or silence with a comment

397

Considering you already use min() in code, I would prefer if the test also included a value that's between min() and max() (i.e. something that's not a special-case)

424

Can you add a couple of calls to test the modtime and size comparison? Like, maybe change DBAZ to DFOO and see that it's picked up despite size being the same if mtime changed?

sammccall updated this revision to Diff 312193.Dec 16 2020, 6:19 AM
sammccall marked 7 inline comments as done.

address comments

clang-tools-extra/clangd/GlobalCompilationDatabase.cpp
220

Maybe in some cases, it depends a bit on the details...

The decision we're making here is really "retry on next access" vs "retry in 30 seconds".
The biggest point of the delay is that when loading background index shards (each corresponding to a source file), we check for each of them whether the stored compile commands are still valid (if not, we still load the shard, but schedule a rebuild). If we stat each possible cdb+config+etc each time, it significantly increases time before the index becomes available.

With this in mind, I don't think forcing ENOACCESS and ENOKEY to try every time is a good idea - the FS might have come back but probably not (user action is probably required). ESTALE sounds more plausible, but I don't know the details there. I'd probably avoid the complexity of this until we have concrete use cases where this is causing problems (e.g. ESTALE isn't portably available through std::error_code AFAIK).

In practice if the CDB is on an unavailable network FS then the sources/shards probably are too so we never really get here, though there can be exceptions

231

Good point, done.

Of course this relies on us getting "lucky" and seeing two different sizes for the file between the calls. If it's being written then we could e.g. see it with size 0 on both stat and read, and then we'll throw away the content.

The way around this would be to e.g. call an unparseable file a transient error (reuse cached value), which requires some structural changes here I think. What do you think about that? (I think this would help with compile_commands.json but not compile_flags.txt)
Another way would be to handle a size of 0 as a special case (presumed invalid), which may catch the practical cases.

clang-tools-extra/clangd/unittests/GlobalCompilationDatabaseTests.cpp
424

Done - I'd forgotten we had mtime support in TestFS. Also added some more comments to the tests as the list gets longer.

(While writing the original test, I had overwritten -DFOO with -DBAR and spent a while debugging why it didn't reload - oops!)

adamcz accepted this revision.Dec 16 2020, 6:35 AM
This revision is now accepted and ready to land.Dec 16 2020, 6:35 AM
sammccall added inline comments.Dec 18 2020, 2:06 AM
clang-tools-extra/clangd/GlobalCompilationDatabase.cpp
231

Decided not to do anything about this for now, the most common generator is cmake and it does atomic writes. Seems likely other build systems will too.

This revision was landed with ongoing or failed builds.Dec 18 2020, 2:18 AM
This revision was automatically updated to reflect the committed changes.

Seems to break tests on windows: http://45.33.8.238/win/30129/step_9.txt

Sorry (and for the delay getting back to keyboard). 0336ff0a17e6aec831334aeb50e6685f6b184065 should fix.

Thanks for the fix :)