Reland "[VM/Service] Record timeline events representing completed microtasks"

This is a reland of commit 22021cc12014feffd33f9a3d50e37447a980b99b

I showed some excerpts of Golem results to @mraleph for varying targets
and constant CPU, patch A, and patch B. Those constants being:
  CPU: Intel Xeon
  patch A: revision=115869&patch=20143
  patch B: revision=115876
(example results:
https://golem.corp.goog/Comparison?repository=dart#targetA%3Ddart%3BmachineTypeA%3Dlinux-x64%3BrevisionA%3D115869%3BpatchA%3Dderekx-Reland---VM%2FService--Record-timeline-events-representing-completed-microtasks--4%3BtargetB%3Ddart%3BmachineTypeB%3Dlinux-x64%3BrevisionB%3D115876%3BpatchB%3DNone)

He said that this change could be relanded as long as the following
"RunTime as Score" regressions of the "dart" target were noted in the
commit message:
  - StreamSingleSubTest: -3.907% (-0.8 noise)
  - ScheduleMicrotaskTest: -12.67% (-0.6 noise)

TEST=pkg/vm_service/test/timeline_events_for_completed_microtasks_test

Original change's description:
> [VM/Service] Record timeline events representing completed microtasks
>
> TEST=pkg/vm_service/test/timeline_events_for_completed_microtasks_test
>
> CoreLibraryReviewExempt: This CL does not include any core library API
> changes, it only modifies the implementation of microtasks (by
> instrumenting them).
> Change-Id: I54d886db9519c73f9e3218a9cc1c46bc9fe9acc3
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/420221
> Commit-Queue: Derek Xu <derekx@google.com>
> Reviewed-by: Ben Konyi <bkonyi@google.com>

CoreLibraryReviewExempt: This CL does not include any core library API
changes, it only modifies the implementation of microtasks (by
instrumenting them).
Change-Id: Ia7741a9bb9ab948e8d5fff1cf9abe9915e247d81
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/423921
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Derek Xu <derekx@google.com>
diff --git a/pkg/vm_service/test/common/service_test_common.dart b/pkg/vm_service/test/common/service_test_common.dart
index ae04eb2..fc208ac 100644
--- a/pkg/vm_service/test/common/service_test_common.dart
+++ b/pkg/vm_service/test/common/service_test_common.dart
@@ -5,11 +5,13 @@
 library service_test_common;
 
 import 'dart:async';
+import 'dart:collection' show HashMap;
 import 'dart:typed_data';
 
 import 'package:path/path.dart' as p;
 import 'package:test/test.dart';
 import 'package:vm_service/vm_service.dart';
+import 'package:vm_service_protos/vm_service_protos.dart' show DebugAnnotation;
 
 typedef IsolateTest = Future<void> Function(
   VmService service,
@@ -914,3 +916,19 @@
     }
   };
 }
+
+Map<String, String> mapFromListOfDebugAnnotations(
+  List<DebugAnnotation> debugAnnotations,
+) {
+  return HashMap.fromEntries(
+    debugAnnotations.map((a) {
+      if (a.hasStringValue()) {
+        return MapEntry(a.name, a.stringValue);
+      } else if (a.hasLegacyJsonValue()) {
+        return MapEntry(a.name, a.legacyJsonValue);
+      } else {
+        throw 'We should not be writing annotations without values';
+      }
+    }),
+  );
+}
diff --git a/pkg/vm_service/test/get_perfetto_vm_timeline_rpc_test.dart b/pkg/vm_service/test/get_perfetto_vm_timeline_rpc_test.dart
index dcd2c7b..850b649 100644
--- a/pkg/vm_service/test/get_perfetto_vm_timeline_rpc_test.dart
+++ b/pkg/vm_service/test/get_perfetto_vm_timeline_rpc_test.dart
@@ -5,7 +5,6 @@
 // VMOptions=
 // VMOptions=--intern_strings_when_writing_perfetto_timeline
 
-import 'dart:collection';
 import 'dart:convert';
 import 'dart:developer';
 import 'dart:io' show Platform;
@@ -14,6 +13,7 @@
 import 'package:vm_service/vm_service.dart' hide Timeline;
 import 'package:vm_service_protos/vm_service_protos.dart';
 
+import 'common/service_test_common.dart' show mapFromListOfDebugAnnotations;
 import 'common/test_helper.dart';
 
 void primeTimeline() {
@@ -143,22 +143,6 @@
   return result;
 }
 
-Map<String, String> mapFromListOfDebugAnnotations(
-  List<DebugAnnotation> debugAnnotations,
-) {
-  return HashMap.fromEntries(
-    debugAnnotations.map((a) {
-      if (a.hasStringValue()) {
-        return MapEntry(a.name, a.stringValue);
-      } else if (a.hasLegacyJsonValue()) {
-        return MapEntry(a.name, a.legacyJsonValue);
-      } else {
-        throw 'We should not be writing annotations without values';
-      }
-    }),
-  );
-}
-
 void checkThatAllEventsHaveIsolateNumbers(Iterable<TrackEvent> events) {
   for (final event in events) {
     final debugAnnotations =
diff --git a/pkg/vm_service/test/timeline_events_for_completed_microtasks_test.dart b/pkg/vm_service/test/timeline_events_for_completed_microtasks_test.dart
new file mode 100644
index 0000000..8dfe37e
--- /dev/null
+++ b/pkg/vm_service/test/timeline_events_for_completed_microtasks_test.dart
@@ -0,0 +1,64 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart' hide Timeline;
+import 'package:vm_service_protos/vm_service_protos.dart';
+
+import 'common/service_test_common.dart' show mapFromListOfDebugAnnotations;
+import 'common/test_helper.dart';
+
+const String shortFile = 'timeline_events_for_completed_microtasks_test.dart';
+
+void primeTimeline() {
+  for (int i = 0; i < 5; i++) {
+    scheduleMicrotask(() {});
+  }
+}
+
+Iterable<TrackEvent> extractTrackEventsFromTracePackets(
+  List<TracePacket> packets,
+) =>
+    packets
+        .where((packet) => packet.hasTrackEvent())
+        .map((packet) => packet.trackEvent);
+
+final tests = <IsolateTest>[
+  (VmService service, IsolateRef isolateRef) async {
+    final result = await service.getPerfettoVMTimeline();
+
+    final trace = Trace.fromBuffer(base64Decode(result.trace!));
+    final packets = trace.packet;
+    final mainIsolateMicrotaskEvents =
+        extractTrackEventsFromTracePackets(packets)
+            .where((event) => event.name == 'Microtask')
+            .where((event) {
+      final debugAnnotations =
+          mapFromListOfDebugAnnotations(event.debugAnnotations);
+      return debugAnnotations['isolateId'] == isolateRef.id;
+    });
+    expect(mainIsolateMicrotaskEvents.length, greaterThanOrEqualTo(5));
+
+    for (final event in mainIsolateMicrotaskEvents) {
+      final debugAnnotations =
+          mapFromListOfDebugAnnotations(event.debugAnnotations);
+      expect(debugAnnotations['microtaskId'], isNotNull);
+      expect(
+        debugAnnotations['stack trace captured when microtask was enqueued'],
+        contains(shortFile),
+      );
+    }
+  },
+];
+
+void main([args = const <String>[]]) => runIsolateTests(
+      args,
+      tests,
+      shortFile,
+      testeeBefore: primeTimeline,
+      extraArgs: ['--profile-microtasks', '--timeline-streams=Microtask'],
+    );
diff --git a/runtime/bin/dart_embedder_api_impl.cc b/runtime/bin/dart_embedder_api_impl.cc
index a9d666b..92081c6 100644
--- a/runtime/bin/dart_embedder_api_impl.cc
+++ b/runtime/bin/dart_embedder_api_impl.cc
@@ -75,8 +75,9 @@
     Dart_ShutdownIsolate();
     return nullptr;
   }
-  result = bin::DartUtils::PrepareForScriptLoading(/*is_service_isolate=*/false,
-                                                   /*trace_loading=*/false);
+  result = bin::DartUtils::PrepareForScriptLoading(
+      /*is_service_isolate=*/false,
+      /*trace_loading=*/false, /*flag_profile_microtasks=*/false);
   Dart_ExitScope();
   Dart_ExitIsolate();
   return kernel_isolate;
diff --git a/runtime/bin/dartutils.cc b/runtime/bin/dartutils.cc
index 8d99be0..78e2c31 100644
--- a/runtime/bin/dartutils.cc
+++ b/runtime/bin/dartutils.cc
@@ -529,7 +529,31 @@
 }
 
 Dart_Handle DartUtils::PrepareAsyncLibrary(Dart_Handle async_lib,
-                                           Dart_Handle isolate_lib) {
+                                           Dart_Handle isolate_lib,
+                                           bool flag_profile_microtasks) {
+#if !defined(PRODUCT)
+  if (flag_profile_microtasks) {
+    Dart_Handle microtask_mirror_queue_type_name =
+        Dart_NewStringFromCString("_MicrotaskMirrorQueue");
+    RETURN_IF_ERROR(microtask_mirror_queue_type_name);
+
+    Dart_Handle microtask_mirror_queue_type =
+        Dart_GetNonNullableType(async_lib, microtask_mirror_queue_type_name,
+                                /*number_of_type_arguments=*/0,
+                                /*type_arguments=*/nullptr);
+    RETURN_IF_ERROR(microtask_mirror_queue_type);
+
+    Dart_Handle should_profile_microtasks_field_name =
+        Dart_NewStringFromCString("_shouldProfileMicrotasks");
+    RETURN_IF_ERROR(should_profile_microtasks_field_name);
+
+    Dart_Handle set_field_result =
+        Dart_SetField(microtask_mirror_queue_type,
+                      should_profile_microtasks_field_name, Dart_True());
+    RETURN_IF_ERROR(set_field_result);
+  }
+#endif  // !defined(PRODUCT)
+
   Dart_Handle schedule_immediate_closure =
       Dart_Invoke(isolate_lib, NewString("_getIsolateScheduleImmediateClosure"),
                   0, nullptr);
@@ -568,7 +592,8 @@
 }
 
 Dart_Handle DartUtils::PrepareForScriptLoading(bool is_service_isolate,
-                                               bool trace_loading) {
+                                               bool trace_loading,
+                                               bool flag_profile_microtasks) {
   // First ensure all required libraries are available.
   Dart_Handle url = NewString(kCoreLibURL);
   RETURN_IF_ERROR(url);
@@ -610,7 +635,8 @@
                                  trace_loading);
   RETURN_IF_ERROR(result);
 
-  RETURN_IF_ERROR(PrepareAsyncLibrary(async_lib, isolate_lib));
+  RETURN_IF_ERROR(
+      PrepareAsyncLibrary(async_lib, isolate_lib, flag_profile_microtasks));
   RETURN_IF_ERROR(PrepareCoreLibrary(core_lib, io_lib, is_service_isolate));
   RETURN_IF_ERROR(PrepareIsolateLibrary(isolate_lib));
   RETURN_IF_ERROR(PrepareIOLibrary(io_lib));
diff --git a/runtime/bin/dartutils.h b/runtime/bin/dartutils.h
index 2287790..6ec605b 100644
--- a/runtime/bin/dartutils.h
+++ b/runtime/bin/dartutils.h
@@ -178,7 +178,8 @@
   static Dart_Handle ReadStringFromFile(const char* filename);
   static Dart_Handle MakeUint8Array(const void* buffer, intptr_t length);
   static Dart_Handle PrepareForScriptLoading(bool is_service_isolate,
-                                             bool trace_loading);
+                                             bool trace_loading,
+                                             bool flag_profile_microtasks);
   static Dart_Handle SetupPackageConfig(const char* packages_file);
 
   static Dart_Handle SetupIOLibrary(const char* namespc_path,
@@ -316,7 +317,8 @@
                                         Dart_Handle io_lib,
                                         bool is_service_isolate);
   static Dart_Handle PrepareAsyncLibrary(Dart_Handle async_lib,
-                                         Dart_Handle isolate_lib);
+                                         Dart_Handle isolate_lib,
+                                         bool flag_profile_microtasks);
   static Dart_Handle PrepareIOLibrary(Dart_Handle io_lib);
   static Dart_Handle PrepareIsolateLibrary(Dart_Handle isolate_lib);
   static Dart_Handle PrepareCLILibrary(Dart_Handle cli_lib);
diff --git a/runtime/bin/main_impl.cc b/runtime/bin/main_impl.cc
index 29fbeb9..3f48e0a 100644
--- a/runtime/bin/main_impl.cc
+++ b/runtime/bin/main_impl.cc
@@ -161,7 +161,13 @@
   // Prepare builtin and other core libraries for use to resolve URIs.
   // Set up various closures, e.g: printing, timers etc.
   // Set up package configuration for URI resolution.
-  result = DartUtils::PrepareForScriptLoading(false, Options::trace_loading());
+#if defined(PRODUCT)
+  bool flag_profile_microtasks = false;
+#else
+  bool flag_profile_microtasks = Options::profile_microtasks();
+#endif  // defined(PRODUCT)
+  result = DartUtils::PrepareForScriptLoading(false, Options::trace_loading(),
+                                              flag_profile_microtasks);
   if (Dart_IsError(result)) return result;
 
   // Setup packages config if specified.
diff --git a/runtime/bin/main_options.cc b/runtime/bin/main_options.cc
index eac2cf3..8064ccd 100644
--- a/runtime/bin/main_options.cc
+++ b/runtime/bin/main_options.cc
@@ -213,6 +213,9 @@
 "      --timeline-streams=\"Compiler, Dart, GC, Microtask\"\n"
 "  This set is subject to change.\n"
 "  Please see these options for further documentation.\n"
+"--profile-microtasks\n"
+"  Record information about each microtask. Information about completed\n"
+"  microtasks will be written to the \"Microtask\" timeline stream.\n"
 #endif  // !defined(PRODUCT)
 "--version\n"
 "  Print the VM version.\n"
diff --git a/runtime/bin/main_options.h b/runtime/bin/main_options.h
index 5734a09..b75632c 100644
--- a/runtime/bin/main_options.h
+++ b/runtime/bin/main_options.h
@@ -58,6 +58,7 @@
   V(no_serve_observatory, disable_observatory)                                 \
   V(serve_observatory, enable_observatory)                                     \
   V(print_dtd, print_dtd)                                                      \
+  V(profile_microtasks, profile_microtasks)                                    \
   /* The purpose of this flag is documented in */                              \
   /* pkg/dartdev/lib/src/commands/run.dart. */                                 \
   V(resident, resident)
diff --git a/runtime/bin/run_vm_tests.cc b/runtime/bin/run_vm_tests.cc
index 833e62b..b7c39c4 100644
--- a/runtime/bin/run_vm_tests.cc
+++ b/runtime/bin/run_vm_tests.cc
@@ -255,7 +255,8 @@
 
   bin::DartUtils::SetOriginalWorkingDirectory();
   Dart_Handle result = bin::DartUtils::PrepareForScriptLoading(
-      /*is_service_isolate=*/false, /*trace_loading=*/false);
+      /*is_service_isolate=*/false, /*trace_loading=*/false,
+      /*flag_profile_microtasks=*/false);
   CHECK_RESULT(result);
 
   // Setup kernel service as the main script for this isolate.
diff --git a/runtime/bin/vmservice_impl.cc b/runtime/bin/vmservice_impl.cc
index 3d6a0c3..c99e4ff 100644
--- a/runtime/bin/vmservice_impl.cc
+++ b/runtime/bin/vmservice_impl.cc
@@ -137,8 +137,9 @@
   // Prepare builtin and its dependent libraries for use to resolve URIs.
   // Set up various closures, e.g: printing, timers etc.
   // Set up 'package root' for URI resolution.
-  result = DartUtils::PrepareForScriptLoading(/*is_service_isolate=*/true,
-                                              trace_loading);
+  result = DartUtils::PrepareForScriptLoading(
+      /*is_service_isolate=*/true, trace_loading,
+      /*flag_profile_microtasks=*/false);
   SHUTDOWN_ON_ERROR(result);
 
   Dart_Handle url = DartUtils::NewString(kVMServiceIOLibraryUri);
diff --git a/runtime/engine/engine.cc b/runtime/engine/engine.cc
index e3e9d6b..6d7c642 100644
--- a/runtime/engine/engine.cc
+++ b/runtime/engine/engine.cc
@@ -194,8 +194,8 @@
 
   // In fact, this call initializes core libraries, (e.g. `print` doesn't work
   // without it).
-  Dart_Handle core_libs_result =
-      bin::DartUtils::PrepareForScriptLoading(false, false);
+  Dart_Handle core_libs_result = bin::DartUtils::PrepareForScriptLoading(
+      false, false, /*flag_profile_microtasks=*/false);
   if (Dart_IsError(core_libs_result)) {
     *error = Utils::StrDup(Dart_GetError(core_libs_result));
     Dart_ShutdownIsolate();
diff --git a/runtime/lib/async.cc b/runtime/lib/async.cc
index 538c20e..c8ebb34 100644
--- a/runtime/lib/async.cc
+++ b/runtime/lib/async.cc
@@ -2,9 +2,13 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+#include "lib/stacktrace.h"
+#include "platform/assert.h"
 #include "vm/bootstrap_natives.h"
 #include "vm/debugger.h"
 #include "vm/exceptions.h"
+#include "vm/flags.h"
+#include "vm/microtask_mirror_queues.h"
 #include "vm/native_entry.h"
 #include "vm/object_store.h"
 #include "vm/runtime_entry.h"
@@ -56,4 +60,51 @@
   return closure.ptr();
 }
 
+DEFINE_NATIVE_ENTRY(MicrotaskMirrorQueue_onScheduleAsyncCallback, 0, 0) {
+  // There is logic in `sdk/lib/async/schedule_microtask.dart` that ensures that
+  // this function can only ever be called when the `--profile-microtasks` CLI
+  // flag is set in non-PRODUCT modes.
+#if !defined(PRODUCT)
+  const StackTrace& stack_trace = GetCurrentStackTrace(
+      // We pass a `skip_frames` argument of 1 to skip the
+      // `_MicrotaskMirrorQueue._onScheduleAsyncCallback` frame.
+      1);
+  MicrotaskMirrorQueues::GetQueue(static_cast<int64_t>(isolate->main_port()))
+      ->OnScheduleAsyncCallback(stack_trace);
+  return Object::null();
+#else
+  UNREACHABLE();
+#endif  // !defined(PRODUCT)
+}
+
+DEFINE_NATIVE_ENTRY(MicrotaskMirrorQueue_onSchedulePriorityAsyncCallback,
+                    0,
+                    0) {
+  // There is logic in `sdk/lib/async/schedule_microtask.dart` that ensures that
+  // this function can only ever be called when the `--profile-microtasks` CLI
+  // flag is set in non-PRODUCT modes.
+#if !defined(PRODUCT)
+  MicrotaskMirrorQueues::GetQueue(static_cast<int64_t>(isolate->main_port()))
+      ->OnSchedulePriorityAsyncCallback();
+  return Object::null();
+#else
+  UNREACHABLE();
+#endif  // !defined(PRODUCT)
+}
+
+DEFINE_NATIVE_ENTRY(MicrotaskMirrorQueue_onAsyncCallbackComplete, 0, 2) {
+  // There is logic in `sdk/lib/async/schedule_microtask.dart` that ensures that
+  // this function can only ever be called when the `--profile-microtasks` CLI
+  // flag is set in non-PRODUCT modes.
+#if !defined(PRODUCT)
+  GET_NON_NULL_NATIVE_ARGUMENT(Integer, start_time, arguments->NativeArgAt(0));
+  GET_NON_NULL_NATIVE_ARGUMENT(Integer, end_time, arguments->NativeArgAt(1));
+  MicrotaskMirrorQueues::GetQueue(static_cast<int64_t>(isolate->main_port()))
+      ->OnAsyncCallbackComplete(start_time.Value(), end_time.Value());
+  return Object::null();
+#else
+  UNREACHABLE();
+#endif  // !defined(PRODUCT)
+}
+
 }  // namespace dart
diff --git a/runtime/vm/bootstrap_natives.h b/runtime/vm/bootstrap_natives.h
index 1562fcc..1ad3835c6 100644
--- a/runtime/vm/bootstrap_natives.h
+++ b/runtime/vm/bootstrap_natives.h
@@ -149,6 +149,9 @@
   V(Timeline_getTraceClock, 0)                                                 \
   V(Timeline_isDartStreamEnabled, 0)                                           \
   V(Timeline_reportTaskEvent, 5)                                               \
+  V(MicrotaskMirrorQueue_onScheduleAsyncCallback, 0)                           \
+  V(MicrotaskMirrorQueue_onSchedulePriorityAsyncCallback, 0)                   \
+  V(MicrotaskMirrorQueue_onAsyncCallbackComplete, 2)                           \
   V(TypedDataBase_length, 1)                                                   \
   V(TypedDataBase_setClampedRange, 5)                                          \
   V(TypedData_GetFloat32, 2)                                                   \
diff --git a/runtime/vm/dart.cc b/runtime/vm/dart.cc
index 093cbef..d17a7a8 100644
--- a/runtime/vm/dart.cc
+++ b/runtime/vm/dart.cc
@@ -33,6 +33,7 @@
 #include "vm/kernel_isolate.h"
 #include "vm/message_handler.h"
 #include "vm/metrics.h"
+#include "vm/microtask_mirror_queues.h"
 #include "vm/native_entry.h"
 #include "vm/native_message_handler.h"
 #include "vm/object.h"
@@ -764,6 +765,7 @@
   }
   Timeline::Cleanup();
 #endif
+  NOT_IN_PRODUCT(MicrotaskMirrorQueues::CleanUp());
   Zone::Cleanup();
   Random::Cleanup();
   // Delete the current thread's TLS and set it's TLS to null.
diff --git a/runtime/vm/microtask_mirror_queues.cc b/runtime/vm/microtask_mirror_queues.cc
new file mode 100644
index 0000000..07f6e9f
--- /dev/null
+++ b/runtime/vm/microtask_mirror_queues.cc
@@ -0,0 +1,92 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+#if !defined(PRODUCT)
+
+#include "vm/microtask_mirror_queues.h"
+
+#include <utility>
+
+#include "platform/hashmap.h"
+#include "vm/flags.h"
+#include "vm/object.h"
+#include "vm/timeline.h"
+
+namespace dart {
+
+MicrotaskMirrorQueue* MicrotaskMirrorQueues::GetQueue(int64_t isolate_id) {
+  void* key = reinterpret_cast<void*>(isolate_id);
+  const intptr_t hash = Utils::WordHash(isolate_id);
+
+  MutexLocker ml(&isolate_id_to_queue_lock_);
+
+  SimpleHashMap::Entry* entry = isolate_id_to_queue_.Lookup(key, hash, true);
+  if (entry->value == nullptr) {
+    entry->value = new MicrotaskMirrorQueue();
+  }
+  return static_cast<MicrotaskMirrorQueue*>(entry->value);
+}
+
+void MicrotaskMirrorQueue::OnScheduleAsyncCallback(const StackTrace& st) {
+  if (is_disabled_) {
+    return;
+  }
+
+  queue_.PushBack(MicrotaskMirrorQueueEntry(
+      next_available_id_++, CStringUniquePtr(Utils::StrDup(st.ToCString()))));
+}
+
+void MicrotaskMirrorQueue::OnSchedulePriorityAsyncCallback() {
+  // If this function is called, it means that the microtask queue can no longer
+  // be accurately modeled by a |ListQueue|. This only ever gets called when an
+  // exception goes unhandled, so we just handle the situation by disabling all
+  // further reads from / writes to this queue.
+  is_disabled_ = true;
+}
+
+void MicrotaskMirrorQueue::OnAsyncCallbackComplete(int64_t start_time,
+                                                   int64_t end_time) {
+  if (is_disabled_) {
+    return;
+  }
+
+  ASSERT(queue_.Length() >= 1);
+  MicrotaskMirrorQueueEntry&& front = std::move(queue_.PopFront());
+
+  TimelineStream* stream = Timeline::GetMicrotaskStream();
+  ASSERT(stream != nullptr);
+  TimelineEvent* event = stream->StartEvent();
+  if (event != nullptr) {
+    if (start_time < end_time) {
+      event->Duration("Microtask", start_time, end_time);
+    } else {
+      event->Instant("Microtask", start_time);
+    }
+
+    event->SetNumArguments(2);
+    event->FormatArgument(0, "microtaskId", "%" Pd, front.id());
+    event->SetArgument(1, "stack trace captured when microtask was enqueued",
+                       front.ReleaseStackTrace());
+    event->Complete();
+  }
+}
+
+Mutex MicrotaskMirrorQueues::isolate_id_to_queue_lock_;
+
+SimpleHashMap MicrotaskMirrorQueues::isolate_id_to_queue_(
+    &SimpleHashMap::SamePointerValue,
+    MicrotaskMirrorQueues::kIsolateIdToQueueInitialCapacity);
+
+void MicrotaskMirrorQueues::CleanUp() {
+  for (SimpleHashMap::Entry* entry = isolate_id_to_queue_.Start();
+       entry != nullptr; entry = isolate_id_to_queue_.Next(entry)) {
+    MicrotaskMirrorQueue* value =
+        static_cast<MicrotaskMirrorQueue*>(entry->value);
+    delete value;
+  }
+}
+
+}  // namespace dart
+
+#endif  // !defined(PRODUCT)
diff --git a/runtime/vm/microtask_mirror_queues.h b/runtime/vm/microtask_mirror_queues.h
new file mode 100644
index 0000000..f47192f
--- /dev/null
+++ b/runtime/vm/microtask_mirror_queues.h
@@ -0,0 +1,78 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+#ifndef RUNTIME_VM_MICROTASK_MIRROR_QUEUES_H_
+#define RUNTIME_VM_MICROTASK_MIRROR_QUEUES_H_
+
+#include <utility>
+
+#include "platform/hashmap.h"
+#include "platform/list_queue.h"
+#include "platform/synchronization.h"
+#include "platform/utils.h"
+#include "vm/stack_frame.h"
+
+namespace dart {
+
+class MicrotaskMirrorQueueEntry : public ValueObject {
+ public:
+  static constexpr intptr_t kInvalidId = -1;
+
+  MicrotaskMirrorQueueEntry() : id_(kInvalidId), stack_trace_(nullptr) {}
+  MicrotaskMirrorQueueEntry(intptr_t id, CStringUniquePtr&& st)
+      : id_(id), stack_trace_(std::move(st)) {}
+
+  void operator=(MicrotaskMirrorQueueEntry&& other) {
+    id_ = other.id_;
+    stack_trace_.swap(other.stack_trace_);
+  }
+
+  intptr_t id() const { return id_; }
+  CStringUniquePtr const& stack_trace() const { return stack_trace_; }
+  /// Releases ownership of the stack trace string that this entry is holding.
+  char* ReleaseStackTrace() { return stack_trace_.release(); }
+
+ private:
+  intptr_t id_;
+  CStringUniquePtr stack_trace_;
+};
+
+/// A queue that mirrors the microtask queue of an isolate. This allows the VM
+/// Service to return information about microtasks.
+class MicrotaskMirrorQueue {
+ public:
+  MicrotaskMirrorQueue()
+      : is_disabled_(false), queue_(), next_available_id_(0) {}
+
+  void OnScheduleAsyncCallback(const StackTrace& st);
+  void OnSchedulePriorityAsyncCallback();
+  void OnAsyncCallbackComplete(int64_t start_time, int64_t end_time);
+
+ private:
+  bool is_disabled_;
+  ListQueue<MicrotaskMirrorQueueEntry> queue_;
+  // The unique ID that will be assigned to the next |MicrotaskMirrorQueueEntry|
+  // added to |queue_|.
+  intptr_t next_available_id_;
+
+  DISALLOW_COPY_AND_ASSIGN(MicrotaskMirrorQueue);
+};
+
+/// A wrapper around a map from isolate IDs to |MicrotaskMirrorQueue|s.
+class MicrotaskMirrorQueues : public AllStatic {
+ public:
+  static MicrotaskMirrorQueue* GetQueue(int64_t isolate_id);
+  static void CleanUp();
+
+ private:
+  static constexpr intptr_t kIsolateIdToQueueInitialCapacity = 1 << 4;  // 16
+  static Mutex isolate_id_to_queue_lock_;
+  static SimpleHashMap isolate_id_to_queue_;
+
+  DISALLOW_COPY_AND_ASSIGN(MicrotaskMirrorQueues);
+};
+
+}  // namespace dart
+
+#endif  // RUNTIME_VM_MICROTASK_MIRROR_QUEUES_H_
diff --git a/runtime/vm/unit_test.cc b/runtime/vm/unit_test.cc
index 4f9714c..a6fb14c 100644
--- a/runtime/vm/unit_test.cc
+++ b/runtime/vm/unit_test.cc
@@ -154,7 +154,7 @@
   RELEASE_ASSERT(ok);
   Dart_Handle result = bin::DartUtils::PrepareForScriptLoading(
       /*is_service_isolate=*/false,
-      /*trace_loading=*/false);
+      /*trace_loading=*/false, /*flag_profile_microtasks=*/false);
   Dart_ExitScope();
 
   RELEASE_ASSERT(!Dart_IsError(result));
diff --git a/runtime/vm/vm_sources.gni b/runtime/vm/vm_sources.gni
index a983cbc..70079cb 100644
--- a/runtime/vm/vm_sources.gni
+++ b/runtime/vm/vm_sources.gni
@@ -177,6 +177,8 @@
   "message_snapshot.h",
   "metrics.cc",
   "metrics.h",
+  "microtask_mirror_queues.cc",
+  "microtask_mirror_queues.h",
   "native_arguments.h",
   "native_entry.cc",
   "native_entry.h",
diff --git a/sdk/lib/_internal/vm/lib/schedule_microtask_patch.dart b/sdk/lib/_internal/vm/lib/schedule_microtask_patch.dart
index dd5bb1a..0d94a8b 100644
--- a/sdk/lib/_internal/vm/lib/schedule_microtask_patch.dart
+++ b/sdk/lib/_internal/vm/lib/schedule_microtask_patch.dart
@@ -5,6 +5,24 @@
 part of "async_patch.dart";
 
 @patch
+abstract final class _MicrotaskMirrorQueue {
+  @patch
+  @pragma("vm:external-name", "MicrotaskMirrorQueue_onScheduleAsyncCallback")
+  external static void _onScheduleAsyncCallback();
+
+  @patch
+  @pragma(
+    "vm:external-name",
+    "MicrotaskMirrorQueue_onSchedulePriorityAsyncCallback",
+  )
+  external static void _onSchedulePriorityAsyncCallback();
+
+  @patch
+  @pragma("vm:external-name", "MicrotaskMirrorQueue_onAsyncCallbackComplete")
+  external static void _onAsyncCallbackComplete(int startTime, int endTime);
+}
+
+@patch
 class _AsyncRun {
   @patch
   static void _scheduleImmediate(void callback()) {
diff --git a/sdk/lib/async/async.dart b/sdk/lib/async/async.dart
index af2f4c3..7808477 100644
--- a/sdk/lib/async/async.dart
+++ b/sdk/lib/async/async.dart
@@ -104,6 +104,7 @@
 library dart.async;
 
 import "dart:collection" show HashMap;
+import 'dart:developer' show Timeline;
 import "dart:_internal"
     show
         CastStream,
diff --git a/sdk/lib/async/schedule_microtask.dart b/sdk/lib/async/schedule_microtask.dart
index 25e7c03..32c2cbd 100644
--- a/sdk/lib/async/schedule_microtask.dart
+++ b/sdk/lib/async/schedule_microtask.dart
@@ -37,7 +37,18 @@
     var next = entry.next;
     _nextCallback = next;
     if (next == null) _lastCallback = null;
-    (entry.callback)();
+    if (const bool.fromEnvironment("dart.vm.product") ||
+        !_MicrotaskMirrorQueue._shouldProfileMicrotasks) {
+      (entry.callback)();
+    } else {
+      final callbackStartTime = Timeline.now;
+      (entry.callback)();
+      final callbackEndTime = Timeline.now;
+      _MicrotaskMirrorQueue._onAsyncCallbackComplete(
+        callbackStartTime,
+        callbackEndTime,
+      );
+    }
   }
 }
 
@@ -62,6 +73,10 @@
 /// microtasks, but as part of the current system event.
 void _scheduleAsyncCallback(_AsyncCallback callback) {
   _AsyncCallbackEntry newEntry = _AsyncCallbackEntry(callback);
+  if (!const bool.fromEnvironment("dart.vm.product") &&
+      _MicrotaskMirrorQueue._shouldProfileMicrotasks) {
+    _MicrotaskMirrorQueue._onScheduleAsyncCallback();
+  }
   _AsyncCallbackEntry? lastCallback = _lastCallback;
   if (lastCallback == null) {
     _nextCallback = _lastCallback = newEntry;
@@ -87,6 +102,10 @@
     return;
   }
   _AsyncCallbackEntry entry = _AsyncCallbackEntry(callback);
+  if (!const bool.fromEnvironment("dart.vm.product") &&
+      _MicrotaskMirrorQueue._shouldProfileMicrotasks) {
+    _MicrotaskMirrorQueue._onSchedulePriorityAsyncCallback();
+  }
   _AsyncCallbackEntry? lastPriorityCallback = _lastPriorityCallback;
   if (lastPriorityCallback == null) {
     entry.next = _nextCallback;
@@ -148,6 +167,22 @@
   Zone.current.scheduleMicrotask(Zone.current.bindCallbackGuarded(callback));
 }
 
+@pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product"))
+abstract final class _MicrotaskMirrorQueue {
+  // This will be set to true by the native runtime's
+  // `DartUtils::PrepareAsyncLibrary` when the CLI flag `--profile-microtasks`
+  // is set.
+  @pragma("vm:entry-point", !const bool.fromEnvironment("dart.vm.product"))
+  static bool _shouldProfileMicrotasks = false;
+
+  // The following methods are only implemented for the native runtime. Those
+  // implementations are in
+  // `sdk/lib/_internal/vm/lib/schedule_microtask_patch.dart`.
+  static void _onScheduleAsyncCallback() {}
+  static void _onSchedulePriorityAsyncCallback() {}
+  static void _onAsyncCallbackComplete(int startTime, int endTime) {}
+}
+
 class _AsyncRun {
   /// Schedule the given callback before any other event in the event-loop.
   external static void _scheduleImmediate(void Function() callback);