neowit / tooling-force.com

Work with force.com Tooling/Meta API from various command line tools and editor plugins like vim-force.com.
62 stars 13 forks source link

Canceling requests to service? #30

Open ChuckJonas opened 7 years ago

ChuckJonas commented 7 years ago

Hey,

Just wondering how you handle the queueing up of multiple listCompletions requests. I'm noticing that if I queue up several requests back to back, the service starts to take longer and longer to respond (likely running out of memory?).

I figure this is something that you've handled in your VIM plugin. Is there someway to cancel the previous listCompletion when new ones come in?

Here what my plugin debugs end up looking like to help you visualize the problem I'm running into.

Sending CMD: --action=listCompletions 
Sending CMD: --action=listCompletions 
Sending CMD: --action=listCompletions 
Sending CMD: --action=listCompletions 

[INFO] Executor - # Time taken: 20.004s

[INFO] Executor - # Time taken: 20.004s

[INFO] Executor - # Time taken: 20.005s

[INFO] Executor - # Time taken: 15.032s
neowit commented 7 years ago

There is no queuing or cancelling presently. If you send second request before first had a chance to complete then both will be executed in parallel. Moreover, there is no way to say which request will complete first so your result file may not contain the result you expected. With vim this is a non issue because you can not send second code completion request before first one completes, vim UI just does not allow you to do so.

At first glance I am not sure how code completion request cancelling could be achieved because all operations involved are synchronous and there is no waiting phase like for instance in case of deployment. The longest running operation is a project source scan (which is normally done once per session, on the first code completion call). Once it is completed first time, it is not done in synchronous mode again (while server is running). if we cancel full source scan for the first completion call then it would have to be done for the next one anyway, so cancelling in this situation does not buy any time.

If you have ideas please share.

I am not sure if there is any way to block while code completion is in progress in VSCode or maybe there is a way to show user that their request is in progress. If neither is possible then for VSCode plugin one might consider tracking whether previous completion has finished and do not send second completion request while first one is in progress, or simply not send second one at all.

ChuckJonas commented 7 years ago

I don't think I can block in vscode. I'm basically just implementing providers and vscode handles all user interactions. Unfortunately, this means I don't even have great control over when a completion request is made.

For example, if I type:

Account acc = new Account();

vscode asks for completions basically every other character (can't figure out a pattern). I can specify Trigger characters but that only seems to guaranteed that it will fire on that character (doesn't restrict when it fires).

How are completions "triggered" in VIM?

I was hoping there might be a timeout parameter on the request that would kill off anything that doesn't respond in x seconds. Or maybe a command to kill all processing requests. I can seem how that would be difficult to implement though.

I might be able to solve this on my with the following:

1: Add a small delay to the request to catch rapid spamming from vscode and only listCompletions on for the final request

2: Use my typescript implementation of your antlr grammar to check if tooling-force can actually provide completions for the line/column before sending the request (not sure exactly what tokens I would need to look for)

Both of these have the potential to make the response time unacceptably slow.

Also:

I'm still trying to debug further exactly what conditions cause the service to get bogged down. Most the time it can handle multiple request fine, but once it gets bogged, it almost will never catch up to the fury of a heads down programmer.

One suspicion that might be contributing is how I'm providing credentials. If I include -sf.username && --sf.password with every listCompletion command, does the service send a login request EVERY time? Or does it check if it already has a token cached?

Sorry for the long response! As always, thanks for your help!

neowit commented 7 years ago
1: Add a small delay to the request to catch rapid spamming from vscode and only listCompletions on for the final request

Is it possible to record the fact if response to last call to code completion has been received ? If response has not been received then just do nothing on subsequent call to completion.

2: Use my typescript implementation of your antlr grammar to check if tooling-force can actually provide completions for the line/column before sending the request (not sure exactly what tokens I would need to look for)

Short of providing completion result yourself I can not see how this can be used. There is no way to see if completion can be provided until all options to provide this completion have been tried.

One suspicion that might be contributing is how I'm providing credentials.

As long as server has access to session.properties file in .vim-force.com directory (from --projectPath) it will use cached session token first

How are completions "triggered" in VIM?

Here is an example of code completion call from vim

--action=listCompletions --tempFolderPath='/Users/username/temp/apex/gvim deployment' 
--authConfigPath='/Users/username/Private/SFDC Credentials/oauth2/SForce (vim-force.com)' 
--config='/Users/username/Private/SFDC Credentials/SForce (vim-force.com).properties' 
--projectPath='/Users/username/eclipse.workspace/Sforce - SFDC Experiments/SForce (vim-force.com)' 
--column=13 --line=13 --currentFilePath='/Users/username/eclipse.workspace/Sforce - SFDC Experiments/SForce (vim-force.com)/src/classes/CompletionTester.cls' 
--currentFileContentPath='/var/folders/1l/lwgr141d1hdbcs8zsy6sh36m0000gn/T/vPQevxn/81CompletionTester.cls' 
--responseFilePath='/Users/username/eclipse.workspace/Sforce - SFDC Experiments/SForce (vim-force.com)/.vim-force.com/response_listCompletions' 
--pollWaitMillis=1000 --maxPollRequests=1000

In the above example client provides both OAuth2 credentials (--authConfigPath) and login/pass (--config), but server will always do something like this

--pollWaitMillis=1000 --maxPollRequests=1000 are not used in code completion call and simply ignored.

Client (vim) on the other hand just waits for response to complete (editor area of the UI is frozen) and I am not aware of any graceful way to trigger second completion before first one had a chance to complete (with success or failure). While vim is waiting for completion result user can press CTRL-C which does not send any cancel calls, but rather abandons completion on vim side and and unfreezes editor UI.

Current completion is done in following stages

  1. Scan project code (if has not been done already in the current server session)
  2. identify type in front of caret
  3. try to find member of this type in project code
  4. try to find member of this type in standard apex library (System, ApexPages, etc)
  5. try to check if given type represents SObject (which means have to do describe global). Describe global in synchronous mode is done only once and then cached
  6. if current expression types represent something like Opportunity.Account.name then we then have to call describe SObject for Opportunity and then describe SObject for Account.

Theoretically before invoking every step of the above list we could check if cancel call is received (does VSCode actually send cancel completion call ?), but current version of tooling-force.com.jar does not support cancellation for any calls.

ChuckJonas commented 7 years ago

Is it possible to record the fact if response to last call to code completion has been received ? If response has not been received then just do nothing on subsequent call to completion.

Yes, but I guess the problem with that is that only the last completion request from vscode is really valid. For example in foo.method(bar. if vscode request completion on both . and I cancel the second one, the user will not receive completions.

Short of providing completion result yourself I can not see how this can be used. There is no way to see if completion can be provided until all options to provide this completion have been tried.

Correct me if I'm wrong, but I only know of 3 instances where tooling-force actually provides completion:

1: On . accessors 2: @ attributes 3: inside of SOQL/SOSL queries

It doesn't seem to provide type completions on variable declarations or constructors. But I'm probably not considering all the edge cases I would need to handle to implement the approach I mentioned above.

In the above example client provides both OAuth2 credentials (--authConfigPath) and login/pass (--config), but server will always do something like this check if we have session token in --projectPath/.vim-force.com if above does not have session token or it has expired -- use credentials from --authConfigPath (if one provided) or login/pass from --config or command line

Ok, I think the way I'm sending it it should be using the token after initial auth:

--action=listCompletions --projectPath='/Users/johndoe/Documents/project' --responseFilePath='/Users/johndoe/.vscode/extensions/chuckjonas.apex-autocomplete-0.2.13/bin/listCompletionsResult.json' --pollWaitMillis=1000 --maxPollRequests=1000 --currentFilePath='/Users/johndoe/Documents/project/src/classes/MyClass.cls' --currentFileContentPath='/Users/johndoe/.vscode/extensions/chuckjonas.apex-autocomplete-0.2.13/bin/listCompletionsTmp.cls' --line='36' --column='32' --sf.username='abc@example.com' --sf.password='password1' --sf.serverurl='https://test.salesforce.com'

I'm using sf.username & sf.password because the extension just takes advantage of vscode's workspace/user settings to configure authentication (and that way I don't have to copy the password around).

The session.properties file does get created under the vim-force.com folder so I assume it's properly using that for subsequent requests. I'd like to also allow for oAuth (another discussion for another time). The only other difference I see is --tempFolderPath.

Theoretically before invoking every step of the above list we could check if cancel call is received (does VSCode actually send cancel completion call ?), but current version of tooling-force.com.jar does not support cancellation for any calls.

I think something like that would work with my implementation.

The vscode CompletionProvider does pass a CancellationToken in.

A cancellation token is passed to an asynchronous or long running operation to request cancellation, like cancelling a request for completion items because the user continued to type.

I would be able to cancel the request (--action=cancelCompletions) using the token's onCancellationRequested event.

However, I understand implementing that would likely be involved (I'm already very grateful for the time you spend on this project).

neowit commented 7 years ago

Correct me if I'm wrong, but I only know of 3 instances

Yes that is correct (except SOSL is not supported). I made wrong assumption about the meaning of your question.

The vscode CompletionProvider does pass a CancellationToken in.

This looks like something worth considering.

If user/client requested multiple completions too close to one another then there will be multiple threads running completion logic.

Do you know in which order VSCode sends completion and cancellation token ? Is it always

or can it be

How would we identify which completion call this cancellation token relates to ?

ChuckJonas commented 7 years ago

Do you know in which order VSCode sends completion and cancellation token ?

I added some logging and spammed a bunch of requests. From what I can tell vscode will always cancel the previous request before firing a new one.

[TESTING: completion request]
[TESTING: cancelation event fired]
[TESTING: completion request]
[TESTING: cancelation event fired]
[TESTING: completion request]
[TESTING: cancelation event fired]
[TESTING: completion request]
[TESTING: cancelation event fired]
[TESTING: completion request]
[TESTING: cancelation event fired]
[TESTING: completion request]
[TESTING: cancelation event fired]
[TESTING: completion request]
[TESTING: cancelation event fired]
[TESTING: completion request]
[TESTING: cancelation event fired]
[TESTING: completion request]

How would we identify which completion call this cancellation token relates to ?

I'm open to whatever you think is best, but it would be easier (for me) if I could provide an 'id' or 'label' string. That way I could generate a guid and pass it into the listCompletions request. Maybe something like this:

--action=listCompletions --id='123abc'

Then I could just pass that same id into the cancelation request

--action=cancelCompletions --id='123abc'

The reason this is easier for me is that I could define the ID as soon as I get a completion request and register the cancelation event immediately. If tooling-force assigned an id and returned it in the stream, I'd have to delay the event registration until I could parse it.

public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable<vscode.CompletionItem[]>{
        let id = generateGuid();
        token.onCancellationRequested(() => {
                this.toolingService.cancelCompletions(id);
            }
        );
       return this.toolingService.listCompletions(id, document, position, token);
}

Alternatively, I think a solution that just cancels ALL open completion requests would work (although there might be a race condition that I'm not considering if I sent an --action=cancelCompletions followed directly by an --action=listCompletions).

neowit commented 7 years ago

Both "cancel with Id" and "cancel all open" sound like potential candidates. I will need to do some testing. "Id based" one does seem to be more precise at first glance, but the fact that backend would have to keep track of this Id at every turn may be problematic. Thanks for the suggestions.

neowit commented 7 years ago

Hello @ChuckJonas

Can you share an example of a sequence of steps which leads to those long response times you specified in your original message on this issue ?

Sending CMD: --action=listCompletions 
Sending CMD: --action=listCompletions 
Sending CMD: --action=listCompletions 
Sending CMD: --action=listCompletions 

[INFO] Executor - # Time taken: 20.004s
[INFO] Executor - # Time taken: 20.004s
[INFO] Executor - # Time taken: 20.005s
[INFO] Executor - # Time taken: 15.032s

Thanks.

ChuckJonas commented 7 years ago

@neowit ya, so basically vscode automatically handles the triggering of "CompletionRequersts". You can specify a trigger character, (like .) but vscode still might send request in other contexts (not sure why).

I've created a screen capture that demonstrates this:

http://recordit.co/HWxJQidaez

In this case above, the lag is due to the latency in the original network request to get meta-data information for the account.

However, this is similar to what I'll see after running the service for 10-20 minutes. For some reason (I haven't been able to identify a consistent pattern), the requests start to queue up and when that happens they start taking exponentially longer to return (likely due to shared memory/cpu). Basically once it happens the server will never catch up unless you walk away for half an hour (I just restart it)