Skip to content

Commit 71aa6ee

Browse files
author
whitema
committed
Reduce allocations when parsing files
The previous implementation used LINQ on a relatively hot path, returned a lazy IEnumerable that was always eagerly converted to a List or Array and allocated two strings per line when Windows linebreaks were used. Profiling indicates that PSES spends a huge percentage of its time in GC, so switching to a slightly more complex implementation that allocates less than half as much seems justified.
1 parent bd400a8 commit 71aa6ee

File tree

3 files changed

+51
-5
lines changed

3 files changed

+51
-5
lines changed

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,7 @@ protected async Task HandleCommentHelpRequest(
10961096
triggerLine1b,
10971097
out helpLocation);
10981098
var result = new CommentHelpRequestResult();
1099+
IList<string> lines = null;
10991100
if (functionDefinitionAst != null)
11001101
{
11011102
var funcExtent = functionDefinitionAst.Extent;
@@ -1104,7 +1105,7 @@ protected async Task HandleCommentHelpRequest(
11041105
{
11051106
// check if the previous character is `<` because it invalidates
11061107
// the param block the follows it.
1107-
var lines = ScriptFile.GetLines(funcText).ToArray();
1108+
lines = ScriptFile.GetLines(funcText);
11081109
var relativeTriggerLine0b = triggerLine1b - funcExtent.StartLineNumber;
11091110
if (relativeTriggerLine0b > 0 && lines[relativeTriggerLine0b].IndexOf("<") > -1)
11101111
{
@@ -1122,8 +1123,12 @@ protected async Task HandleCommentHelpRequest(
11221123
requestParams.BlockComment,
11231124
true,
11241125
helpLocation));
1126+
11251127
var help = analysisResults?.FirstOrDefault()?.Correction?.Edits[0].Text;
1126-
result.Content = help == null ? null : ScriptFile.GetLines(help).ToArray();
1128+
result.Content = help != null
1129+
? (lines ?? ScriptFile.GetLines(funcText)).ToArray()
1130+
: null;
1131+
11271132
if (helpLocation != null &&
11281133
!helpLocation.Equals("before", StringComparison.OrdinalIgnoreCase))
11291134
{

src/PowerShellEditorServices/Workspace/ScriptFile.cs

+29-3
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,40 @@ public ScriptFile(
197197
/// </summary>
198198
/// <param name="text">Input string to be split up into lines.</param>
199199
/// <returns>The lines in the string.</returns>
200-
public static IEnumerable<string> GetLines(string text)
200+
public static IList<string> GetLines(string text)
201201
{
202202
if (text == null)
203203
{
204204
throw new ArgumentNullException(nameof(text));
205205
}
206206

207-
return text.Split('\n').Select(line => line.TrimEnd('\r'));
207+
var list = new List<string>();
208+
int cursor = 0;
209+
210+
while (cursor < text.Length)
211+
{
212+
int nextBreak = text.IndexOf('\n', cursor);
213+
214+
// Last line
215+
if (nextBreak == -1)
216+
{
217+
list.Add(text.Substring(cursor));
218+
break;
219+
}
220+
221+
if (nextBreak > cursor && text[nextBreak - 1] == '\r')
222+
{
223+
list.Add(text.Substring(cursor, nextBreak - cursor - 1));
224+
}
225+
else
226+
{
227+
list.Add(text.Substring(cursor, nextBreak - cursor));
228+
}
229+
230+
cursor = nextBreak + 1;
231+
}
232+
233+
return list;
208234
}
209235

210236
/// <summary>
@@ -517,7 +543,7 @@ private void SetFileContents(string fileContents)
517543
{
518544
// Split the file contents into lines and trim
519545
// any carriage returns from the strings.
520-
this.FileLines = GetLines(fileContents).ToList();
546+
this.FileLines = GetLines(fileContents);
521547

522548
// Parse the contents to get syntax tree and errors
523549
this.ParseFileContents();

test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs

+15
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ public class ScriptFileGetLinesTests
213213
new string[] { "\r\n" },
214214
StringSplitOptions.None);
215215

216+
private const string TestStringUnix = "Line One\nLine Two\nLine Three\nLine Four\nLine Five";
217+
216218
public ScriptFileGetLinesTests()
217219
{
218220
this.scriptFile =
@@ -296,6 +298,19 @@ public void CanGetRangeAtLineBoundaries()
296298

297299
Assert.Equal(expectedLines, lines);
298300
}
301+
302+
[Fact]
303+
public void CanSplitLines()
304+
{
305+
Assert.Equal(TestStringLines, scriptFile.FileLines);
306+
}
307+
308+
[Fact]
309+
public void CanGetSameLinesWithUnixLineBreaks()
310+
{
311+
var unixFile = ScriptFileChangeTests.CreateScriptFile(TestStringUnix);
312+
Assert.Equal(scriptFile.FileLines, unixFile.FileLines);
313+
}
299314
}
300315

301316
public class ScriptFilePositionTests

0 commit comments

Comments
 (0)