Skip to content

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

Merged

Conversation

SeeminglyScience
Copy link
Collaborator

  • 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 if you could do the honors of Mac testing again, that'd be awesome.

- 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.
@SeeminglyScience
Copy link
Collaborator Author

@tylerl0706 mind restarting AppVeyor when you get a minute? Looks like it hung completely this time.

Copy link
Member

@TylerLeonhardt TylerLeonhardt left a 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?
Copy link
Member

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.

Copy link
Collaborator Author

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.

Copy link

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.

Copy link
Collaborator Author

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.

Copy link

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
Copy link
Member

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?

Copy link
Collaborator Author

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();
Copy link
Member

@TylerLeonhardt TylerLeonhardt Feb 5, 2018

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Correct

Copy link
Member

@TylerLeonhardt TylerLeonhardt left a 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.

@TylerLeonhardt
Copy link
Member

The steps I took:

  1. Start debugging a file with break points, make sure you can
    • Press F5 to continue to the next breakpoint
    • Press F11 to step to the next sequence point
    • Never have to hit enter in the integrated console to do any of that
  2. Spam input to see if any input is lost or just feels bad (timeout between checking for a available key may be need to be adjusted)
  3. Run $Host.UI.ReadLineAsSecureString() and ensure plain text characters aren't echoed to the screen

/// </returns>
Task<ConsoleKeyInfo> ReadKeyAsync(CancellationToken cancellationToken);
}
}
Copy link
Contributor

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

Copy link

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 :)

Copy link
Contributor

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.

Copy link
Collaborator Author

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?

Copy link
Contributor

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.

Copy link
Collaborator Author

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
Copy link
Contributor

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.

Copy link
Collaborator Author

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))
Copy link
Contributor

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,
Copy link

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.

Copy link
Collaborator Author

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?
Copy link

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);
}
}
Copy link

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;
Copy link

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.

Copy link
Collaborator Author

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! 😄

Copy link
Collaborator Author

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
Copy link
Contributor

@daviwil daviwil left a 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);
Copy link
Contributor

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?

Copy link
Contributor

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);
Copy link
Contributor

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!

@TylerLeonhardt
Copy link
Member

So excited to merge this :) thanks Patrick!

@TylerLeonhardt TylerLeonhardt merged commit 406f10b into PowerShell:master Feb 20, 2018
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.

5 participants