-
Notifications
You must be signed in to change notification settings - Fork 223
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
Revamp pipeline thread handling #1295
Comments
I've been thinking about this too, and wrote a simple consumer that allows asynchronous dispatch of PowerShell commands like this: public class AsyncPowerShell
{
private readonly PowerShell _pwsh;
private readonly Runspace _runspace;
private readonly BlockingCollection<Tuple<PSCommand, TaskCompletionSource<IReadOnlyCollection<PSObject>>>> _commandQueue;
private readonly Thread _pwshThread;
private readonly CancellationTokenSource _cancellationSource;
public AsyncPowerShell()
{
_commandQueue = new BlockingCollection<Tuple<PSCommand, TaskCompletionSource<IReadOnlyCollection<PSObject>>>>();
_cancellationSource = new CancellationTokenSource();
_runspace = RunspaceFactory.CreateRunspace();
_runspace.Open();
_pwsh = PowerShell.Create();
_pwsh.Runspace = _runspace;
_runspace.Debugger.DebuggerStop += OnDebuggerStop;
_pwshThread = new Thread(RunPwshConsumer);
_pwshThread.Start();
}
public Task<IReadOnlyCollection<PSObject>> ExecutePowerShellAsync(string command)
{
var completionSource = new TaskCompletionSource<IReadOnlyCollection<PSObject>>();
_commandQueue.Add(new Tuple<PSCommand, TaskCompletionSource<IReadOnlyCollection<PSObject>>>(new PSCommand().AddCommand(command), completionSource));
return completionSource.Task;
}
public void Stop()
{
_commandQueue.CompleteAdding();
_cancellationSource.Cancel();
_pwshThread.Join();
}
private void RunPwshConsumer()
{
try
{
foreach (Tuple<PSCommand, TaskCompletionSource<IReadOnlyCollection<PSObject>>> executionRequest in _commandQueue.GetConsumingEnumerable(_cancellationSource.Token))
{
try
{
_pwsh.Commands = executionRequest.Item1;
executionRequest.Item2.SetResult(_pwsh.Invoke());
}
catch (OperationCanceledException)
{
executionRequest.Item2.SetCanceled();
}
catch (Exception e)
{
executionRequest.Item2.SetException(e);
}
finally
{
_pwsh.Commands.Clear();
}
}
}
catch (OperationCanceledException)
{
// End nicely
}
}
private void OnDebuggerStop(object sender, DebuggerStopEventArgs args)
{
Console.WriteLine("Debugger stopped");
}
} I think with some added sophistication, we could build off such a model to add:
The only part I don't quite understand in your description @SeeminglyScience is why we need another thread for message dispatch. @daxian-dbw might also be interested in this discussion |
Oops that made sense in the context of the original thread, but OmniSharp already does that. I'll rip that out |
Love the code btw ❤️. Basically the same thing I was picturing, except include an option to queue a Would also maybe make accessing some internals via reflection a bit safer. I think there have been a few situations where it would have been worth it to use an internal API, but threading concerns made it really risky. |
This is exactly what you're saying... |
I think setting the Setting @rjmholt @SeeminglyScience Do you mean the |
I know there are a few other things that are thread static though. Drawing a blank on them, but iirc one has to do with DSC cache. Also I do think it would easier to manage access to the thread than it is to manage what is using the runspace (that's more or less what we do today).
Yeah, the idea being is that
That's interesting... I kind of expected that would throw. Similar to the "cannot invoke async on nested pipeline" exception.
He was talking about a different part I already removed, but yeah that one kind of isn't either. That's also already mostly handled by omnisharp. I went ahead and just removed all that since @rjmholt's code shows what I'm talking about better than I was explaining it. |
The parts I'm not quite clear on here are:
|
That sounds like an unexpected use to me. What is the scenario? I looked into the PSES source code recently, and get the idea of how script execution for intellisense is done via the What does the existing |
@daxian-dbw do you know if PSReadLine handles either of those? |
For PowerShell/vscode-powershell#2448, PSReadLine doesn't handle breaking into debugger or the nested debugger prompt. The debugger prompt is handled by the PowerShell host. With For PowerShell/vscode-powershell#2246, the script block is converted to a delegate which calls |
I like all of this. It sounds like we could really simplify how we manage running (PowerShell or not) on the pipeline thread. My thinking is that this is something that can be totally standalone and not specific to PSES... That way it can be self-contained and easy to test. (I'm not saying it should be its own assembly - just that it can be in its own folder and is clear what the purpose is for) We tackled handling async requests with Omnisharp... the next big hurdle is our management of the PowerShell runspace... and this discussion should address the fear and uncertainty I have with the existing code which is great. |
Yeah that's my feeling too. Because of how PowerShell is set up, this execution model is coupled to the host implementation, which is something I didn't appreciate until reading through the implementation; I believe debugging and nested prompts only work if the host attached to the code implements them (or something along those lines). Basically, today we do a bunch of complicated stuff with the host implementation and instead my hope is to have the PowerShell executor and host implementation together in its own folder. Possibly for testing a dummy host will be needed. |
Also thanks for looking at those @daxian-dbw -- have been meaning to ask you how those get handled when run in pwsh.exe. Down the line we might be able to do something similar |
Yeah probably. Though this would be way easier to do if we stopped using OnIdle. I don't know if I ever recorded it anywhere, but I regret using that. Instead, PSRL should have a private contract delegate field (or a public API) that it calls on idle if not null. Using PowerShell's eventing is too flimsy and prone to dead locks. Especially when two things try to marshal a call back to the pipeline thread using it, it gets real messy. Edit: I should say that's all a guess. I haven't actually tried it, but I'd guess it's getting hung up when we try to retake PSRL with eventing while the event manager is already processing our first tab completion request.
I say we record the current "task" that's processing. I'm thinking the queue will take an interface like If we can't stop, then there's not much we can do. We can't pretend that we cancelled because it'll still be executing on the pipeline thread.
I think you just check for cancelled at the start of item processing, if it's cancelled move on to the next one. |
Or if you mean how do we cancel something that may or may not be currently processing (like might be the current item or might be further in the queue) then: We'd probably also want to store a lookup of job ID to job. The job itself would know if it's currently processing or not, if it is it'd call the appropriate cancel method, if not it'd just mark itself as cancelled. |
That's right. If no one subscribes to |
It's kind of like what this issue is discussing, but only for nested prompts/debug stops. The current execution flow is sort of like this:
Part of the problem with the above is that so much of the implementation of |
A private delegate field can be considered, but not a public API, at least initially. It allows you to avoid the The challenge I see is that PSReadLine will hold the pipeline thread (the thread with
@SeeminglyScience this summary is helpful for me to read the code, thanks! |
The problem isn't necessarily two things trying to marshal back at the same time, it's the nesting of them (see below).
Yeah for sure, lets take this series of events:
(this is a real example btw,
Well
❤️ |
Just a reminder if anyone is still working on this currently, if y'all get stuck, want advise, or would like me to work on something for this, let me know. DM's are open on the PS discord same name there (there's also a #vscode-contributors channel). Always down to chat PSES! 🙂 |
There's a Discord too??? @rjmholt is currently working on this, and when I'm done getting my feet wet (read: setting up PSES so I can actually debug it all and get the OmniSharp update done) he's going to transition it over to me (read: work on finishing it together). |
@andschwa yes the PS discord is quite popular :) |
@andschwa welcome back ! |
Big day! Congrats and thank you for all the hard word to get this done. |
A lot of the problems we face is based around handling of the pipeline thread. In order to invoke something on it, we need to interact with the
PowerShell
class, making us invoke at least a small bit of script in most cases. The reason for this is that the actual thread used by PowerShell is internal to the runspace by default. The only way to access it is to invoke a command of some sort.This is the default experience, but we can change it.
Runspace
has a property calledThreadOptions
. One of the choices for that enum isUseCurrentThread
. So what we can do is start our own thread, create the runspace there with that option, and never give up that thread.One of the biggest wins here would be that we could call
PSConsoleReadLine.ReadLine
directly without having to invoke something. We could also ditch using the thread pool to wait forPowerShell.Invoke
to finish (which probably causes some dead locks). We could get rid of a lot of the more complicated code inPowerShellContext
.I'm pretty confident that if this doesn't outright solve a lot of the sluggishness and dead locks, it'll at the very least be significantly easier to debug.
The rest of this post is taken from #980 since the idea is the same:Nvm, just look at @rjmholt's code and the rest of conversation. The linked post is pretty outdated.The text was updated successfully, but these errors were encountered: