Skip to content

[SourceKit] ⚡️Fast code completion within function bodies #28727

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Dec 19, 2019

Conversation

rintaro
Copy link
Member

@rintaro rintaro commented Dec 12, 2019

Reuse ASTContext(CompilerInstance) from the previous completions when possible.

ide::CompletionInstance manages CompilerInstance for completion like requests (i.e. code completion, type context info, and conforming method list).
It vends the cached CompilerInstance if:

  • This functionality is enabled, and
  • The compiler arguments (i.e. CompilerInvocation) has not changed, and
  • The primary file is the same, and
  • The completion happens inside function bodies in both previous and current completion, and
  • The interface hash of of the primary file has not changed

Since cached instance already have AST including imported modules, we just run the second pass.

Otherwise, CompletionInstance vends new CompilerInstance, so we run both first and second pass using that instance. This instance is cached for the next completion request.

NOTE: currently it's disabled by default.

For example, using this simple example:

func test(x: Int) {
  x.#^COMPLETE^#
}
# Disabled
$ sourcekitd-test -req=complete -pos=2:5 $PWD/test.swift -repeat-request=10 -dont-print-request -dont-print-response -time-request -- $PWD/test.swift
request time: 109.469 ms
request time: 102.269 ms
request time: 101.260 ms
request time: 99.622 ms
request time: 100.751 ms
request time: 100.154 ms
request time: 99.670 ms
request time: 100.965 ms
request time: 101.426 ms
request time: 100.784 ms
# Enabled
$ sourcekitd-test -req=complete -req-opts=reuseastcontext=1 -pos=2:5 $PWD/test.swift -repeat-request=10 -dont-print-request -dont-print-response -time-request -- $PWD/test.swift
request time: 108.051 ms
request time: 1.716 ms
request time: 1.553 ms
request time: 1.559 ms
request time: 1.466 ms
request time: 1.497 ms
request time: 1.445 ms
request time: 1.544 ms
request time: 1.447 ms
request time: 1.506 ms

rdar://problem/20787086

@rintaro
Copy link
Member Author

rintaro commented Dec 12, 2019

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 205cfc40cb85a6ae33d1917182eb7cb99110d09d

@rintaro
Copy link
Member Author

rintaro commented Dec 12, 2019

@swift-ci Please test Linux

@rintaro
Copy link
Member Author

rintaro commented Dec 13, 2019

Forgot to add a test file.
@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 205cfc40cb85a6ae33d1917182eb7cb99110d09d

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 205cfc40cb85a6ae33d1917182eb7cb99110d09d

@rintaro rintaro force-pushed the sourcekit-completion-reuseinstance branch 3 times, most recently from efbd9d6 to d36ae5d Compare December 17, 2019 21:54
@rintaro
Copy link
Member Author

rintaro commented Dec 17, 2019

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 4ccc35cbddc29a3b27cd8196e2a152dd5f5886ac

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 4ccc35cbddc29a3b27cd8196e2a152dd5f5886ac

@rintaro
Copy link
Member Author

rintaro commented Dec 17, 2019

@swift-ci Please test Linux

@rintaro rintaro requested a review from benlangmuir December 17, 2019 22:53
Copy link
Contributor

@benlangmuir benlangmuir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to have some additional tests:

  • Compiler argument changed
  • Source changed in a compatible way -> reuse
  • Source changed in an incompatible way -> new AST

You can modify the text that is passed without changing the compiler arguments by using -text-input option to sourcekitd-test I think.

@@ -970,6 +972,8 @@ static int handleTestInvocation(TestOptions Opts, TestOptions &InitOpts) {
if (Opts.SourceText) {
sourcekitd_request_dictionary_set_string(Req, KeySourceText,
Opts.SourceText->c_str());
sourcekitd_request_dictionary_set_string(Req, KeySourceFile,
SemaName.c_str());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was needed to use -name as the filename (key.sourcefile).

@rintaro
Copy link
Member Author

rintaro commented Dec 18, 2019

@swift-ci Please smoke test

Invocation.setCodeCompletionPoint(NewBuffer.get(), CodeCompletionOffset);
// Pin completion instance.
auto CompletionInst = Lang.getCompletionInstance();
CompilerInstance *CI = CompletionInst->getCompilerInstance(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh hmm, this probably isn't thread safe. I don't think anything prevents you from making multiple code-completion requests (or other completion-like requests) at the same time. In sourcekitd-test, for example if you add -async and then make a bunch of requests in a row they probably overlap.

Perhaps we should use shared_ptr<CompilerInstance> so that if we renew it can't affect an ongoing completion?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I confirmed the crashes with -async. Will fix.

@rintaro rintaro force-pushed the sourcekit-completion-reuseinstance branch 2 times, most recently from f353894 to 5f7090a Compare December 18, 2019 22:14
@rintaro
Copy link
Member Author

rintaro commented Dec 18, 2019

@benlangmuir The last commit changes the structure of the processing. So CompletionIntance take the control of the lifetime of CompilerInstace.
CompletionIntance::performOperation() receives a callback void(CompilerInstance &), and call it with the prepared compiler instance. This change has several benefits:

  • Prevent multiple completions use the same compiler instance at the same time.
  • Stacktraces are distinguishable between "fast" and "normal" mode
  • Consolidate more code between completion / type context / conforming method list

What do you think?

@rintaro
Copy link
Member Author

rintaro commented Dec 18, 2019

@swift-ci Please smoke test

@rintaro
Copy link
Member Author

rintaro commented Dec 18, 2019

Modified the way of storing the cached information. It should prevent CompilerInstance/ArgHash/ReuseCount from being out-of-sync.
@swift-ci Please smoke test

@rintaro rintaro requested a review from benlangmuir December 19, 2019 01:01
@benlangmuir
Copy link
Contributor

Ah good point, a shared_ptr is wrong since you actually can't share this safely across threads. Note: your current code is still technically racy because the std::swap() of the state itself is not atomic.


So if you have racing completions, one will win and get to reuse the AST, and the next one will go through the slow path. Is that better than blocking on the first one? If you're reusing ASTs it seems like it would usually be better to wait since you have a chance to hit the fast path on every completion. Assuming the reuse case is 10x faster and that the number of concurrent completions is usually very small, you would be better off waiting.

If you're not reusing ASTs, then I guess running the completions in parallel might be better in some instances - or at least it matches the current behaviour.

@rintaro rintaro force-pushed the sourcekit-completion-reuseinstance branch from a4eee1a to 23f4416 Compare December 19, 2019 05:46
@rintaro
Copy link
Member Author

rintaro commented Dec 19, 2019

Fair enough. I update the PR to block other threads using std::mutex.
@swift-ci Please smoke test

Specifically, align with swiftTypeContextInfoImpl() and
swiftConformingMethodListImpl()
So that we can use the different the buffer for the second pass from the
first pass.
- Introduce ide::CompletionInstance to manage CompilerInstance
- `CompletionInstance` vends the cached CompilerInstance when:
-- The compiler arguments (i.e. CompilerInvocation) has has not changed
-- The primary file is the same
-- The completion happens inside function bodies in both previous and
   current completion
-- The interface hash of the primary file has not changed
- Otherwise, it vends a fresh CompilerInstance and cache it for the next
  completion

rdar://problem/20787086
…ation

Checking "selected" properties of CompilerInvocation is hard to keep it
up to date. If the arguments are the same, the invocations are the same.
To controls the lifetime of CompilerInstance within CompletionIntance.

- Prevent from using the same CompilerInstance from multiple completion
  requests
- Separate the stacktrace between "fast" and "normal" completions
- Further code consolidation between various completion-like requests
if fast-completion is enabled. So they have higher chance to use the cached
completion instance.
If it's disabled, don't block, use an ephemeral instance so we can peform
multiple completions simultaneously.
CachedInst.swap(TheInstance);
CachedCI = std::move(TheInstance);
CachedArgHash = ArgsHash;
CachedReuseCound = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: "Cound" vs "Count"

llvm::function_ref<void(CompilerInstance &)> Callback);

public:
void setEnableASTCaching(bool Flag) { EnableASTCaching = Flag; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if this were done atomically it would not be correct for racing completions. Can we pass EnableASTCaching in as an argument to performOperation so that is is associated with a specific completion?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! How does it look now?


// If AST caching is enabled, block completions in other threads. So they
// have higher chance to use the cached completion instance.
Optional<std::lock_guard<std::mutex>> lock;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fun, but maybe too magical? Could we instead separate these code-paths, like

if (EnableASTCaching) {
  // Compute the signature of the invocation.
  llvm::hash_code ArgsHash(0);
  for (auto arg : Args)
     ArgsHash = llvm::hash_combine(ArgsHash, StringRef(arg));

  // Concurrent completions will block so that they have a higher chance to use the cached completion instance.
  std::lock_guard<std::mutex> lock(mtx);
  if (performCachedOperaitonIfPossible(...))
    return true;
  if (performNewOperation(...))
     return true;

} else {
  // Concurrent completions may happen in parallel when caching is disabled.
  if (performNewOperation(Invocation, ArgsHash, FileSystem, completionBuffer,
                           Offset, Error, DiagC, Callback))
     return true;
}

@rintaro rintaro force-pushed the sourcekit-completion-reuseinstance branch from 23f4416 to 4327111 Compare December 19, 2019 20:19
so that it is associated with a specific completion.
@rintaro rintaro force-pushed the sourcekit-completion-reuseinstance branch from 4327111 to eebcbf6 Compare December 19, 2019 20:20
@rintaro
Copy link
Member Author

rintaro commented Dec 19, 2019

@swift-ci Please smoke test

@rintaro rintaro merged commit 10936f6 into swiftlang:master Dec 19, 2019
@rintaro rintaro deleted the sourcekit-completion-reuseinstance branch December 19, 2019 22:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants