This is an archive of the discontinued LLVM Phabricator instance.

[analyzer][ctu] Avoid parsing invocation list again and again during on-demand parsing of CTU
ClosedPublic

Authored by OikawaKirie on May 3 2021, 9:05 AM.

Details

Summary

During CTU, the *on-demand parsing* will read and parse the invocation list to know how to compile the file being imported. However, it seems that the invocation list will be parsed again if a previous parsing has failed. Then, parse again and fail again. This patch tries to overcome the problem by storing the error code during the first parsing, and re-create the stored error during the later parsings.

Diff Detail

Event Timeline

OikawaKirie created this revision.May 3 2021, 9:05 AM
OikawaKirie requested review of this revision.May 3 2021, 9:05 AM

Awesome! Seems good to me. Though I've got limited experience on CTU stuff.
It would be nice to have tests, but it seems pretty hard to come up with one for this. Given that this is just a 'performance' issue, I'm fine with it.
Somehow try to check if this resolved your original concern.

clang/include/clang/CrossTU/CrossTranslationUnit.h
41

What about success?
That way it would resonate well with the return's success();

clang/lib/CrossTU/CrossTranslationUnit.cpp
694

Shouldn't be small enough to pass-by-value?

Thank you for this patch!

Could you please provide a lit test that ignites the over and over parsing behaviour? I think you need to create two files and the second one should contain parser error(s).

clang/include/clang/CrossTU/CrossTranslationUnit.h
257

I think it would be more consistent to make this an llvm::Error. I.e. we would store the result of the whole lazyInitInvocationList in this member variable. And as such, a better name could be PreviousResult. And this could be an Optional to indicate that we have never called lazyInitInvocationList before.
This means we would check in lazyInitInvocationList whether the PreviousResult is set and if yes is it an error. And if both conditions are true then return with the stashed error.

clang/lib/CrossTU/CrossTranslationUnit.cpp
674

The if here is superfluous because the condition must be true always, otherwise we would have an InvocationList, wouldn't we?

Or we can have this condition, but then the if (InvocatioList) is not needed.

balazske added inline comments.May 5 2021, 12:11 AM
clang/lib/CrossTU/CrossTranslationUnit.cpp
674

This looks correct: We have an InvocationList if it is usable as result (initialized and no error). Otherwise there is an error or it is the first time the function is called. The second if checks for this condition.

balazske added inline comments.May 5 2021, 12:13 AM
clang/include/clang/CrossTU/CrossTranslationUnit.h
257

The current solution looks good, probably it is more convenient to store the result in a llvm::Error.

Add a regression test case by mocking the open function. When this function is called with the file name of the invocation list, the mocked open function will reject the open operation and dump a log. And we will then check how many times are the invocation list opened. (Thanks to the ideas by steakhal in another patch of mocking a library function :-).)

Identifiers renamed as suggested.

Unfortunately, we cannot store and copy a llvm::Error object. Therefore, the new version still uses the error code to store the previous parsing results.

Overall, it looks promising. But I don't quite get this test.
There is no invocation yaml in the temp directory. So, you are probably not testing the right thing.
You wanted to test if the invocation yaml exists, and could be opened but the parsing fails.
You should demonstrate that when a parsing error happens, the error code has recoded and it won't try to reparse the invocation yaml again and again.

Overall, it looks promising. But I don't quite get this test.
There is no invocation yaml in the temp directory. So, you are probably not testing the right thing.
You wanted to test if the invocation yaml exists, and could be opened but the parsing fails.
You should demonstrate that when a parsing error happens, the error code has recoded and it won't try to reparse the invocation yaml again and again.

The main idea of the test case is that the mocked open function will dump a log every time when the invocation list file is opened to be read. If a second open operation is detected in the log, it means the list is parsed again.

As the behavior of missing the invocation list file or failing to parse it are the same (both returns the error), the previous version checks whether function open will be called again after a file-not-found error.
In this update, I use an empty invocation list to really trigger an invocation_list_empty error and check whether the invocation list file will be read again via function open.
Compared with the previous version, opening the invocation list will not be manually failed, and a real parsing error is triggered instead.

Okay, so you 'just' want an indication for the given open call. What about using strace?
strace -e trace=openat %clang_cc1 ... 2>&1 | grep '"invocations.yaml"' | FileCheck %s

This way the given test file could contain the contents of the importer.c.

  • Update the test case as suggested.
  • Add a case branch for the index_error_code::success error code in function IndexErrorCategory::message to silent the compiler warnings.

Replace wc -l with count.

Sorry for the spam.

I would rather match on the complete line (except the file descriptor ofc).

By inspecting the output that is piped to the count, I have a suspicion:

openat(AT_FDCWD, "invocations.yaml", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

It should have found it! It exists, but ill-formed. @martong what did we misconfigure?

My guess is that ctu-invocation-list is not using the ctu-dir. It should be a different bug though.
To workaround this issue, simply use an absolute path for the invocation yaml.

What about this?

// RUN ... 2>&1 | grep 'invocations\.yaml' | FileCheck %s
void foo() {
  // It finds the invocation yaml, then it tries to parse it which expected to fail.
  bar();
  // CHECK: openat(AT_FDCWD, "{{[^"]+}}invocations.yaml", O_RDONLY|O_CLOEXEC) = {{[0-9]+}}

  // We should remember that the parsing failed, we shouldn't try to reopen and parse the yaml again.
  bar();
  // CHECK-NOT: openat(AT_FDCWD, "{{[^"]+}}invocations.yaml", O_RDONLY|O_CLOEXEC) = {{[0-9]+}}
}

This way we are sure that the file opened (it resulted in a non-negative file descriptor) and opened only once!

By inspecting the output that is piped to the count, I have a suspicion:

openat(AT_FDCWD, "invocations.yaml", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

According to the output of the failed test, it seems that CTU is not working during the test case execution (Expected 1 lines, got 0.). Otherwise, it should be matched by grep.
There seems to be something wrong with the test case, although it works well in my environment.

My guess is that ctu-invocation-list is not using the ctu-dir. It should be a different bug though.
To workaround this issue, simply use an absolute path for the invocation yaml.

It seems that you are correct, I will fix this problem later.
I can just cd to the ctu-dir before running CSA as a workaround. Just as the author of on-demand parsing did (refer to clang/test/Analysis/ctu-on-demand-parsing.c).

// CHECK: openat(AT_FDCWD, "{{[^"]+}}invocations.yaml", O_RDONLY|O_CLOEXEC) = {{[0-9]+}}

I am not quite sure whether we need to verify the flags (AT_FDCWD, O_RDONLY and O_CLOEXEC) as well. Therefore, I removed them in this update.

steakhal accepted this revision.May 7 2021, 4:37 AM

I would love to see PreviousParsingResult combined with InvocationList using a llvm::Error. I'm pretty sure it can be done.
Regardless, I think it's already better than it was.

This revision is now accepted and ready to land.May 7 2021, 4:37 AM

Okay, so you 'just' want an indication for the given open call. What about using strace?
strace -e trace=openat %clang_cc1 ... 2>&1 | grep '"invocations.yaml"' | FileCheck %s

I do not know why the test case always fails on the build server, it runs perfectly on my computer. : (
Do any reviewers have any ideas?

Monitor function open together with openat to fix the failure in the test case.
As some distros actually call function open rather than forward to function openat, both functions should be monitored by strace.

But I am still worrying about whether function open will call openat and vice versa. If this circumstance is possible, the test case may still fail.
Although, I have not found any pieces of evidence for such invocations.

I do not know why the test case always fails on the build server, it runs perfectly on my computer. : (

I've locally run your patch and seemed good to me, I regret not having a look at the actual build bot.

Monitor function open together with openat to fix the failure in the test case.
As some distros actually call function open rather than forward to function openat, both functions should be monitored by strace.

But I am still worrying about whether function open will call openat and vice versa. If this circumstance is possible, the test case may still fail.
Although, I have not found any pieces of evidence for such invocations.

Uh, that's a fair point. Now, I'm also worried xD
To be fair, I don't have any better idea testing this without overwhelming effort.
If anything goes wrong, we can simply revert this change. This is 'just' a performance fix.

FileCheck error: '<stdin>' is empty.

It fails again on the build bot, another dead end. Maybe I need a Debian server to reproduce the problem encountered in the test case.

I do not know why the test case always fails on the build server, it runs perfectly on my computer. : (

I've locally run your patch and seemed good to me, I regret not having a look at the actual build bot.

I have tried the test case on a Ubuntu 20.04 server with kernel 5.4.0 and gcc 9.3.0, as well as a CentOS 7 server with kernel 3.10.0 and gcc 7.1.0. Both of them work well.

Monitor function open together with openat to fix the failure in the test case.
As some distros actually call function open rather than forward to function openat, both functions should be monitored by strace.

But I am still worrying about whether function open will call openat and vice versa. If this circumstance is possible, the test case may still fail.
Although, I have not found any pieces of evidence for such invocations.

Uh, that's a fair point. Now, I'm also worried xD
To be fair, I don't have any better idea testing this without overwhelming effort.
If anything goes wrong, we can simply revert this change. This is 'just' a performance fix.

I will try to reproduce and fix the problem of the test case next week. If I cannot find other ways to overcome the problem by the next weekend, we can have a try on landing this patch.

First of all, thank you for the patch!
We had a meeting with my colleges (@steakhal, @gamesh411) and we agreed in the following. This is indeed an issue and the fix is okay. About the test, we'd like to ask if you could create a small test lib with its own 'open' function that would simply call the system's open and would print 'open'. And then with LD_PREOLOAD you could use that lib in the lit test. (If that approach is not working for some reason then we may just mark the test with XFAIL.)

First of all, thank you for the patch!
We had a meeting with my colleges (@steakhal, @gamesh411) and we agreed in the following. This is indeed an issue and the fix is okay. About the test, we'd like to ask if you could create a small test lib with its own 'open' function that would simply call the system's open and would print 'open'. And then with LD_PREOLOAD you could use that lib in the lit test. (If that approach is not working for some reason then we may just mark the test with XFAIL.)

Updated as suggested based on a previous submit https://reviews.llvm.org/D101763?id=343346. An empty invocation list is used to trigger a parsing failure, and name invocations.yaml is used as the file name for logging and checking.

Could you please reupload your diff to retrigger the bots?
I'd like to make sure that everything passes, even the tests that do not belong to you.
Unfortunately, I don't know any better way of retriggering the pre-merge checks - I don't have permission to manually retrigger builds.

Re-submit the patch to re-build the code.

Re-submit the patch to re-build the code.

Sorry for the spam of submitting an incomplete diff which triggers a patch apply failure.

steakhal accepted this revision.May 24 2021, 11:44 AM

It seems like everything passes. Yeey, good job!
Shall I commit this tomorrow on your behalf?

It seems like everything passes. Yeey, good job!
Shall I commit this tomorrow on your behalf?

I do not have commit access to the code base. It would be appreciated if you could commit it on my behalf (Ella Ma <alansnape3058@gmail.com>).
Thanks to all reviewers again for your suggestions.

I've decided to land this without the test.
Thank you for your contribution.