Reduce string allocation in console output code (#6882)

This commit is contained in:
Ilya 2019-04-17 22:42:14 +05:00 committed by Dongbo Wang
parent 50efc4192d
commit 2174dd81a4
9 changed files with 294 additions and 123 deletions

View file

@ -2542,61 +2542,91 @@ namespace Microsoft.PowerShell
/// 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 @@ namespace Microsoft.PowerShell
[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);
}
}
[DllImport(PinvokeDllNames.GetConsoleTitleDllName, SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern DWORD GetConsoleTitle(StringBuilder consoleTitle, DWORD size);

View file

@ -705,9 +705,7 @@ namespace Microsoft.PowerShell
if ((options & ReadKeyOptions.NoEcho) == 0)
{
parent.WriteToConsole(
keyInfo.Character.ToString(),
true);
parent.WriteToConsole(keyInfo.Character, transcribeResult: true);
}
return keyInfo;

View file

@ -10,15 +10,9 @@ using Dbg = System.Management.Automation.Diagnostics;
namespace Microsoft.PowerShell
{
internal sealed partial
class ConsoleHost
:
PSHost,
IDisposable
internal sealed partial class ConsoleHost : PSHost, IDisposable
{
internal
bool
IsTranscribing
internal bool IsTranscribing
{
get
{
@ -69,9 +63,7 @@ namespace Microsoft.PowerShell
*/
private string _transcriptFileName = string.Empty;
internal
string
StopTranscribing()
internal string StopTranscribing()
{
lock (_transcriptionStateLock)
{
@ -106,15 +98,30 @@ namespace Microsoft.PowerShell
}
}
internal
void
WriteToTranscript(string text)
internal void WriteToTranscript(ReadOnlySpan<char> text)
{
WriteToTranscript(text, newLine: false);
}
internal void WriteLineToTranscript(ReadOnlySpan<char> text)
{
WriteToTranscript(text, newLine: true);
}
internal void WriteToTranscript(ReadOnlySpan<char> text, bool newLine)
{
lock (_transcriptionStateLock)
{
if (_isTranscribing && _transcriptionWriter != null)
{
_transcriptionWriter.Write(text);
if (newLine)
{
_transcriptionWriter.WriteLine(text);
}
else
{
_transcriptionWriter.Write(text);
}
}
}
}

View file

@ -2,16 +2,18 @@
// Licensed under the MIT License.
using System;
using System.IO;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Management.Automation.Runspaces;
using System.Text;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Management.Automation.Host;
using System.Management.Automation.Runspaces;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using Dbg = System.Management.Automation.Diagnostics;
#if !UNIX
using ConsoleHandle = Microsoft.Win32.SafeHandles.SafeFileHandle;
@ -268,7 +270,7 @@ namespace Microsoft.PowerShell
#else
// Ensure that we're in the proper line-input mode.
ConsoleControl.ConsoleModes desiredMode =
const ConsoleControl.ConsoleModes DesiredMode =
ConsoleControl.ConsoleModes.Extended |
ConsoleControl.ConsoleModes.QuickEdit;
@ -278,13 +280,13 @@ namespace Microsoft.PowerShell
bool shouldUnsetMouseInput = shouldUnsetMode(ConsoleControl.ConsoleModes.MouseInput, ref m);
bool shouldUnsetProcessInput = shouldUnsetMode(ConsoleControl.ConsoleModes.ProcessedInput, ref m);
if ((m & desiredMode) != desiredMode ||
if ((m & DesiredMode) != DesiredMode ||
shouldUnsetMouseInput ||
shouldUnsetEchoInput ||
shouldUnsetLineInput ||
shouldUnsetProcessInput)
{
m |= desiredMode;
m |= DesiredMode;
ConsoleControl.SetMode(handle, m);
}
else
@ -542,23 +544,35 @@ namespace Microsoft.PowerShell
#region WriteToConsole
internal void WriteToConsole(string value, bool transcribeResult)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void WriteToConsole(char c, bool transcribeResult)
{
ReadOnlySpan<char> value = stackalloc char[1] { c };
WriteToConsole(value, transcribeResult);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void WriteToConsole(ReadOnlySpan<char> value, bool transcribeResult)
{
WriteToConsole(value, transcribeResult, newLine: false);
}
private void WriteToConsole(ReadOnlySpan<char> value, bool transcribeResult, bool newLine)
{
#if !UNIX
ConsoleHandle handle = ConsoleControl.GetActiveScreenBufferHandle();
// Ensure that we're in the proper line-output mode. We don't lock here as it does not matter if we
// attempt to set the mode from multiple threads at once.
ConsoleControl.ConsoleModes m = ConsoleControl.GetMode(handle);
const ConsoleControl.ConsoleModes desiredMode =
ConsoleControl.ConsoleModes.ProcessedOutput
const ConsoleControl.ConsoleModes DesiredMode =
ConsoleControl.ConsoleModes.ProcessedOutput
| ConsoleControl.ConsoleModes.WrapEndOfLine;
if ((m & desiredMode) != desiredMode)
if ((m & DesiredMode) != DesiredMode)
{
m |= desiredMode;
m |= DesiredMode;
ConsoleControl.SetMode(handle, m);
}
#endif
@ -566,21 +580,20 @@ namespace Microsoft.PowerShell
PreWrite();
// This is atomic, so we don't lock here...
#if !UNIX
ConsoleControl.WriteConsole(handle, value);
ConsoleControl.WriteConsole(handle, value, newLine);
#else
Console.Out.Write(value);
ConsoleOutWriteHelper(value, newLine);
#endif
if (_isInteractiveTestToolListening && Console.IsOutputRedirected)
{
Console.Out.Write(value);
ConsoleOutWriteHelper(value, newLine);
}
if (transcribeResult)
{
PostWrite(value);
PostWrite(value, newLine);
}
else
{
@ -588,34 +601,64 @@ namespace Microsoft.PowerShell
}
}
private void WriteToConsole(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string text)
private void WriteToConsole(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string text, bool newLine = false)
{
ConsoleColor fg = RawUI.ForegroundColor;
ConsoleColor bg = RawUI.BackgroundColor;
RawUI.ForegroundColor = foregroundColor;
RawUI.BackgroundColor = backgroundColor;
try
// Sync access so that we don't race on color settings if called from multiple threads.
lock (_instanceLock)
{
WriteToConsole(text, true);
}
finally
{
RawUI.ForegroundColor = fg;
RawUI.BackgroundColor = bg;
ConsoleColor fg = RawUI.ForegroundColor;
ConsoleColor bg = RawUI.BackgroundColor;
RawUI.ForegroundColor = foregroundColor;
RawUI.BackgroundColor = backgroundColor;
try
{
WriteToConsole(text, transcribeResult: true, newLine);
}
finally
{
RawUI.ForegroundColor = fg;
RawUI.BackgroundColor = bg;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ConsoleOutWriteHelper(ReadOnlySpan<char> value, bool newLine)
{
if (newLine)
{
Console.Out.WriteLine(value);
}
else
{
Console.Out.Write(value);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void WriteLineToConsole(ReadOnlySpan<char> value, bool transcribeResult)
{
WriteToConsole(value, transcribeResult, newLine: true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteLineToConsole(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string text)
{
WriteToConsole(foregroundColor, backgroundColor, text, newLine: true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteLineToConsole(string text)
{
WriteToConsole(text, true);
WriteToConsole(Crlf, true);
WriteLineToConsole(text, transcribeResult: true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteLineToConsole()
{
WriteToConsole(Crlf, true);
WriteToConsole(Environment.NewLine, transcribeResult: true, newLine: false);
}
#endregion WriteToConsole
@ -636,15 +679,28 @@ namespace Microsoft.PowerShell
public override void Write(string value)
{
if (string.IsNullOrEmpty(value))
{
// do nothing
WriteImpl(value, newLine: false);
}
private void WriteImpl(string value, bool newLine)
{
if (string.IsNullOrEmpty(value) && !newLine)
{
return;
}
// If the test hook is set, write to it and continue.
if (s_h != null) s_h.Write(value);
if (s_h != null)
{
if (newLine)
{
s_h.WriteLine(value);
}
else
{
s_h.Write(value);
}
}
TextWriter writer = Console.IsOutputRedirected ? Console.Out : _parent.ConsoleTextWriter;
@ -653,10 +709,22 @@ namespace Microsoft.PowerShell
Dbg.Assert(writer == _parent.OutputSerializer.textWriter, "writers should be the same");
_parent.OutputSerializer.Serialize(value);
if (newLine)
{
_parent.OutputSerializer.Serialize(Crlf);
}
}
else
{
writer.Write(value);
if (newLine)
{
writer.WriteLine(value);
}
else
{
writer.Write(value);
}
}
}
@ -682,8 +750,36 @@ namespace Microsoft.PowerShell
public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
{
// Sync access so that we don't race on color settings if called from multiple threads.
Write(foregroundColor, backgroundColor, value, newLine: false);
}
/// <summary>
/// See base class.
/// </summary>
/// <param name="foregroundColor"></param>
/// <param name="backgroundColor"></param>
/// <param name="value"></param>
/// <exception cref="HostException">
/// If obtaining information about the buffer failed
/// OR
/// Win32's SetConsoleTextAttribute
/// OR
/// Win32's CreateFile fails
/// OR
/// Win32's GetConsoleMode fails
/// OR
/// Win32's SetConsoleMode fails
/// OR
/// Win32's WriteConsole fails
/// </exception>
public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
{
Write(foregroundColor, backgroundColor, value, newLine: true);
}
private void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value, bool newLine)
{
// Sync access so that we don't race on color settings if called from multiple threads.
lock (_instanceLock)
{
ConsoleColor fg = RawUI.ForegroundColor;
@ -694,7 +790,7 @@ namespace Microsoft.PowerShell
try
{
this.Write(value);
this.WriteImpl(value, newLine);
}
finally
{
@ -717,16 +813,26 @@ namespace Microsoft.PowerShell
/// OR
/// Win32's WriteConsole fails
/// </exception>
public override void WriteLine(string value)
{
// lock here so that the newline is written atomically with the value
this.WriteImpl(value, newLine: true);
}
lock (_instanceLock)
{
this.Write(value);
this.Write(Crlf);
}
/// <summary>
/// See base class.
/// </summary>
/// <exception cref="HostException">
/// Win32's CreateFile fails
/// OR
/// Win32's GetConsoleMode fails
/// OR
/// Win32's SetConsoleMode fails
/// OR
/// Win32's WriteConsole fails
/// </exception>
public override void WriteLine()
{
this.WriteImpl(Environment.NewLine, newLine: false);
}
#region Word Wrapping
@ -1227,8 +1333,6 @@ namespace Microsoft.PowerShell
{
if (string.IsNullOrEmpty(value))
{
// do nothing
return;
}
@ -1247,7 +1351,7 @@ namespace Microsoft.PowerShell
if (writer == _parent.ConsoleTextWriter)
WriteLine(ErrorForegroundColor, ErrorBackgroundColor, value);
else
Console.Error.Write(value + Crlf);
Console.Error.WriteLine(value);
}
}
@ -1419,15 +1523,15 @@ namespace Microsoft.PowerShell
ConsoleHandle handle = ConsoleControl.GetConioDeviceHandle();
ConsoleControl.ConsoleModes m = ConsoleControl.GetMode(handle);
const ConsoleControl.ConsoleModes desiredMode =
const ConsoleControl.ConsoleModes DesiredMode =
ConsoleControl.ConsoleModes.LineInput
| ConsoleControl.ConsoleModes.EchoInput
| ConsoleControl.ConsoleModes.ProcessedInput;
if ((m & desiredMode) != desiredMode || (m & ConsoleControl.ConsoleModes.MouseInput) > 0)
if ((m & DesiredMode) != DesiredMode || (m & ConsoleControl.ConsoleModes.MouseInput) > 0)
{
m &= ~ConsoleControl.ConsoleModes.MouseInput;
m |= desiredMode;
m |= DesiredMode;
ConsoleControl.SetMode(handle, m);
}
#endif
@ -1941,7 +2045,7 @@ namespace Microsoft.PowerShell
{
// Reads always terminate with the enter key, so add that.
_parent.WriteToTranscript(input + Crlf);
_parent.WriteLineToTranscript(input);
}
return input;

View file

@ -130,7 +130,7 @@ namespace Microsoft.PowerShell
private
void
PostWrite(string value)
PostWrite(ReadOnlySpan<char> value, bool newLine)
{
PostWrite();
@ -138,7 +138,7 @@ namespace Microsoft.PowerShell
{
try
{
_parent.WriteToTranscript(value);
_parent.WriteToTranscript(value, newLine);
}
catch (Exception)
{
@ -178,7 +178,7 @@ namespace Microsoft.PowerShell
try
{
// Reads always terminate with the enter key, so add that.
_parent.WriteToTranscript(value + Crlf);
_parent.WriteLineToTranscript(value);
}
catch (Exception)
{

View file

@ -118,8 +118,7 @@ namespace Microsoft.PowerShell
// Should be a skin lookup
WriteLineToConsole();
WriteToConsole(PromptColor, RawUI.BackgroundColor, WrapToCurrentWindowWidth(caption));
WriteLineToConsole();
WriteLineToConsole(PromptColor, RawUI.BackgroundColor, WrapToCurrentWindowWidth(caption));
}
if (!string.IsNullOrEmpty(message))

View file

@ -70,8 +70,7 @@ namespace Microsoft.PowerShell
// Should be a skin lookup
WriteLineToConsole();
WriteToConsole(PromptColor, RawUI.BackgroundColor, WrapToCurrentWindowWidth(caption));
WriteLineToConsole();
WriteLineToConsole(PromptColor, RawUI.BackgroundColor, WrapToCurrentWindowWidth(caption));
}
if (!string.IsNullOrEmpty(message))
@ -216,8 +215,7 @@ namespace Microsoft.PowerShell
{
// Should be a skin lookup
WriteLineToConsole();
WriteToConsole(PromptColor, RawUI.BackgroundColor, WrapToCurrentWindowWidth(caption));
WriteLineToConsole();
WriteLineToConsole(PromptColor, RawUI.BackgroundColor, WrapToCurrentWindowWidth(caption));
}
// write message
if (!string.IsNullOrEmpty(message))

View file

@ -72,8 +72,7 @@ namespace Microsoft.PowerShell
// Should be a skin lookup
WriteLineToConsole();
WriteToConsole(PromptColor, RawUI.BackgroundColor, WrapToCurrentWindowWidth(caption));
WriteLineToConsole();
WriteLineToConsole(PromptColor, RawUI.BackgroundColor, WrapToCurrentWindowWidth(caption));
}
if (!string.IsNullOrEmpty(message))

View file

@ -39,38 +39,59 @@ namespace Microsoft.PowerShell
void
Write(string value)
{
_ui.WriteToConsole(value, true);
_ui.WriteToConsole(value, transcribeResult: true);
}
public override
void
Write(ReadOnlySpan<char> value)
{
_ui.WriteToConsole(value, transcribeResult: true);
}
public override
void
WriteLine(string value)
{
this.Write(value + ConsoleHostUserInterface.Crlf);
_ui.WriteLineToConsole(value, transcribeResult: true);
}
public override
void
Write(Boolean b)
WriteLine(ReadOnlySpan<char> value)
{
this.Write(b.ToString());
_ui.WriteLineToConsole(value, transcribeResult: true);
}
public override
void
Write(bool b)
{
if (b)
{
_ui.WriteToConsole(bool.TrueString, transcribeResult: true);
}
else
{
_ui.WriteToConsole(bool.FalseString, transcribeResult: true);
}
}
public override
void
Write(char c)
{
this.Write(new String(c, 1));
ReadOnlySpan<char> c1 = stackalloc char[1] { c };
_ui.WriteToConsole(c1, transcribeResult: true);
}
public override
void
Write(char[] a)
{
this.Write(new String(a));
_ui.WriteToConsole(a, transcribeResult: true);
}
private ConsoleHostUserInterface _ui;
}
} // namespace
}