diff --git a/src/System.Management.Automation/System.Management.Automation.csproj b/src/System.Management.Automation/System.Management.Automation.csproj
index 203d0a6afc..10f2ae45ae 100644
--- a/src/System.Management.Automation/System.Management.Automation.csproj
+++ b/src/System.Management.Automation/System.Management.Automation.csproj
@@ -16,6 +16,7 @@
+
diff --git a/src/System.Management.Automation/namespaces/FileSystemContentStream.cs b/src/System.Management.Automation/namespaces/FileSystemContentStream.cs
index c7b9b80000..a6612c6312 100644
--- a/src/System.Management.Automation/namespaces/FileSystemContentStream.cs
+++ b/src/System.Management.Automation/namespaces/FileSystemContentStream.cs
@@ -6,11 +6,12 @@ using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Management.Automation.Provider;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
using System.Threading.Tasks;
using Dbg = System.Management.Automation;
@@ -54,8 +55,11 @@ namespace Microsoft.PowerShell.Commands
private StreamReader _reader;
private StreamWriter _writer;
private bool _usingByteEncoding;
- private string _delimiter = "\n";
+ private const char DefaultDelimiter = '\n';
+ private string _delimiter = $"{DefaultDelimiter}";
+ private int[] _offsetDictionary;
private bool _usingDelimiter;
+ private StringBuilder _currentLineContent;
private bool _waitForChanges;
private bool _isRawStream;
private long _fileOffset;
@@ -222,50 +226,6 @@ namespace Microsoft.PowerShell.Commands
_suppressNewline = suppressNewline;
}
- ///
- /// Constructor for the content stream
- ///
- ///
- /// The path to the file to get the content from.
- ///
- ///
- /// The file mode to open the file with.
- ///
- ///
- /// The file access requested in the file.
- ///
- ///
- /// The file share to open the file with
- ///
- ///
- /// The delimiter to use when reading strings. Each time read is called, all contents up to an including
- /// the delimiter is read.
- ///
- ///
- /// The encoding of the file to be read or written.
- ///
- ///
- /// If true, we will perform blocking reads on the file, waiting for new content to be appended
- ///
- ///
- /// The CmdletProvider invoking this stream
- ///
- ///
- /// Indicates raw stream.
- ///
- public FileSystemContentReaderWriter(
- string path,
- FileMode mode,
- FileAccess access,
- FileShare share,
- string delimiter,
- Encoding encoding,
- bool waitForChanges,
- CmdletProvider provider,
- bool isRawStream) : this(path, null, mode, access, share, encoding, false, waitForChanges, provider, isRawStream)
- {
- }
-
///
/// Constructor for the content stream
///
@@ -314,8 +274,50 @@ namespace Microsoft.PowerShell.Commands
bool isRawStream)
: this(path, streamName, mode, access, share, encoding, false, waitForChanges, provider, isRawStream)
{
- _delimiter = delimiter;
- _usingDelimiter = true;
+ // If the delimiter is default ('\n') we'll use ReadLine() method.
+ // Otherwise allocate temporary structures for ReadDelimited() method.
+ if (!(delimiter.Length == 1 && delimiter[0] == DefaultDelimiter))
+ {
+ _delimiter = delimiter;
+ _usingDelimiter = true;
+
+ // We expect that we are parsing files where line lengths can be relatively long.
+ const int DefaultLineLength = 256;
+ _currentLineContent = new StringBuilder(DefaultLineLength);
+
+ // For Boyer-Moore string search algorithm.
+ // Populate the offset lookups.
+ // These will tell us the maximum number of characters
+ // we can read to generate another possible match (safe shift).
+ // If we read more characters than this, we risk consuming
+ // more of the stream than we need.
+ //
+ // Because an unicode character size is 2 byte we would to have use
+ // very large array with 65535 size to keep this safe offsets.
+ // One solution is to pack unicode character to byte.
+ // The workaround is to use low byte from unicode character.
+ // This allow us to use small array with size 256.
+ // This workaround is the fastest and provides excellent results
+ // in regular search scenarios when the file contains
+ // mostly characters from the same alphabet.
+ _offsetDictionary = new int[256];
+
+ // If next char from file is not in search pattern safe shift is the search pattern length.
+ for (var n = 0; n < _offsetDictionary.Length; n++)
+ {
+ _offsetDictionary[n] = _delimiter.Length;
+ }
+
+ // If next char from file is in search pattern we should calculate a safe shift.
+ char currentChar;
+ byte lowByte;
+ for (var i = 0; i < _delimiter.Length; i++)
+ {
+ currentChar = _delimiter[i];
+ lowByte = Unsafe.As(ref currentChar);
+ _offsetDictionary[lowByte] = _delimiter.Length - i - 1;
+ }
+ }
}
///
@@ -578,103 +580,111 @@ namespace Microsoft.PowerShell.Commands
private bool ReadDelimited(bool waitChanges, ArrayList blocks, bool readBackward, string actualDelimiter)
{
+ if (_isRawStream)
+ {
+ // when -Raw is used we want to anyway read the whole thing
+ // so avoiding the while loop by reading the entire content.
+ string contentRead = _reader.ReadToEnd();
+ if (contentRead.Length > 0)
+ {
+ blocks.Add(contentRead);
+ }
+
+ // We already read whole file so return EOF.
+ return false;
+ }
+
// Since the delimiter is a string, we're essentially
// dealing with a "find the substring" algorithm, but with
// the additional restriction that we cannot read past the
- // end of the delimiter. If we read past the end of the delimiter,
+ // end of the delimiter. If we read past the end of the delimiter,
// then we'll eat up bytes that we need from the filestream.
// The solution is a modified Boyer-Moore string search algorithm.
// This version retains the sub-linear search performance (via the
- // lookup tables,) but offloads much of the dirty work to the
- // very efficient BCL String.IndexOf(, StringComparison.Ordinal) method.
+ // lookup tables).
int numRead = 0;
int currentOffset = actualDelimiter.Length;
- StringBuilder content = new StringBuilder();
-
- // Populate the offset lookups
- // These will tell us the maximum number of characters
- // we can read to generate another possible match.
- // If we read more characters than this, we risk consuming
- // more of the stream than we need.
- Dictionary offsetDictionary = new Dictionary();
- foreach (char currentChar in actualDelimiter)
- offsetDictionary[currentChar] = actualDelimiter.Length - actualDelimiter.LastIndexOf(currentChar) - 1;
+ Span readBuffer = stackalloc char[currentOffset];
+ bool delimiterNotFound = true;
+ _currentLineContent.Clear();
do
{
- if (_isRawStream)
- {
- // when -Raw is used we want to anyway read the whole thing
- // so avoiding the while loop by reading the entire content.
- string contentRead = _reader.ReadToEnd();
- numRead = contentRead.Length;
- content.Append(contentRead);
- }
- else
- {
- // Read in the required batch of characters
- var readBuffer = new char[currentOffset];
- numRead = readBackward
- ? _backReader.Read(readBuffer, 0, currentOffset)
- : _reader.Read(readBuffer, 0, currentOffset);
+ // Read in the required batch of characters
+ numRead = readBackward
+ ? _backReader.Read(readBuffer.Slice(0, currentOffset))
+ : _reader.Read(readBuffer.Slice(0, currentOffset));
- // If we want to wait for changes, then we'll keep on attempting to read
- // until we fill the buffer.
- if (numRead == 0)
+ // If we want to wait for changes, then we'll keep on attempting to read
+ // until we fill the buffer.
+ if (numRead == 0)
+ {
+ if (waitChanges)
{
- if (waitChanges)
+ // But stop reading if the provider is stopping
+ while ((numRead < currentOffset) && (!_provider.Stopping))
{
- // But stop reading if the provider is stopping
- while ((numRead < currentOffset) && (!_provider.Stopping))
+ // Get the change, and try to read more characters
+ // We only wait for changes when read forwards, so here we don't need to check if 'readBackward' is
+ // true or false, we only use 'reader'. The member 'reader' will be updated by WaitForChanges.
+ WaitForChanges(_path, _mode, _access, _share, _reader.CurrentEncoding);
+ numRead += _reader.Read(readBuffer.Slice(0, (currentOffset - numRead)));
+ }
+ }
+ }
+
+ if (numRead > 0)
+ {
+ _currentLineContent.Append(readBuffer.Slice(0, numRead));
+
+ // Look up the final character in our offset table.
+ // If the character doesn't exist in the lookup table, then it's not in
+ // our search key. That means the match must happen strictly /after/ the
+ // current position. Because of that, we can feel confident reading in the
+ // number of characters in the search key, without the risk of reading too many.
+ var currentChar = _currentLineContent[_currentLineContent.Length - 1];
+ currentOffset = _offsetDictionary[Unsafe.As(ref currentChar)];
+
+ // We want to keep reading if delimiter not found and we haven't hit the end of file
+ delimiterNotFound = true;
+
+ // If the final letters matched, then we will get an offset of "0".
+ // In that case, we'll either have a match (and break from the while loop,)
+ // or we need to move the scan forward one position.
+ if (currentOffset == 0)
+ {
+ currentOffset = 1;
+
+ if (actualDelimiter.Length <= _currentLineContent.Length)
+ {
+ delimiterNotFound = false;
+ int i = 0;
+ int j = _currentLineContent.Length - actualDelimiter.Length;
+ for (; i < actualDelimiter.Length; i++, j++)
{
- // Get the change, and try to read more characters
- // We only wait for changes when read forwards, so here we don't need to check if 'readBackward' is
- // true or false, we only use 'reader'. The member 'reader' will be updated by WaitForChanges.
- WaitForChanges(_path, _mode, _access, _share, _reader.CurrentEncoding);
- numRead += _reader.Read(readBuffer, 0, (currentOffset - numRead));
+ if (actualDelimiter[i] != _currentLineContent[j])
+ {
+ delimiterNotFound = true;
+ break;
+ }
}
}
}
-
- if (numRead > 0)
- {
- content.Append(readBuffer, 0, numRead);
-
- // Look up the final character in our offset table.
- // If the character doesn't exist in the lookup table, then it's not in
- // our search key. That means the match must happen strictly /after/ the
- // current position. Because of that, we can feel confident reading in the
- // number of characters in the search key, without the risk of reading too many.
- if (!offsetDictionary.TryGetValue(content[content.Length - 1], out currentOffset))
- currentOffset = actualDelimiter.Length;
-
- // If the final letters matched, then we will get an offset of "0".
- // In that case, we'll either have a match (and break from the while loop,)
- // or we need to move the scan forward one position.
- if (currentOffset == 0)
- currentOffset = 1;
- }
}
-
- // Two cases where we want to keep reading:
- // 1. Raw stream and we haven't hit the end of file
- // 2. Delimiter not found and we haven't hit the end of file
- } while ((_isRawStream && (numRead != 0)) ||
- ((content.ToString().IndexOf(actualDelimiter, StringComparison.Ordinal) < 0) && (numRead != 0)));
+ } while (delimiterNotFound && (numRead != 0));
// We've reached the end of file or end of line.
- if (content.Length > 0)
+ if (_currentLineContent.Length > 0)
{
// Add the block read to the ouptut array list, trimming a trailing delimiter, if present.
// Note: If -Tail was specified, we get here in the course of 2 distinct passes:
// - Once while reading backward simply to determine the appropriate *start position* for later forward reading, ignoring the content of the blocks read (in reverse).
// - Then again during forward reading, for regular output processing; it is only then that trimming the delimiter is necessary.
// (Trimming it during backward reading would not only be unnecessary, but could interfere with determining the correct start position.)
- string contentString = content.ToString();
blocks.Add(
- !readBackward && contentString.EndsWith(actualDelimiter, StringComparison.Ordinal) && !_isRawStream
- ? contentString.Substring(0, content.Length - actualDelimiter.Length)
- : contentString
+ !readBackward && !delimiterNotFound
+ ? _currentLineContent.ToString(0, _currentLineContent.Length - actualDelimiter.Length)
+ : _currentLineContent.ToString()
);
}
@@ -683,7 +693,7 @@ namespace Microsoft.PowerShell.Commands
return true;
else
{
- if (readBackward && content.Length > 0)
+ if (readBackward && _currentLineContent.Length > 0)
{
return true;
}
@@ -1259,14 +1269,32 @@ namespace Microsoft.PowerShell.Commands
///
/// Read a specific maximum of characters from the current stream into a buffer
///
- ///
- ///
- ///
+ /// Output buffer.
+ /// Start position to write with.
+ /// Number of bytes to read.
+ /// Return the number of characters read, or -1 if we reach the head of the file
/// Return the number of characters read, or -1 if we reach the head of the file
public override int Read(char[] buffer, int index, int count)
+ {
+ return ReadSpan(new Span(buffer, index, count));
+ }
+
+ ///
+ /// Read characters from the current stream into a Span buffer.
+ ///
+ /// Output buffer.
+ /// Return the number of characters read, or -1 if we reach the head of the file.
+ public override int Read(Span buffer)
+ {
+ return ReadSpan(buffer);
+ }
+
+ private int ReadSpan(Span buffer)
{
// deal with the argument validation
int charRead = 0;
+ int index = 0;
+ int count = buffer.Length;
do
{
@@ -1284,7 +1312,8 @@ namespace Microsoft.PowerShell.Commands
{
buffer[index++] = _charBuff[--_charCount];
}
- } while (count > 0);
+ }
+ while (count > 0);
return charRead;
}