Skip to content

Commit 6de7ea7

Browse files
scheglovCommit Bot
authored and
Commit Bot
committed
Don't remove overlays immediately, wait for the next watch event.
Change-Id: Ie3352bf70010de88874aea122d83a5533a00a1ca Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/235740 Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Jaime Wren <[email protected]> Commit-Queue: Konstantin Shcheglov <[email protected]>
1 parent ec28580 commit 6de7ea7

File tree

4 files changed

+156
-20
lines changed

4 files changed

+156
-20
lines changed

pkg/analysis_server/lib/src/analysis_server.dart

+68-15
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import 'package:analyzer/dart/analysis/results.dart';
4747
import 'package:analyzer/dart/ast/ast.dart';
4848
import 'package:analyzer/exception/exception.dart';
4949
import 'package:analyzer/file_system/file_system.dart';
50+
import 'package:analyzer/file_system/overlay_file_system.dart';
5051
import 'package:analyzer/instrumentation/instrumentation.dart';
5152
import 'package:analyzer/src/dart/analysis/driver.dart' as analysis;
5253
import 'package:analyzer/src/dart/analysis/status.dart' as analysis;
@@ -123,6 +124,34 @@ class AnalysisServer extends AbstractAnalysisServer {
123124
final StreamController _onAnalysisSetChangedController =
124125
StreamController.broadcast(sync: true);
125126

127+
/// Key: a file path for which removing of the overlay was requested.
128+
/// Value: a timer that will remove the overlay, or cancelled.
129+
///
130+
/// This helps for analysis server running remotely, with slow remote file
131+
/// systems, in the following scenario:
132+
/// 1. User edits file, IDE sends "add overlay".
133+
/// 2. User saves file, IDE saves file locally, sends "remove overlay".
134+
/// 3. The remove server reads the file on "remove overlay". But the content
135+
/// of the file on the remove machine is not the same as it is locally,
136+
/// and not what the user looks in the IDE. So, analysis results, such
137+
/// as semantic highlighting, are inconsistent with the local file content.
138+
/// 4. (after a few seconds) The file is synced to the remove machine,
139+
/// the watch event happens, server reads the file, sends analysis
140+
/// results that are consistent with the local file content.
141+
///
142+
/// We try to prevent the inconsistency between moments (3) and (4).
143+
/// It is not wrong, we are still in the eventual consistency, but we
144+
/// want to keep the inconsistency time shorter.
145+
///
146+
/// To do this we keep the last overlay content on "remove overlay",
147+
/// and wait for the next watch event in (4). But there might be race
148+
/// condition, and when it happens, we still want to get to the eventual
149+
/// consistency, so on timer we remove the overlay anyway.
150+
final Map<String, Timer> _pendingFilesToRemoveOverlay = {};
151+
152+
@visibleForTesting
153+
Duration pendingFilesRemoveOverlayDelay = const Duration(seconds: 10);
154+
126155
final DetachableFileSystemManager? detachableFileSystemManager;
127156

128157
/// The broadcast stream of requests that were discarded because there
@@ -231,6 +260,12 @@ class AnalysisServer extends AbstractAnalysisServer {
231260
cancellationTokens[id]?.cancel();
232261
}
233262

263+
Future<void> dispose() async {
264+
for (var timer in _pendingFilesToRemoveOverlay.values) {
265+
timer.cancel();
266+
}
267+
}
268+
234269
/// The socket from which requests are being read has been closed.
235270
void done() {}
236271

@@ -511,7 +546,7 @@ class AnalysisServer extends AbstractAnalysisServer {
511546
} catch (_) {}
512547

513548
// Prepare the new contents.
514-
String? newContents;
549+
String newContents;
515550
if (change is AddContentOverlay) {
516551
newContents = change.content;
517552
} else if (change is ChangeContentOverlay) {
@@ -530,25 +565,28 @@ class AnalysisServer extends AbstractAnalysisServer {
530565
'Invalid overlay change')));
531566
}
532567
} else if (change is RemoveContentOverlay) {
533-
newContents = null;
568+
_pendingFilesToRemoveOverlay[file]?.cancel();
569+
_pendingFilesToRemoveOverlay[file] = Timer(
570+
pendingFilesRemoveOverlayDelay,
571+
() {
572+
resourceProvider.removeOverlay(file);
573+
_changeFileInDrivers(file);
574+
},
575+
);
576+
return;
534577
} else {
535578
// Protocol parsing should have ensured that we never get here.
536579
throw AnalysisException('Illegal change type');
537580
}
538581

539-
if (newContents != null) {
540-
resourceProvider.setOverlay(
541-
file,
542-
content: newContents,
543-
modificationStamp: overlayModificationStamp++,
544-
);
545-
} else {
546-
resourceProvider.removeOverlay(file);
547-
}
582+
_pendingFilesToRemoveOverlay[file]?.cancel();
583+
resourceProvider.setOverlay(
584+
file,
585+
content: newContents,
586+
modificationStamp: overlayModificationStamp++,
587+
);
548588

549-
driverMap.values.forEach((driver) {
550-
driver.changeFile(file);
551-
});
589+
_changeFileInDrivers(file);
552590

553591
// If the file did not exist, and is "overlay only", it still should be
554592
// analyzed. Add it to driver to which it should have been added.
@@ -586,6 +624,12 @@ class AnalysisServer extends AbstractAnalysisServer {
586624
// });
587625
}
588626

627+
void _changeFileInDrivers(String path) {
628+
for (var driver in driverMap.values) {
629+
driver.changeFile(path);
630+
}
631+
}
632+
589633
/// Returns `true` if there is a subscription for the given [service] and
590634
/// [file].
591635
bool _hasAnalysisServiceSubscription(AnalysisService service, String file) {
@@ -671,7 +715,7 @@ class ServerContextManagerCallbacks extends ContextManagerCallbacks {
671715
final AnalysisServer analysisServer;
672716

673717
/// The [ResourceProvider] by which paths are converted into [Resource]s.
674-
final ResourceProvider resourceProvider;
718+
final OverlayResourceProvider resourceProvider;
675719

676720
/// The set of files for which notifications were sent.
677721
final Set<String> filesToFlush = {};
@@ -698,6 +742,15 @@ class ServerContextManagerCallbacks extends ContextManagerCallbacks {
698742

699743
@override
700744
void afterWatchEvent(WatchEvent event) {
745+
var path = event.path;
746+
747+
var pendingTimer = analysisServer._pendingFilesToRemoveOverlay.remove(path);
748+
if (pendingTimer != null) {
749+
pendingTimer.cancel();
750+
resourceProvider.removeOverlay(path);
751+
analysisServer._changeFileInDrivers(path);
752+
}
753+
701754
analysisServer._onAnalysisSetChangedController.add(null);
702755
}
703756

pkg/analysis_server/test/analysis/notification_errors_test.dart

+5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class NotificationErrorsTest extends AbstractAnalysisTest {
4545
void setUp() {
4646
registerLintRules();
4747
super.setUp();
48+
server.pendingFilesRemoveOverlayDelay = const Duration(milliseconds: 10);
4849
server.handlers = [
4950
AnalysisDomainHandler(server),
5051
];
@@ -408,6 +409,10 @@ main() {
408409
brokenFile: RemoveContentOverlay(),
409410
}).toRequest('1'),
410411
);
412+
413+
// Wait for the timer to remove the overlay to fire.
414+
await Future.delayed(server.pendingFilesRemoveOverlayDelay);
415+
411416
await waitForTasksFinished();
412417
await pumpEventQueue(times: 5000);
413418

pkg/analysis_server/test/domain_analysis_test.dart

+78-5
Original file line numberDiff line numberDiff line change
@@ -569,8 +569,8 @@ transforms: []
569569

570570
await setRoots(included: [workspaceRootPath], excluded: []);
571571

572-
// The test file is analyzed, no errors.
573-
await server.onAnalysisComplete;
572+
// The test file (overlay) is analyzed, no errors.
573+
await _waitAnalysisComplete();
574574
_assertAnalyzedFiles(
575575
hasErrors: [],
576576
noErrors: [testFilePathPlatform],
@@ -581,21 +581,84 @@ transforms: []
581581
newFile(testFilePath, content: 'error');
582582

583583
// But the overlay is still present, so the file is not analyzed.
584-
await server.onAnalysisComplete;
584+
await _waitAnalysisComplete();
585585
_assertAnalyzedFiles(
586586
hasErrors: [],
587587
notAnalyzed: [testFilePathPlatform],
588588
);
589589

590-
// Remove the overlay, now the file will be read.
590+
// Ask to remove the overlay, still active, start a timer.
591591
await handleSuccessfulRequest(
592592
AnalysisUpdateContentParams({
593593
testFilePathPlatform: RemoveContentOverlay(),
594594
}).toRequest('0'),
595595
);
596596

597+
// Wait for the timer to remove the overlay to fire.
598+
await Future.delayed(server.pendingFilesRemoveOverlayDelay);
599+
597600
// The file has errors.
598-
await server.onAnalysisComplete;
601+
await _waitAnalysisComplete();
602+
_assertAnalyzedFiles(
603+
hasErrors: [testFilePathPlatform],
604+
noErrors: [],
605+
notAnalyzed: [],
606+
);
607+
}
608+
609+
Future<void>
610+
test_fileSystem_changeFile_hasOverlay_removeOverlay_delayed() async {
611+
// Use long delay, so that it does not happen.
612+
server.pendingFilesRemoveOverlayDelay = const Duration(seconds: 300);
613+
614+
newFile(testFilePath, content: '');
615+
616+
// Add an overlay without errors.
617+
await handleSuccessfulRequest(
618+
AnalysisUpdateContentParams({
619+
testFilePathPlatform: AddContentOverlay(''),
620+
}).toRequest('0'),
621+
);
622+
623+
await setRoots(included: [workspaceRootPath], excluded: []);
624+
625+
// The test file (overlay) is analyzed, no errors.
626+
await _waitAnalysisComplete();
627+
_assertAnalyzedFiles(
628+
hasErrors: [],
629+
noErrors: [testFilePathPlatform],
630+
notAnalyzed: [],
631+
);
632+
633+
// Change the file, has errors.
634+
modifyFile(testFilePath, 'error');
635+
636+
// But the overlay is still present, so the file is not analyzed.
637+
await _waitAnalysisComplete();
638+
_assertAnalyzedFiles(
639+
hasErrors: [],
640+
notAnalyzed: [testFilePathPlatform],
641+
);
642+
643+
// Ask to remove the overlay, still active, start a timer.
644+
await handleSuccessfulRequest(
645+
AnalysisUpdateContentParams({
646+
testFilePathPlatform: RemoveContentOverlay(),
647+
}).toRequest('0'),
648+
);
649+
650+
// Long timer, so still not analyzed.
651+
await _waitAnalysisComplete();
652+
_assertAnalyzedFiles(
653+
hasErrors: [],
654+
notAnalyzed: [testFilePathPlatform],
655+
);
656+
657+
// Change the file again, has errors.
658+
newFile(testFilePath, content: 'error');
659+
660+
// The timer cancelled on the watch event, and the file analyzed.
661+
await _waitAnalysisComplete();
599662
_assertAnalyzedFiles(
600663
hasErrors: [testFilePathPlatform],
601664
noErrors: [],
@@ -1280,6 +1343,16 @@ void f(A a) {}
12801343
),
12811344
);
12821345
}
1346+
1347+
/// Pump the event queue, so that watch events are processed.
1348+
/// Wait for analysis to complete.
1349+
/// Repeat a few times, eventually there will be no work to do.
1350+
Future<void> _waitAnalysisComplete() async {
1351+
for (var i = 0; i < 128; i++) {
1352+
pumpEventQueue();
1353+
await server.onAnalysisComplete;
1354+
}
1355+
}
12831356
}
12841357

12851358
@reflectiveTest

pkg/analysis_server/test/domain_completion_test.dart

+5
Original file line numberDiff line numberDiff line change
@@ -3016,9 +3016,14 @@ class PubPackageAnalysisServerTest with ResourceProviderMixin {
30163016
InstrumentationService.NULL_SERVICE,
30173017
);
30183018

3019+
server.pendingFilesRemoveOverlayDelay = const Duration(milliseconds: 10);
30193020
completionDomain.budgetDuration = const Duration(seconds: 30);
30203021
}
30213022

3023+
Future<void> tearDown() async {
3024+
await server.dispose();
3025+
}
3026+
30223027
void writePackageConfig(Folder root, PackageConfigFileBuilder config) {
30233028
newPackageConfigJsonFile(
30243029
root.path,

0 commit comments

Comments
 (0)