Fix perf issue in LineTrackingStringbuffer.ScanLines.

I noticed several hundred ms spent in this method from a customer profile. Primarilly, the method was doing a linear scan of all lines trying to find one that contained the requested position. I changed this to a binary search, but kept/improved the optimization around checking next/previous lines before instigating the search.

Note, there was also a bug where the old code did:

else if (absoluteIndex > _currentLine.Index && _currentLine.Index + 1 < _lines.Count)

but it should have been coparing absoluteIndex with _currentLine.Start
\n\nCommit migrated from 32a0f28708
This commit is contained in:
Todd Grunke 2020-05-07 17:35:41 -07:00
parent 7eca4ab2ec
commit 069b409dd4
1 changed files with 63 additions and 24 deletions

View File

@ -82,25 +82,51 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{ {
TextLine selected = null; TextLine selected = null;
if (_currentLine != null) if (_currentLine == null)
{
if (_currentLine.Contains(absoluteIndex))
{
// This index is on the last read line
selected = _currentLine;
}
else if (absoluteIndex > _currentLine.Index && _currentLine.Index + 1 < _lines.Count)
{
// This index is ahead of the last read line
selected = ScanLines(absoluteIndex, _currentLine.Index);
}
}
// Have we found a line yet?
if (selected == null)
{ {
// Scan from line 0 // Scan from line 0
selected = ScanLines(absoluteIndex, 0); selected = ScanLines(absoluteIndex, 0, _lines.Count);
}
else if (absoluteIndex >= _currentLine.End)
{
if (_currentLine.Index + 1 < _lines.Count)
{
// This index is after the last read line
var nextLine = _lines[_currentLine.Index + 1];
// Optimization to not search if it's the common case where the line after _currentLine is being requested.
if (nextLine.Contains(absoluteIndex))
{
selected = nextLine;
}
else
{
selected = ScanLines(absoluteIndex, _currentLine.Index, _lines.Count);
}
}
}
else if (absoluteIndex < _currentLine.Start)
{
if (_currentLine.Index > 0)
{
// This index is before the last read line
var prevLine = _lines[_currentLine.Index - 1];
// Optimization to not search if it's the common case where the line before _currentLine is being requested.
if (prevLine.Contains(absoluteIndex))
{
selected = prevLine;
}
else
{
selected = ScanLines(absoluteIndex, 0, _currentLine.Index);
}
}
}
else
{
// This index is on the last read line
selected = _currentLine;
} }
Debug.Assert(selected == null || selected.Contains(absoluteIndex)); Debug.Assert(selected == null || selected.Contains(absoluteIndex));
@ -108,18 +134,31 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return selected; return selected;
} }
private TextLine ScanLines(int absoluteIndex, int startPos) private TextLine ScanLines(int absoluteIndex, int startLineIndex, int endLineIndex)
{ {
for (int i = 0; i < _lines.Count; i++) // binary search for the line containing absoluteIndex
{ var lowIndex = startLineIndex;
var idx = (i + startPos) % _lines.Count; var highIndex = _lines.Count;
Debug.Assert(idx >= 0 && idx < _lines.Count);
if (_lines[idx].Contains(absoluteIndex)) while (lowIndex != highIndex)
{
var midIndex = (lowIndex + highIndex) / 2;
var midLine = _lines[midIndex];
if (absoluteIndex >= midLine.End)
{ {
return _lines[idx]; lowIndex = midIndex + 1;
}
else if (absoluteIndex < midLine.Start)
{
highIndex = midIndex;
}
else
{
return midLine;
} }
} }
return null; return null;
} }