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);