Skip to content

Commit 0f48263

Browse files
DanTupCommit Queue
authored andcommitted
[analysis_server] Improve the performance of applying LSP edits in tests
This slightly improves the performance of applying edits in LSP tests. The previous code would sort the edits in reverse and then sequentially replace each edit into a string. With a large number of edits, both the string replacement and the subsequent rebuilding of LineInfo after each change could be quite slow. With this change, we instead work through the edits forwards, appending the original text + new text into a StringBuffer to be combined once at the end. Since most tests don't make large numbers of edits this only shaved a few seconds off the whole server test run, however when running a small benchmark of 20000 edits that Brian sent me recently, the time taken comes down from 42s (which hit the default test timeout) to 17s (which is not fast, but is faster). Change-Id: Ia91937f4912b35ee1fe29f31157db3ef0e4bd79e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/429960 Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Samuel Rawlins <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]>
1 parent fb03a18 commit 0f48263

File tree

3 files changed

+55
-42
lines changed

3 files changed

+55
-42
lines changed

pkg/analysis_server/test/lsp/change_verifier.dart

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -171,19 +171,12 @@ class LspChangeVerifier {
171171
}
172172

173173
String _applyTextDocumentEditEdit(String content, TextDocumentEdit edit) {
174-
// To simulate the behaviour we'll get from an LSP client, apply edits from
175-
// the latest offset to the earliest, but with items at the same offset
176-
// being reversed so that when applied sequentially they appear in the
177-
// document in-order.
178-
//
179-
// This is essentially a stable sort over the offset (descending), but since
180-
// List.sort() is not stable so we additionally sort by index).
181-
var indexedEdits =
182-
edit.edits.mapIndexed(TextEditWithIndex.fromUnion).toList();
183-
indexedEdits.sort(TextEditWithIndex.compare);
184-
return indexedEdits
185-
.map((e) => e.edit)
186-
.fold(content, editHelpers.applyTextEdit);
174+
// Extract the edits from the union (they all have the same superclass).
175+
var edits =
176+
edit.edits
177+
.map((edit) => edit.map((e) => e, (e) => e, (e) => e))
178+
.toList();
179+
return _applyTextEdits(content, edits);
187180
}
188181

189182
String _applyTextEdits(String content, List<TextEdit> changes) =>
@@ -273,8 +266,8 @@ class LspChangeVerifier {
273266
}
274267
}
275268

276-
/// An LSP TextEdit with its index, and a comparer to sort them in a way that
277-
/// can be applied sequentially while preserving expected behaviour.
269+
/// An LSP TextEdit with its index, and comparers to stably sort them by source
270+
/// position (forwards).
278271
class TextEditWithIndex {
279272
final int index;
280273
final TextEdit edit;
@@ -286,9 +279,13 @@ class TextEditWithIndex {
286279
Either3<AnnotatedTextEdit, SnippetTextEdit, TextEdit> edit,
287280
) : edit = edit.map((e) => e, (e) => e, (e) => e);
288281

289-
/// Compares two [TextEditWithIndex] to sort them by the order in which they
290-
/// can be sequentially applied to a String to match the behaviour of an LSP
291-
/// client.
282+
/// Compares two [TextEditWithIndex] to sort them stably in source-order.
283+
///
284+
/// In this order, edits cannot be applied sequentially to a file because
285+
/// each edit may change the location of future edits. This can be used to
286+
/// apply them if all locations are computed against the original code using
287+
/// a [StringBuffer] for better performance than repeatedly applying
288+
/// sequentially.
292289
static int compare(TextEditWithIndex edit1, TextEditWithIndex edit2) {
293290
var end1 = edit1.edit.range.end;
294291
var end2 = edit2.edit.range.end;
@@ -297,11 +294,11 @@ class TextEditWithIndex {
297294
// https://github.com/microsoft/vscode/blob/856a306d1a9b0879727421daf21a8059e671e3ea/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts#L475
298295

299296
if (end1.line != end2.line) {
300-
return end1.line.compareTo(end2.line) * -1;
297+
return end1.line.compareTo(end2.line);
301298
} else if (end1.character != end2.character) {
302-
return end1.character.compareTo(end2.character) * -1;
299+
return end1.character.compareTo(end2.character);
303300
} else {
304-
return edit1.index.compareTo(edit2.index) * -1;
301+
return edit1.index.compareTo(edit2.index);
305302
}
306303
}
307304
}

pkg/analysis_server/test/lsp/request_helpers_mixin.dart

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,6 @@ import 'server_abstract.dart';
2424

2525
/// A mixin with helpers for applying LSP edits to strings.
2626
mixin LspEditHelpersMixin {
27-
String applyTextEdit(String content, TextEdit edit) {
28-
var startPos = edit.range.start;
29-
var endPos = edit.range.end;
30-
var lineInfo = LineInfo.fromContent(content);
31-
var start = lineInfo.getOffsetOfLine(startPos.line) + startPos.character;
32-
var end = lineInfo.getOffsetOfLine(endPos.line) + endPos.character;
33-
return content.replaceRange(start, end, edit.newText);
34-
}
35-
3627
String applyTextEdits(String content, List<TextEdit> changes) {
3728
// Complex text manipulations are described with an array of TextEdit's,
3829
// representing a single change to the document.
@@ -48,22 +39,48 @@ mixin LspEditHelpersMixin {
4839

4940
/// Ensures changes are simple enough to apply easily without any complicated
5041
/// logic.
51-
void validateChangesCanBeApplied() {
52-
for (var change1 in changes) {
53-
for (var change2 in changes) {
54-
if (change1 != change2 && change1.range.intersects(change2.range)) {
55-
throw 'Test helper applyTextEdits does not support applying multiple edits '
56-
'where the edits are not in reverse order.';
57-
}
42+
for (var change1 in changes) {
43+
for (var change2 in changes) {
44+
if (change1 != change2 && change1.range.intersects(change2.range)) {
45+
throw 'Test helper applyTextEdits does not support applying multiple edits '
46+
'where the edits are not in reverse order.';
5847
}
5948
}
6049
}
6150

62-
validateChangesCanBeApplied();
63-
6451
var indexedEdits = changes.mapIndexed(TextEditWithIndex.new).toList();
6552
indexedEdits.sort(TextEditWithIndex.compare);
66-
return indexedEdits.map((e) => e.edit).fold(content, applyTextEdit);
53+
54+
var lineInfo = LineInfo.fromContent(content);
55+
var buffer = StringBuffer();
56+
var currentOffset = 0;
57+
58+
// Build a new string by appending parts of the original string and then
59+
// the new text from each edit into a buffer. This is faster for multiple
60+
// edits than sorting in reverse and repeatedly applying sequentially as it
61+
// cuts out a lot of (potentially large) intermediate strings.
62+
for (var edit in indexedEdits.map((e) => e.edit)) {
63+
var startPos = edit.range.start;
64+
var endPos = edit.range.end;
65+
var start = lineInfo.getOffsetOfLine(startPos.line) + startPos.character;
66+
var end = lineInfo.getOffsetOfLine(endPos.line) + endPos.character;
67+
68+
// Append any text between the current position and the start of this edit
69+
if (start != currentOffset) {
70+
buffer.write(content.substring(currentOffset, start));
71+
}
72+
// Append the replacement text
73+
buffer.write(edit.newText);
74+
// Move the current position to the end of this edit
75+
currentOffset = end;
76+
}
77+
78+
// Finally, add any remainder from after the last edit.
79+
if (currentOffset != content.length) {
80+
buffer.write(content.substring(currentOffset));
81+
}
82+
83+
return buffer.toString();
6784
}
6885

6986
/// Returns the text for [range] in [content].

pkg/analysis_server/test/lsp_over_legacy/format_test.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import 'package:test/test.dart';
66
import 'package:test_reflective_loader/test_reflective_loader.dart';
77

8-
import '../lsp/request_helpers_mixin.dart';
98
import 'abstract_lsp_over_legacy.dart';
109

1110
void main() {
@@ -15,7 +14,7 @@ void main() {
1514
}
1615

1716
@reflectiveTest
18-
class FormatTest extends LspOverLegacyTest with LspEditHelpersMixin {
17+
class FormatTest extends LspOverLegacyTest {
1918
Future<void> test_format() async {
2019
const content = 'void main() {}';
2120
const expectedContent = 'void main() {}';

0 commit comments

Comments
 (0)