This is an archive of the discontinued LLVM Phabricator instance.

[clangd] Cache FS stat() calls when building preamble.
ClosedPublic

Authored by ioeric on Sep 24 2018, 7:14 AM.

Details

Summary

The file stats can be reused when preamble is reused (e.g. code
completion). It's safe to assume that cached status is not outdated as we
assume preamble files to remain unchanged.

On real file system, this made code completion ~20% faster on a measured file
(with big preamble). The preamble build time doesn't change much.

Event Timeline

ioeric created this revision.Sep 24 2018, 7:14 AM
ioeric updated this revision to Diff 166667.Sep 24 2018, 7:17 AM
  • update comment
Harbormaster completed remote builds in B22975: Diff 166667.
ioeric added inline comments.Sep 24 2018, 7:45 AM
clangd/ClangdUnit.cpp
119

Would it make sense to add a clang::vfs::ProxyFS that proxies calls to underlying FS by default and allows to override some methods?

ilya-biryukov added inline comments.Sep 24 2018, 10:04 PM
clangd/ClangdUnit.cpp
119

This makes sense, we seem to have a recurring pattern of forwarding all methods by default in this commit.
+1 to the suggestion

128

One can obtain file stats from the returned iterator.
I believe this is exactly what will start happening after Sam's patch to list dirs when processing include dirs.

We should probably also wrap iterators that we return and cache stats from those.

149

If we cache the stats, we should probably cache the file contents too.
Not sure how much of an extra memory this will require.

Though maybe we could get away with returning the old stat, but new file contents. It would look as if the file was between calls to stat() and openForRead.

338

We store absolute paths, but Path can be relative here.

339

Maybe remove parens?
Comparisons associate properly with ternary op and it's common enough to not cause any confusion with the reader.

clangd/CodeComplete.cpp
1003–1004

Maybe pass a single pointer to PreambleData here?

1028

It feels like we could apply this caching one level higher, on the TUScheduler level when creating the filesystem. It makes sense not only for code completion, but also for all other features, including rebuilds of ASTs that were washed out of the cache.
WDYT?

1627–1628

signatureHelp could also benefit from the cached fs, maybe pass it too?
See the other comments about applying this caching one level higher (on TUScheduler level) too.

clangd/CodeComplete.h
217

We should not be depending TUScheduler.h, I suggest to pass PreambleDatainstead of PrecompiledPreamble instead.

Depending on TUScheduler.h our layering in clangd: low-level implementation of features should not depend on higher-level building blocks like TUScheduler.

Nice win!

This is missing high-level documentation explaining what problem it's trying to solve and what the lifetime is.
I think pulling the factories out into a dedicated header would give you good space for that.

clangd/ClangdUnit.cpp
128

The returned iterator can't be used to obtain file status (since my patch).

149

I don't think I agree, what's the argument for caching both?

In the observed profile, stats are a problem but openFileForRead is not (in fact the files are never open).

This can't be a hard correctness requirement, because stat->open is racy anyway.

155

why are you only caching success?

166

this isn't threadsafe.

Note that adding a lock here isn't enough, as you may have multiple FS instances referring to the same cache. The lock needs to live alongside the cache storage, so the latter should probably be a class.

311

outline this implementation next to the other one, and give them parallel names?

statCacheProducingFS/statCacheConsumingFS?

The design is hard to follow without this parallelism explicit.

clangd/ClangdUnit.h
57

This doesn't belong in ClangdUnit.
(Potentially it could live in clang as a VFS util, but a FS.h or so here seems fine. Maybe merge with FSProvider.h)

clangd/CodeComplete.h
217

+1. We're maybe not doing the best job of header encapsulation here, but InputsAndPreamble is an implementation detail of ClangdServer.

ioeric updated this revision to Diff 166951.Sep 25 2018, 10:51 AM
ioeric marked 10 inline comments as done.
  • Address review comments

Thanks for the reviews!

clangd/ClangdUnit.cpp
155

I had a comment in openFileForRead:

// Only cache stats for files that exist because, unlike building preamble,
// we only care about existing files when reusing preamble.

Another reason is that we are using the file name in the Status as the cache key.

338

This is because preamble stores absolute paths. Path should be an path from preamble. Added documentation.

clangd/CodeComplete.cpp
1028

It sounds like a reasonable thing to do. I took a quick look, and it seemed that this would probably require some refactoring/redesign on TUScheduler - it doesn't know about the VFS?

I think it shouldn't be hard to do this case by case. For example, code completion and signature help will be covered in this patch. And it should be pretty easy to make AST rebuild use the cache as well?

clangd/CodeComplete.h
217

I thought InputsAndPreamble was a pretty natural fit here, but I guess I hadn't understand the abstractions well enough. Thanks for the explanation!

Generally LG. A few things I'm unsure about.

clangd/CodeComplete.cpp
1028

It sounds like a reasonable thing to do. I took a quick look, and it seemed that this would probably require some refactoring/redesign on TUScheduler - it doesn't know about the VFS?

Right, this is a bit of a mess...
Currently there are features that access the FS "directly". This includes CodeComplete which runs its own compile action, as well as things like switchSourceHeader or format. These invoke FSProvider.
Then there are features that build an AST (which may need file accesses), and then may implicitly read files by querying the AST (the FS is attached to the FileManager or something). These use the FS obtained from the FSProvider in the relevant addDocument call.
There's no fundamental reason we can't have both (access FS directly and use the AST) in which case we'll be using both filesystems together...

In the near term, making the TUScheduler-managed AST rebuild use the cache stored in the preamble seems like a nice win. (As you alluded to I don't think this change is in TUScheduler itself, rather buildAST?)

clangd/FS.cpp
1 ↗(On Diff #166951)

Can we have unit tests for these?
It should be pretty straightforward, as you can inspect/seed the cache state directly.

29 ↗(On Diff #166951)

lock

48 ↗(On Diff #166951)

I'm not sure I get this: AFAICT (at least on linux) the status is never available on a newly opened file, so this will always be a stat() call, so we're just doing the work eagerly and caching it for later. Isn't this just moving the work around?

I'm sure you've verified this is important somehow, but a comment explaining how would be much appreciated :-)

clangd/FS.h
20 ↗(On Diff #166951)

can you give a little more colour on *why* code completion tends to stat a bunch of the same files over again?
(One might assume it's to validate the preamble, but it shouldn't be, because we don't do that for other reasons)

21 ↗(On Diff #166951)

whene -> when

unittests/clangd/ClangdTests.cpp
966

it's not obvious what this test does (without the rest of the patch as context).
Maybe a high-level comment: Check that running code completion doesn't stat() a bunch of files from the preamble again. (They should be using the preamble's stat-cache)

980

just ++CountStats[...]?
life is too short :-)

984

or just return ProxyFileSystem::status(Path); which is *slightly* less coupled

1018

I think this would be much clearer if we asserted the actual count here. It should be 1, right? Do you think it'd be too brittle?

As it stands, it seems entirely possible that the test is not seeing any stats for whatever reason and is spuriously passing.

ilya-biryukov added inline comments.Sep 25 2018, 11:05 PM
clangd/ClangdUnit.cpp
338

It's true that preamble stores and checks absolute paths, however clang can later access the same file using a relative path.
Calling makeAbsolute here shouldn't hurt and would obviously make it work in both cases.

clangd/CodeComplete.cpp
1028

My idea would be to create and pass the updated cached FS into both buildAST and codeComplete higher up the call chain. This would allow localizing the code that creates caching VFSes in one file (TUScheduler.h or similar).

The advantage of receiving the patched-up FS in buildAST and codeComplete is that they don't have to care about this trick, making the implementation simpler.
The fewer functions across the codebase have to apply the trick the better, e.g. this would make sure we won't accidentally forget it to apply it when implementing a new feature.

ioeric updated this revision to Diff 167124.Sep 26 2018, 6:42 AM
ioeric marked 7 inline comments as done.
  • address review comments
ioeric added inline comments.Sep 26 2018, 6:43 AM
clangd/ClangdUnit.cpp
338

It would "obviously work"... until you have symlinks and tricks to handle symlinks ;)

clangd/CodeComplete.cpp
1028

Added the cache support to buildAST as we thinks it's beneficial (haven't profiled this though).

Currently, the trick is applied in two places (semaCodeComplete in CodeComplete and buildAST in ClangdUnit), and I'm not sure how much this would grow in the near future. It's also unclear to me whether baking this into TUScheduler would be less work in the long run. I would suggest revisiting this when we have more evidence.

clangd/FS.cpp
29 ↗(On Diff #166951)

After a second thought, I'm wondering if the mutex is necessary, if the cache is only updated during preamble build in a single thread. The cache would also remain the same in preamble reuses.

48 ↗(On Diff #166951)

Files opened via openFileForRead() wouldn't usually trigger status() calls on the wrap FS. And most header files are opened instead of stated.

Added comment explaining this.

sammccall added inline comments.Sep 27 2018, 9:51 AM
clangd/FS.cpp
53 ↗(On Diff #167124)

do you want to check if the file is already in the stat cache first?

29 ↗(On Diff #166951)

Indeed if you have the following sequencing:

  • one FS writes to the cache from one thread (or several but strictly sequenced)
  • sequence point (no writes after this point, no reads before)
  • several FSs read from the cache

then no lock is required, either for writer or reader.
The API doesn't particularly suggest this, so please explicitly document the threading model in the header.
(An API that would suggest it is to e.g. make the producing FS the top-level object, give it a &&-qualified method to retrieve the cache, and give the cache methods to retrieve the consuming FSes. But I think that's awkward here with the VFS interface)

48 ↗(On Diff #166951)

Ah, OK.
The implementation comment is good, but this is significantly different from "caching stat calls" as described in the header files.

Maybe update the comment there: e.g. "Records status information for files open()ed or stat()ed during preamble build, so we can avoid stat()s on the underlying FS when reusing the preamble"

ioeric updated this revision to Diff 167370.Sep 27 2018, 12:18 PM
ioeric marked 2 inline comments as done.
  • Address review comments
  • address review comments
  • address review comments
clangd/FS.cpp
53 ↗(On Diff #167124)

This would always invalidate status in cache when file is reopened or restated in case file status has changed during preamble build. Added a comment.

29 ↗(On Diff #166951)

(Stole some wording here. Thanks!)

sammccall accepted this revision.Oct 2 2018, 12:49 AM

Sorry for dropping this.

clangd/ClangdUnit.h
71

This deserves a comment, even if it's a bit repetitive.

// Cache of FS operations performed when building the preamble.
// When reusing a preamble, this cache can be consumed to save IO.
This revision is now accepted and ready to land.Oct 2 2018, 12:49 AM
ioeric updated this revision to Diff 167913.Oct 2 2018, 3:41 AM
  • add comment for StatCache in PreambleData
ioeric updated this revision to Diff 167914.Oct 2 2018, 3:42 AM
  • Rebase
This revision was automatically updated to reflect the committed changes.
This revision was automatically updated to reflect the committed changes.