-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Optimize console output in ConsoleHost #6882
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
Changes from all commits
0043f30
fdd4e8d
c8e106c
ded4dd7
a6be5b1
9250356
afea4df
0354f72
b66f03e
42823c6
273b1b7
5b7bb77
e57001d
d16d779
c97670d
232e303
59b6cd0
f6a72af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2542,61 +2542,91 @@ internal static void SetConsoleWindowTitle(string consoleTitle) | |
| /// Wrap Win32 WriteConsole. | ||
| /// </summary> | ||
| /// <param name="consoleHandle"> | ||
| /// handle for the console where the string is written | ||
| /// Handle for the console where the string is written. | ||
| /// </param> | ||
| /// <param name="output"> | ||
| /// string that is written | ||
| /// String that is written. | ||
| /// </param> | ||
| /// <param name="newLine"> | ||
| /// New line is written. | ||
| /// </param> | ||
| /// <exception cref="HostException"> | ||
| /// if the Win32's WriteConsole fails | ||
| /// If the Win32's WriteConsole fails. | ||
| /// </exception> | ||
|
|
||
| internal static void WriteConsole(ConsoleHandle consoleHandle, string output) | ||
| internal static void WriteConsole(ConsoleHandle consoleHandle, ReadOnlySpan<char> output, bool newLine) | ||
| { | ||
| Dbg.Assert(!consoleHandle.IsInvalid, "ConsoleHandle is not valid"); | ||
| Dbg.Assert(!consoleHandle.IsClosed, "ConsoleHandle is closed"); | ||
|
|
||
| if (string.IsNullOrEmpty(output)) | ||
| if (output.Length == 0) | ||
| { | ||
| if (newLine) | ||
| { | ||
| WriteConsole(consoleHandle, ConsoleHostUserInterface.Crlf); | ||
| } | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| // Native WriteConsole doesn't support output buffer longer than 64K. | ||
| // We need to chop the output string if it is too long. | ||
|
|
||
| int cursor = 0; // This records the chopping position in output string | ||
| const int maxBufferSize = 16383; // this is 64K/4 - 1 to account for possible width of each character. | ||
| const int MaxBufferSize = 16383; // this is 64K/4 - 1 to account for possible width of each character. | ||
|
|
||
| while (cursor < output.Length) | ||
| { | ||
| string outBuffer; | ||
| ReadOnlySpan<char> outBuffer; | ||
|
|
||
| if (cursor + maxBufferSize < output.Length) | ||
| if (cursor + MaxBufferSize < output.Length) | ||
| { | ||
| outBuffer = output.Substring(cursor, maxBufferSize); | ||
| cursor += maxBufferSize; | ||
| outBuffer = output.Slice(cursor, MaxBufferSize); | ||
| cursor += MaxBufferSize; | ||
|
|
||
| WriteConsole(consoleHandle, outBuffer); | ||
| } | ||
| else | ||
| { | ||
| outBuffer = output.Substring(cursor); | ||
| outBuffer = output.Slice(cursor); | ||
| cursor = output.Length; | ||
|
|
||
| if (newLine) | ||
| { | ||
| var endOfLine = ConsoleHostUserInterface.Crlf.AsSpan(); | ||
| var endOfLineLength = endOfLine.Length; | ||
| Span<char> outBufferLine = stackalloc char[outBuffer.Length + endOfLineLength]; | ||
| outBuffer.CopyTo(outBufferLine); | ||
| endOfLine.CopyTo(outBufferLine.Slice(outBufferLine.Length - endOfLineLength)); | ||
| WriteConsole(consoleHandle, outBufferLine); | ||
| } | ||
| else | ||
| { | ||
| WriteConsole(consoleHandle, outBuffer); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| DWORD charsWritten; | ||
| bool result = | ||
| NativeMethods.WriteConsole( | ||
| consoleHandle.DangerousGetHandle(), | ||
| outBuffer, | ||
| (DWORD)outBuffer.Length, | ||
| out charsWritten, | ||
| IntPtr.Zero); | ||
| private static void WriteConsole(ConsoleHandle consoleHandle, ReadOnlySpan<char> buffer) | ||
| { | ||
| DWORD charsWritten; | ||
| bool result = | ||
| NativeMethods.WriteConsole( | ||
| consoleHandle.DangerousGetHandle(), | ||
| buffer, | ||
| (DWORD)buffer.Length, | ||
| out charsWritten, | ||
| IntPtr.Zero); | ||
|
|
||
| if (result == false) | ||
| { | ||
| int err = Marshal.GetLastWin32Error(); | ||
| if (result == false) | ||
| { | ||
| int err = Marshal.GetLastWin32Error(); | ||
|
|
||
| HostException e = CreateHostException(err, "WriteConsole", | ||
| ErrorCategory.WriteError, ConsoleControlStrings.WriteConsoleExceptionTemplate); | ||
| throw e; | ||
| } | ||
| HostException e = CreateHostException( | ||
| err, | ||
| "WriteConsole", | ||
| ErrorCategory.WriteError, | ||
| ConsoleControlStrings.WriteConsoleExceptionTemplate); | ||
| throw e; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -3009,15 +3039,30 @@ out DWORD numberOfCharsWritten | |
|
|
||
| [DllImport(PinvokeDllNames.WriteConsoleDllName, SetLastError = true, CharSet = CharSet.Unicode)] | ||
| [return: MarshalAs(UnmanagedType.Bool)] | ||
| internal static extern bool WriteConsole | ||
| private static extern unsafe bool WriteConsole | ||
| ( | ||
| NakedWin32Handle consoleOutput, | ||
| string buffer, | ||
| char* buffer, | ||
| DWORD numberOfCharsToWrite, | ||
| out DWORD numberOfCharsWritten, | ||
| IntPtr reserved | ||
| ); | ||
|
|
||
| internal static unsafe bool WriteConsole | ||
| ( | ||
| NakedWin32Handle consoleOutput, | ||
| ReadOnlySpan<char> buffer, | ||
| DWORD numberOfCharsToWrite, | ||
| out DWORD numberOfCharsWritten, | ||
| IntPtr reserved | ||
| ) | ||
| { | ||
| fixed (char* bufferPtr = &MemoryMarshal.GetReference(buffer)) | ||
| { | ||
| return WriteConsole(consoleOutput, bufferPtr, numberOfCharsToWrite, out numberOfCharsWritten, reserved); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This obviously complexes the code. Actually, all changes here make code more complex. Do we have measurable perf improvement to justify the changes? I don't see a large amount of string manipulation here (please point it out if I miss them). Note that many string APIs and
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like complement - I'm not sure I can create a complex code 😄 Exactly the pattern I found in CoreFX. This is due to C# language limitations. Really the code isn't getting any more complicated. Rather on the contrary it becomes more complete. WriteToConsole(text, true);
WriteToConsole(Crlf, true);I was very surprised that we don't have a WriteLine() method to write to a host and use many allocations like
Yes, I saw up to 1100000 allocations in VS Profiler at pwsh.exe 6.0 startup and up to 850000 allocation at 6.1 (already on .Net 2.1) startup. Nevertheless, I still see a lot of allocations. The profile show that up to 36% allocations is chars and strings.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not convinced whether the startup is the right scenario to measure for changes in this PR, but still, for the startup scenario, what's the improvement in object allocation with your changes?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it was only sample that 2.1 reduce allocations but not remove its at all. I don't know how measure wins in the PR - VS Profiler is crashed after first run. I look https://benchmarkdotnet.org/ but it takes a lot of effort. At least in the first steps.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I attached a file with simple perf tests. I saw win ~2 second on 100000 outputs.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really need any of this code anymore? It was written back in V1 when the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that this code would have to be cleaned more thoroughly. The problem is that we do not have tests for the console code. Therefore, I would prefer to make small changes step by step so as not to break something. I intend to continue working in this direction (if only you do not chop it in one your PR). |
||
| } | ||
|
|
||
| [DllImport(PinvokeDllNames.GetConsoleTitleDllName, SetLastError = true, CharSet = CharSet.Unicode)] | ||
| internal static extern DWORD GetConsoleTitle(StringBuilder consoleTitle, DWORD size); | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.