-
Notifications
You must be signed in to change notification settings - Fork 237
Fix performance issue with ReadKey changes #621
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
Fix performance issue with ReadKey changes #621
Conversation
- Separated the ReadKey changes to be platform specific. This way we don't need a busy loop to wait until a key is available on Windows. - Changed the Unix implementation of ReadKey to wait for a longer period of time between Console.KeyAvailable checks if the user has not pressed a key within the last five seconds. This change was made to ensure the CPU is able to enter low power mode.
@tylerl0706 mind restarting AppVeyor when you get a minute? Looks like it hung completely this time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll approve after I try it out on my mac.
|
||
private PowerShellContext powerShellContext; | ||
|
||
#endregion | ||
|
||
#region Constructors | ||
static ConsoleReadLine() | ||
{ | ||
// Maybe we should just include the RuntimeInformation package for FullCLR? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know the answer to this question but I think we should answer it in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm split on it. The compiler directives feel a little messy, especially if we end up using them more. On the other hand including an assembly that's probably going to be included in a whole bunch of modules doesn't feel great either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I opted to include System.Runtime.InteropServices.RuntimeInformation.dll
in PSReadLine
so I could build and ship a single assembly that works on all supported platforms - it definitely kept builds/packing simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lzybkr Yeah that would be ideal for sure. Do you know of a way to do that and remain buildable on Unix? I came up empty last time I looked into it. The blocker was targeting only net46x
required Windows (or Mono?) and targeting only netstandard
required shims to load properly in Windows PowerShell.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SeeminglyScience - I haven't done this, but I think you would need to build targets, one for net46x
and one for netstandard
. I don't run PSReadLine
tests on Linux/Mac (and there is very little reason to, the platform specifics are mostly mocked anyway), so haven't dealt with this situation yet.
|
||
namespace Microsoft.PowerShell.EditorServices.Console | ||
{ | ||
internal class UnixConsoleOperations : IConsoleOperations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this apply to Unix and Linux?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm using "Unix" to refer to Mac and Linux. Not sure if there's a better term, but that's what corefx does.
|
||
// I tried to replace this library with a call to `stty -echo`, but unfortunately | ||
// the library also sets up allowing backspace to trigger `Console.KeyAvailable`. | ||
InputEcho.Disable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is your library, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worked on Mac :) signing off on that reason. Others should review as well.
The steps I took:
|
/// </returns> | ||
Task<ConsoleKeyInfo> ReadKeyAsync(CancellationToken cancellationToken); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For POSIX compliance, don't we want a newline at the end of the file? See https://stackoverflow.com/questions/729692/why-should-text-files-end-with-a-newline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
POSIX compliance is sometimes overrated :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just thinking if someone wanted to work on this repo on Linux/macOS. OTOH they'd probably be using PSCore utilities on files and not the native utilities.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rkeithhill good catch, I usually have the VSCode workspace setting that adds it automatically. Any objections to me adding it here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not from me. In fact, add that setting to the .vscode\settings.json file. Or maybe we adopt .editorconfig so that VS 2017 will honor this setting. There's a plugin for VSCode for EditorConfig so that VSCode will also honor the setting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rkeithhill added to the settings.json
for now, but I agree with translating that to an editor config. I'll make a separate issue for that.
try | ||
{ | ||
return | ||
_bufferedKey.HasValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this ever be anything but false
? _bufferedKey
is private and I'm not seeing how it would get set before we reach this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rkeithhill it's set on line 22. I was trying to avoid the whole temp variable dance from the original implementation. Might have went a little too complicated with it though. @daviwil thoughts?
|
||
private async Task<bool> ShortWaitForKey(CancellationToken cancellationToken) | ||
{ | ||
if (await SpinUntilKeyAvailable(SHORT_READ_TIMEOUT)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any chance we could we get @lzybkr to review this bit? He's more of a perf wonk. :-)
{ | ||
return await Task<bool>.Factory.StartNew( | ||
() => SpinWait.SpinUntil( | ||
() => System.Console.KeyAvailable, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a feeling you'll want a short sleep in here, keyboard latency is at least 15ms (see https://danluu.com/keyboard-latency/) so you can consume a bit less CPU by yielding frequently during those 5 seconds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a 30ms sleep between KeyAvailable
checks
|
||
private PowerShellContext powerShellContext; | ||
|
||
#endregion | ||
|
||
#region Constructors | ||
static ConsoleReadLine() | ||
{ | ||
// Maybe we should just include the RuntimeInformation package for FullCLR? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I opted to include System.Runtime.InteropServices.RuntimeInformation.dll
in PSReadLine
so I could build and ship a single assembly that works on all supported platforms - it definitely kept builds/packing simpler.
/// </returns> | ||
Task<ConsoleKeyInfo> ReadKeyAsync(CancellationToken cancellationToken); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
POSIX compliance is sometimes overrated :)
{ | ||
internal class UnixConsoleOperations : IConsoleOperations | ||
{ | ||
private const int LONG_READ_DELAY = 400; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this feel laggy? 300
is probably fine and would help feel less laggy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started with 500
which did feel laggy. 400
didn't feel laggy, but that was through VNC so not super accurate. Either way, I'm definitely more comfortable with 300
, I'll get that in. Thanks for your help on this! 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switched to 300ms. The first delay can still feel a little laggy if you type particularly fast and catch it right at the start of the wait, but I don't think it'll be all that noticeable if you aren't looking for it. At least good enough until the issue is fixed in corefx.
- Added final new lines to new files - Added VSCode workspace setting to automatically add final new lines - Changed unix readkey long delay to 300ms from 400ms - Added a short wait timer to unix readkey short delay to reduce CPU consumption
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks awesome! One area of concern though
_bufferedKey.HasValue | ||
? _bufferedKey.Value | ||
: await Task.Factory.StartNew( | ||
() => (_bufferedKey = System.Console.ReadKey(intercept: true)).Value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this implementation might have a different effect than the original, but correct me if I'm wrong. The original implementation only stores the buffered key if the cancellation token is cancelled so that it can be returned the next time the ReadKeyAsync method is called.
In the new implementation, we're buffering the key on each read, potentially causing the same key to be returned twice since it gets returned the first time and then buffered to be returned once again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Patrick just explained how this works to me over Slack, the finally
block takes care of clearing the bufferedKey if the CancellationToken hasn't been cancelled :) Looks good!
_bufferedKey.HasValue | ||
? _bufferedKey.Value | ||
: await Task.Factory.StartNew( | ||
() => (_bufferedKey = System.Console.ReadKey(intercept: true)).Value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Patrick just explained how this works to me over Slack, the finally
block takes care of clearing the bufferedKey if the CancellationToken hasn't been cancelled :) Looks good!
So excited to merge this :) thanks Patrick! |
Separated the
ReadKey
changes to be platform specific. This way we don't need a busy loop to wait until a key is available on Windows.Changed the Unix implementation of
ReadKey
to wait for a longer period of time betweenConsole.KeyAvailable
checks if the user has not pressed a key within the last five seconds. This change was made to ensure the CPU is able to enter low power mode.@tylerl0706 if you could do the honors of Mac testing again, that'd be awesome.