This is an archive of the discontinued LLVM Phabricator instance.

[clang][deps] Compute command lines and file deps on-demand
Needs ReviewPublic

Authored by jansvoboda11 on Aug 21 2023, 5:22 PM.

Details

Reviewers
benlangmuir
Summary

Although generating command lines and collecting file dependencies recently got faster, there are still benefits to be had from doing these lazily, on-demand.

This patch makes it so that the ModuleDepsGraph keeps the scanning instance alive. Instances of ModuleDeps can then use pointer to the ModuleDepCollector to compute the information lazily.

Diff Detail

Event Timeline

jansvoboda11 created this revision.Aug 21 2023, 5:22 PM
Herald added a project: Restricted Project. · View Herald TranscriptAug 21 2023, 5:22 PM
Herald added a subscriber: ributzka. · View Herald Transcript
jansvoboda11 requested review of this revision.Aug 21 2023, 5:22 PM
Herald added a project: Restricted Project. · View Herald TranscriptAug 21 2023, 5:22 PM
Herald added a subscriber: cfe-commits. · View Herald Transcript

I find this a bit hard to understand as far as object lifetime is concerned: you're storing the ScanInstance in TranslationUnitDeps, but that's a layer above the actual consumer interface, which means every consumer needs to understand how this lifetime is managed or it will have dangling references by default. You also seem to have ScanInstance stored twice -- once explicitly and once in the graph.

What do you think of an alternate design where ModuleDeps has shared_ptr<ModuleDepCollector> MDC and ModuleDepCollector has shared_ptr<CompilerInstance>? That way we don't need to explicitly expose the scan instance to the consumer, it comes with the ModuleDeps, which then keeps all the necessary memory alive?

I find this a bit hard to understand as far as object lifetime is concerned: you're storing the ScanInstance in TranslationUnitDeps, but that's a layer above the actual consumer interface, which means every consumer needs to understand how this lifetime is managed or it will have dangling references by default. You also seem to have ScanInstance stored twice -- once explicitly and once in the graph.

Agreed, I'm not happy about the lifetime complexity here.

What do you think of an alternate design where ModuleDeps has shared_ptr<ModuleDepCollector> MDC and ModuleDepCollector has shared_ptr<CompilerInstance>? That way we don't need to explicitly expose the scan instance to the consumer, it comes with the ModuleDeps, which then keeps all the necessary memory alive?

I tried that approach, but found it way too easy to keep ModuleDeps around, which keep scanning instances alive, and use tons of memory.

jansvoboda11 added a comment.EditedAug 23 2023, 4:00 PM

Hmm, maybe we could avoid holding on to the whole CompilerInstance. For generating the command-line, we only need the CompilerInvocation. For collecting file dependencies, we could hold on to the FileManager and MemoryBuffer (and maybe offset to the input files block), and deserialize that on-demand, without keeping the whole CompilerInstance around.

I tried that approach, but found it way too easy to keep ModuleDeps around, which keep scanning instances alive, and use tons of memory.

It seems like the problem (too easy to keep around MD) is the same either way, it's just instead of wasting memory it's leaving dangling pointers that could cause UAF if they're actually used. Where were you seeing MD held too long? I wonder if there's another way to fix that.

Hmm, maybe we could avoid holding on to the whole CompilerInstance.

This seems promising!

I tried that approach, but found it way too easy to keep ModuleDeps around, which keep scanning instances alive, and use tons of memory.

It seems like the problem (too easy to keep around MD) is the same either way, it's just instead of wasting memory it's leaving dangling pointers that could cause UAF if they're actually used.

You're right. I have a version of this patch that stores std::weak_ptr<ModuleDepCollector> MDC in ModuleDeps and asserts when you're about to dereference dangling pointer. That might solve that part of the issue.

Where were you seeing MD held too long? I wonder if there's another way to fix that.

For example in clang-scan-deps, we accumulate all ModuleDeps into FullDeps::Modules. This means that at the end of the scan, that can be keeping up to NumTUs worth of CompilerInvocations alive.

Hmm, maybe we could avoid holding on to the whole CompilerInstance.

This seems promising!

OK, I'll try to explore this a little further. I'm a bit concerned that FileManager (and its caches) might still be too heavy to keep around, though.


I guess what makes this harder to get right is the fact that ModuleDeps and ModuleDepsGraph live on different levels. I think that maybe restructuring the API a little bit could simplify things. What I have in mind is replacing the DependencyConsumer function handleModuleDependency(ModuleDeps) with handleModuleDepsGraph(ModuleDepsGraph). We could then turn ModuleDepsGraph into an iterator over proxy objects representing what's currently called ModuleDeps. (Lifetime of these proxies would be clearly tied to that of ModuleDepsGraph.) I think we can assume that scanner clients are less tempted to "permanently" store the full ModuleDepsGraph (since that's wasteful due to cross-TU sharing), compared to ModuleDeps (which you probably want to store in some form). Clients would therefore be nudged into creating own storage-friendly version of ModuleDeps before disposing of ModuleDepsGraph, which would force them to call getBuildArguments() and getFileDeps() on our ModuleDeps proxy before disposing of the heavy-weight graph. WDYT about this direction?