[analysis_server] Exclude instrumentation log file from logged watch events to prevent loops

Fixes https://github.com/dart-lang/sdk/issues/48885.

Change-Id: Idebfd9d4f2867b3053c0ece49b300926956fab2a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/275061
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/server/driver.dart b/pkg/analysis_server/lib/src/server/driver.dart
index 767ee6f..343ab32 100644
--- a/pkg/analysis_server/lib/src/server/driver.dart
+++ b/pkg/analysis_server/lib/src/server/driver.dart
@@ -274,8 +274,9 @@
         : [this.instrumentationService!];
     if (logFilePath != null) {
       _rollLogFiles(logFilePath, 5);
-      allInstrumentationServices.add(
-          InstrumentationLogAdapter(FileInstrumentationLogger(logFilePath)));
+      allInstrumentationServices.add(InstrumentationLogAdapter(
+          FileInstrumentationLogger(logFilePath),
+          watchEventExclusionFiles: {logFilePath}));
     }
 
     var errorNotifier = ErrorNotifier();
diff --git a/pkg/analyzer/lib/instrumentation/log_adapter.dart b/pkg/analyzer/lib/instrumentation/log_adapter.dart
index cd1393c..19b73d4 100644
--- a/pkg/analyzer/lib/instrumentation/log_adapter.dart
+++ b/pkg/analyzer/lib/instrumentation/log_adapter.dart
@@ -27,9 +27,19 @@
   /// A logger used to log instrumentation in string format.
   final InstrumentationLogger _instrumentationLogger;
 
+  /// Files that should not have their watch events logged (to prevent feedback
+  /// loops).
+  final Set<String>? _watchEventExclusionFiles;
+
   /// Initialize a newly created instrumentation service to communicate with the
   /// given [_instrumentationLogger].
-  InstrumentationLogAdapter(this._instrumentationLogger);
+  ///
+  /// Any file paths in [watchEventExclusionFiles] will be excluded from the
+  /// logging of watch events (to prevent feedback loops).
+  InstrumentationLogAdapter(
+    this._instrumentationLogger, {
+    Set<String>? watchEventExclusionFiles,
+  }) : _watchEventExclusionFiles = watchEventExclusionFiles;
 
   /// The current time, expressed as a decimal encoded number of milliseconds.
   String get _timestamp => DateTime.now().millisecondsSinceEpoch.toString();
@@ -142,6 +152,10 @@
 
   @override
   void logWatchEvent(String folderPath, String filePath, String changeType) {
+    if (_watchEventExclusionFiles?.contains(filePath) ?? false) {
+      return;
+    }
+
     _instrumentationLogger
         .log(_join([TAG_WATCH_EVENT, folderPath, filePath, changeType]));
   }
diff --git a/pkg/analyzer/test/instrumentation/instrumentation_test.dart b/pkg/analyzer/test/instrumentation/instrumentation_test.dart
index 8ed202b..077ab94 100644
--- a/pkg/analyzer/test/instrumentation/instrumentation_test.dart
+++ b/pkg/analyzer/test/instrumentation/instrumentation_test.dart
@@ -13,6 +13,11 @@
 
 @reflectiveTest
 class InstrumentationServiceTest {
+  void assertNoLogs(TestInstrumentationLogger logger) {
+    String sent = logger.logged.toString();
+    expect(sent, isEmpty);
+  }
+
   void assertNormal(
       TestInstrumentationLogger logger, String tag, String message) {
     String sent = logger.logged.toString();
@@ -171,6 +176,37 @@
         endsWith(
             ':myUuid:someClientId:someClientVersion:aServerVersion:anSdkVersion\n'));
   }
+
+  void test_logWatch() {
+    TestInstrumentationLogger logger = TestInstrumentationLogger();
+    InstrumentationService service = InstrumentationLogAdapter(logger);
+    service.logWatchEvent('/folder', '/folder/file.txt', 'modify');
+    assertNormal(
+      logger,
+      InstrumentationLogAdapter.TAG_WATCH_EVENT,
+      '/folder:/folder/file.txt:modify',
+    );
+  }
+
+  void test_logWatch_exclusions_excluded() {
+    TestInstrumentationLogger logger = TestInstrumentationLogger();
+    InstrumentationService service = InstrumentationLogAdapter(logger,
+        watchEventExclusionFiles: {'/folder/excluded.txt'});
+    service.logWatchEvent('/folder', '/folder/excluded.txt', 'modify');
+    assertNoLogs(logger);
+  }
+
+  void test_logWatch_exclusions_notExcluded() {
+    TestInstrumentationLogger logger = TestInstrumentationLogger();
+    InstrumentationService service = InstrumentationLogAdapter(logger,
+        watchEventExclusionFiles: {'/folder/excluded.txt'});
+    service.logWatchEvent('/folder', '/folder/file.txt', 'modify');
+    assertNormal(
+      logger,
+      InstrumentationLogAdapter.TAG_WATCH_EVENT,
+      '/folder:/folder/file.txt:modify',
+    );
+  }
 }
 
 @reflectiveTest