Skip to content

Commit 5f75bb8

Browse files
bwilkersonCommit Queue
authored andcommitted
Replace change builder copying with a transactional model
Copying change builders in order to enable reverting changes from a single correction producer when there's an exception is expensive. This CL replaces that implementation with a transactional model. Clients are not required to start a transaction (that happens automatically), but are required to signal the end of a transaction by invoking either `commit` or `revert`. (Actually, `commit` is assumed if neither method is invoked before computing the `SourceChange`.) There is some information in the Dart file edit builder related to imports that isn't correctly handled. The reason for this is that too much of the import computation is done up-front. I don't think this will be a problem in practice (or at least not often), but we should come back at some point to change the implementation so that we retain abstract data longer and perform more processing at the very end (where we'll have complete information and probably be able to do a better job anyway). This CL improved the performance of the benchmark that adds and applies fixes for 10,000 lint violations. Before it took an average of 11216.0 ms. After it took an average of 4200.2 ms. Which is about a 62% improvement. Change-Id: I12710606455a04e34a82308ede5d9fba1c68b972 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/428060 Reviewed-by: Samuel Rawlins <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]>
1 parent e6784e0 commit 5f75bb8

File tree

8 files changed

+372
-11
lines changed

8 files changed

+372
-11
lines changed

pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,8 @@ class BulkFixProcessor {
334334
) => _organizeDirectives(contexts);
335335

336336
Future<void> _applyProducer(CorrectionProducer producer) async {
337+
var localBuilder = builder as ChangeBuilderImpl;
337338
try {
338-
var localBuilder = builder.copy() as ChangeBuilderImpl;
339-
340339
// Set a description of the change for this fix for the duration of
341340
// computer which will be passed down to the individual changes.
342341
localBuilder.currentChangeDescription = producer.fixKind?.message;
@@ -348,11 +347,11 @@ class BulkFixProcessor {
348347
'computation. $producer changed from $fixKind to ${producer.fixKind}.',
349348
);
350349
localBuilder.currentChangeDescription = null;
351-
352-
builder = localBuilder;
350+
localBuilder.commit();
353351
} on ConflictingEditException {
354-
// If a conflicting edit was added in [compute], then the [localBuilder]
355-
// is discarded and we revert to the previous state of the builder.
352+
// If a conflicting edit was added in [compute], then the builder is
353+
// reverted to its previous state.
354+
localBuilder.revert();
356355
}
357356
}
358357

pkg/analysis_server_plugin/lib/src/correction/fix_in_file_processor.dart

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,10 @@ final class FixInFileProcessor {
126126

127127
var producer = generator(context: context);
128128

129+
var builder = fixState.builder as ChangeBuilderImpl;
129130
try {
130-
var localBuilder = fixState.builder.copy();
131131
var fixKind = producer.fixKind;
132-
await producer.compute(localBuilder);
132+
await producer.compute(builder);
133133
assert(
134134
!producer.canBeAppliedAcrossSingleFile || producer.fixKind == fixKind,
135135
'Producers used in bulk fixes must not modify the FixKind during '
@@ -138,19 +138,22 @@ final class FixInFileProcessor {
138138

139139
var multiFixKind = producer.multiFixKind;
140140
if (multiFixKind == null) {
141+
builder.revert();
141142
return fixState;
142143
}
143144

144145
// TODO(pq): consider discarding the change if the producer's `fixKind`
145146
// doesn't match a previously cached one.
147+
builder.commit();
146148
return _NotEmptyFixState(
147-
builder: localBuilder,
149+
builder: builder,
148150
fixKind: multiFixKind,
149151
fixCount: fixState.fixCount + 1,
150152
);
151153
} on ConflictingEditException {
152-
// If a conflicting edit was added in [compute], then the [localBuilder]
153-
// is discarded and we revert to the previous state of the builder.
154+
// If a conflicting edit was added in [compute], then the builder is
155+
// reverted to its previous state.
156+
builder.revert();
154157
return fixState;
155158
}
156159
}

pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ class ChangeBuilderImpl implements ChangeBuilder {
7171
/// by a given correction producer.
7272
int modificationCount = 0;
7373

74+
/// The data used to revert any changes made since the last time [commit] was
75+
/// called.
76+
final _ChangeBuilderRevertData _revertData = _ChangeBuilderRevertData();
77+
7478
/// Initialize a newly created change builder. If the builder will be used to
7579
/// create changes for Dart files, then either a [session] or a [workspace]
7680
/// must be provided (but not both).
@@ -154,6 +158,7 @@ class ChangeBuilderImpl implements ChangeBuilder {
154158
"Can't add multiple edits concurrently for the same file");
155159
}
156160
_dartFileEditBuilders[path] = builder;
161+
_revertData._addedDartFileEditBuilders.add(path);
157162
}
158163
}
159164
if (builder != null) {
@@ -190,6 +195,7 @@ class ChangeBuilderImpl implements ChangeBuilder {
190195
if (builder == null) {
191196
builder = FileEditBuilderImpl(this, path, 0);
192197
_genericFileEditBuilders[path] = builder;
198+
_revertData._addedGenericFileEditBuilders.add(path);
193199
}
194200
builder.currentChangeDescription = currentChangeDescription;
195201
buildFileEdit(builder);
@@ -217,11 +223,43 @@ class ChangeBuilderImpl implements ChangeBuilder {
217223
recover: true),
218224
0);
219225
_yamlFileEditBuilders[path] = builder;
226+
_revertData._addedYamlFileEditBuilders.add(path);
220227
}
221228
builder.currentChangeDescription = currentChangeDescription;
222229
buildFileEdit(builder);
223230
}
224231

232+
/// Commit the changes that have been made up to this point.
233+
void commit() {
234+
// Capture the current values of simple values.
235+
_revertData._selection = _selection;
236+
_revertData._selectionRange = _selectionRange;
237+
_revertData._currentChangeDescription = currentChangeDescription;
238+
_revertData._modificationCount = modificationCount;
239+
240+
// Discard any information about changes to maps.
241+
_revertData._addedLinkedEditGroups.clear();
242+
_revertData._addedLinkedEditGroupPositions.clear();
243+
_revertData._addedLinkedEditGroupSuggestions.clear();
244+
245+
_revertData._addedGenericFileEditBuilders.clear();
246+
_revertData._addedDartFileEditBuilders.clear();
247+
_revertData._addedYamlFileEditBuilders.clear();
248+
249+
// Commit the changes in any pre-existing builders.
250+
for (var builder in _genericFileEditBuilders.values) {
251+
builder.commit();
252+
}
253+
for (var builder in _dartFileEditBuilders.values) {
254+
builder.commit();
255+
}
256+
for (var builder in _yamlFileEditBuilders.values) {
257+
builder.commit();
258+
}
259+
}
260+
261+
@Deprecated('Copying change builders is expensive. Internal users of this '
262+
'method now use `commit` and `revert` instead.')
225263
@override
226264
ChangeBuilder copy() {
227265
var copy = ChangeBuilderImpl(workspace: workspace, eol: eol);
@@ -273,6 +311,7 @@ class ChangeBuilderImpl implements ChangeBuilder {
273311
if (group == null) {
274312
group = LinkedEditGroup.empty();
275313
_linkedEditGroups[groupName] = group;
314+
_revertData._addedLinkedEditGroups[groupName] = group;
276315
}
277316
return group;
278317
}
@@ -284,6 +323,65 @@ class ChangeBuilderImpl implements ChangeBuilder {
284323
_yamlFileEditBuilders.containsKey(path);
285324
}
286325

326+
/// Revert any changes made since the last time [commit] was called.
327+
void revert() {
328+
// Set simple values back to their previous values.
329+
_selection = _revertData._selection;
330+
_selectionRange = _revertData._selectionRange;
331+
currentChangeDescription = _revertData._currentChangeDescription;
332+
modificationCount = _revertData._modificationCount;
333+
334+
// Remove any linked edit groups that have been added.
335+
for (var entry in _revertData._addedLinkedEditGroups.entries) {
336+
_linkedEditGroups.remove(entry.key);
337+
}
338+
// Remove any positions or suggestions that were added to pre-existing
339+
// link edit groups.
340+
//
341+
// Note that this code assumes that the lengths of the groups have not been
342+
// changed. It's invalid to change the length, but there's no code to
343+
// validate that it hasn't been changed.
344+
for (var entry in _revertData._addedLinkedEditGroupPositions.entries) {
345+
for (var position in entry.value) {
346+
entry.key.positions.remove(position);
347+
}
348+
}
349+
for (var entry in _revertData._addedLinkedEditGroupSuggestions.entries) {
350+
for (var suggestion in entry.value) {
351+
entry.key.suggestions.remove(suggestion);
352+
}
353+
}
354+
// Discard any data about linked edit groups because it's no longer needed.
355+
_revertData._addedLinkedEditGroups.clear();
356+
_revertData._addedLinkedEditGroupPositions.clear();
357+
_revertData._addedLinkedEditGroupSuggestions.clear();
358+
359+
// Remove any file edit builders that have been added.
360+
for (var path in _revertData._addedGenericFileEditBuilders) {
361+
_genericFileEditBuilders.remove(path);
362+
}
363+
for (var path in _revertData._addedDartFileEditBuilders) {
364+
_dartFileEditBuilders.remove(path);
365+
}
366+
for (var path in _revertData._addedYamlFileEditBuilders) {
367+
_yamlFileEditBuilders.remove(path);
368+
}
369+
// Revert the data changes in any pre-existing builders.
370+
for (var builder in _genericFileEditBuilders.values) {
371+
builder.revert();
372+
}
373+
for (var builder in _dartFileEditBuilders.values) {
374+
builder.revert();
375+
}
376+
for (var builder in _yamlFileEditBuilders.values) {
377+
builder.revert();
378+
}
379+
// Discard any data about file builders because it's no longer needed.
380+
_revertData._addedGenericFileEditBuilders.clear();
381+
_revertData._addedDartFileEditBuilders.clear();
382+
_revertData._addedYamlFileEditBuilders.clear();
383+
}
384+
287385
@override
288386
void setSelection(Position position) {
289387
_selection = position;
@@ -435,8 +533,15 @@ class EditBuilderImpl implements EditBuilder {
435533
fileEditBuilder.changeBuilder._lockedPositions.add(position);
436534
var group = fileEditBuilder.changeBuilder.getLinkedEditGroup(groupName);
437535
group.addPosition(position, length);
536+
var revertData = fileEditBuilder.changeBuilder._revertData;
537+
revertData._addedLinkedEditGroupPositions
538+
.putIfAbsent(group, () => [])
539+
.add(position);
438540
for (var suggestion in builder.suggestions) {
439541
group.addSuggestion(suggestion);
542+
revertData._addedLinkedEditGroupSuggestions
543+
.putIfAbsent(group, () => [])
544+
.add(suggestion);
440545
}
441546
}
442547
}
@@ -508,6 +613,8 @@ class FileEditBuilderImpl implements FileEditBuilder {
508613
/// single description.
509614
String? currentChangeDescription;
510615

616+
final _FileEditBuilderRevertData _revertData = _FileEditBuilderRevertData();
617+
511618
/// Initialize a newly created builder to build a source file edit within the
512619
/// change being built by the given [changeBuilder]. The file being edited has
513620
/// the given absolute [path] and [timeStamp].
@@ -542,6 +649,10 @@ class FileEditBuilderImpl implements FileEditBuilder {
542649
var position =
543650
Position(fileEdit.file, range.offset + _deltaToOffset(range.offset));
544651
group.addPosition(position, range.length);
652+
var revertData = changeBuilder._revertData;
653+
revertData._addedLinkedEditGroupPositions
654+
.putIfAbsent(group, () => [])
655+
.add(position);
545656
}
546657

547658
@override
@@ -575,6 +686,15 @@ class FileEditBuilderImpl implements FileEditBuilder {
575686
}
576687
}
577688

689+
/// Commit the changes that have been made up to this point.
690+
void commit() {
691+
_revertData._currentChangeDescription = currentChangeDescription;
692+
693+
_revertData._addedEdits.clear();
694+
}
695+
696+
@Deprecated('Copying change builders is expensive. Internal users of this '
697+
'method now use `commit` and `revert` instead.')
578698
FileEditBuilderImpl copyWith(ChangeBuilderImpl changeBuilder) {
579699
var copy =
580700
FileEditBuilderImpl(changeBuilder, fileEdit.file, fileEdit.fileStamp);
@@ -594,8 +714,16 @@ class FileEditBuilderImpl implements FileEditBuilder {
594714
}
595715

596716
/// Replace edits in the [range] with the given [edit].
717+
///
597718
/// The [range] is relative to the original code.
598719
void replaceEdits(SourceRange range, SourceEdit edit) {
720+
// This does not record the edits that are being replaced, so we cannot
721+
// correctly revert the current transaction.
722+
//
723+
// I think this is ok because this method is only called from
724+
// `DartFileEditBuilder.format`, which isn't called from the bulk fix
725+
// processor. But we will need to fix this if we start using it in a context
726+
// where we do want to be able to revert such changes.
599727
fileEdit.edits.removeWhere((edit) {
600728
if (range.contains(edit.offset)) {
601729
if (!range.contains(edit.end)) {
@@ -611,10 +739,18 @@ class FileEditBuilderImpl implements FileEditBuilder {
611739
_addEdit(edit);
612740
}
613741

742+
/// Revert any changes made since the last time [commit] was called.
743+
void revert() {
744+
currentChangeDescription = _revertData._currentChangeDescription;
745+
746+
fileEdit.edits.removeWhere(_revertData._addedEdits.contains);
747+
}
748+
614749
/// Add the edit from the given [edit] to the edits associated with the
615750
/// current file.
616751
void _addEdit(SourceEdit edit, {bool insertBeforeExisting = false}) {
617752
fileEdit.add(edit, insertBeforeExisting: insertBeforeExisting);
753+
_revertData._addedEdits.add(edit);
618754
var delta = _editDelta(edit);
619755
changeBuilder._updatePositions(edit.offset, delta);
620756
changeBuilder._lockedPositions.clear();
@@ -710,6 +846,75 @@ class LinkedEditBuilderImpl implements LinkedEditBuilder {
710846
}
711847
}
712848

849+
/// The data used to revert any changes made to a [ChangeBuilder] since the last
850+
/// time [commit] was called.
851+
class _ChangeBuilderRevertData {
852+
/// The last committed value of the change builder's `_selection`.
853+
Position? _selection;
854+
855+
/// The last committed value of the change builder's `_selectionRange`.
856+
SourceRange? _selectionRange;
857+
858+
/// The last committed value of the change builder's
859+
/// `currentChangeDescription`.
860+
String? _currentChangeDescription;
861+
862+
/// The last committed value of the change builder's `modificationCount`.
863+
int _modificationCount = 0;
864+
865+
/// A map from the names of linked edit groups to the linked edit groups that
866+
/// have been added since the last commit.
867+
final Map<String, LinkedEditGroup> _addedLinkedEditGroups = {};
868+
869+
/// A map from pre-existing linked edit groups to the positions that were
870+
/// added to the group.
871+
final Map<LinkedEditGroup, List<Position>> _addedLinkedEditGroupPositions =
872+
{};
873+
874+
/// A map from pre-existing linked edit groups to the suggestions that were
875+
/// added to the group.
876+
final Map<LinkedEditGroup, List<LinkedEditSuggestion>>
877+
_addedLinkedEditGroupSuggestions = {};
878+
879+
/// A map of absolute normalized path to generic file edit builders that have
880+
/// been added since the last commit.
881+
final Set<String> _addedGenericFileEditBuilders = {};
882+
883+
/// A map of absolute normalized path to Dart file edit builders that have
884+
/// been added since the last commit.
885+
final Set<String> _addedDartFileEditBuilders = {};
886+
887+
/// A map of absolute normalized path to YAML file edit builders that have
888+
/// been added since the last commit.
889+
final Set<String> _addedYamlFileEditBuilders = {};
890+
}
891+
892+
/// The data used to revert any changes made to a [FileEditBuilder] since the
893+
/// last time [commit] was called.
894+
class _FileEditBuilderRevertData {
895+
/// The last committed value of the change builder's
896+
/// `currentChangeDescription`.
897+
String? _currentChangeDescription;
898+
899+
// This needs to be an identify set because it is possible for the data-driven
900+
// fixes support to produce the same set of (multiple) edits multiple times.
901+
// More investigation is required to determine whether that behavior is a bug
902+
// in the data-driven fixes support or whether this is (or can be) caused by
903+
// the data being used by it. If it is a bug that can be fixed, then we can
904+
// safely use a non-identiy set, though leaving it won't cause any problems.
905+
//
906+
// In either case, when that happens, the second set of edits will usually
907+
// conflict with the previous set of edits. Because the edits are the same,
908+
// they will return `true` from `==`, causing some of the edits to not be
909+
// recorded in this set. When the conflict is detected and an attempt is made
910+
// to revert the changes, only the one edit in this set will be removed from
911+
// the builder's list, making it impossible to correctly revert the change.
912+
//
913+
// The other option would be to make this a list, but doing so would decrease
914+
// the performance of `revert`.
915+
final Set<SourceEdit> _addedEdits = HashSet.identity();
916+
}
917+
713918
/// Workspace that wraps a single [AnalysisSession].
714919
class _SingleSessionWorkspace extends ChangeWorkspace {
715920
final AnalysisSession session;

0 commit comments

Comments
 (0)