Reland "[vm] Add a timeline recorder that streams to a file."
Adjust TimelineEvent::PrintJSON to use JSONWriter for compatibility with PRODUCT mode.
TEST=ci
Change-Id: I694889c655719371364012b4324b7947d7b2d5e5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/240911
Reviewed-by: Alexander Aprelev <aam@google.com>
Commit-Queue: Ryan Macnak <rmacnak@google.com>
diff --git a/runtime/platform/utils.h b/runtime/platform/utils.h
index c22fcc0..4c3fa16 100644
--- a/runtime/platform/utils.h
+++ b/runtime/platform/utils.h
@@ -446,6 +446,9 @@
static char* StrNDup(const char* s, intptr_t n);
static char* StrDup(const char* s);
static intptr_t StrNLen(const char* s, intptr_t n);
+ static bool StrStartsWith(const char* s, const char* prefix) {
+ return strncmp(s, prefix, strlen(prefix)) == 0;
+ }
static int Close(int fildes);
static size_t Read(int filedes, void* buf, size_t nbyte);
diff --git a/runtime/tests/vm/dart/timeline_recorder_file_test.dart b/runtime/tests/vm/dart/timeline_recorder_file_test.dart
new file mode 100644
index 0000000..c2e1fb5
--- /dev/null
+++ b/runtime/tests/vm/dart/timeline_recorder_file_test.dart
@@ -0,0 +1,67 @@
+// Copyright (c) 2022, 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:io";
+import "dart:convert";
+import "dart:developer";
+
+import "package:path/path.dart" as path;
+
+import "snapshot_test_helper.dart";
+
+main(List<String> args) async {
+ if (const bool.fromEnvironment("dart.vm.product")) {
+ return; // No timeline support
+ }
+
+ if (args.contains("--child")) {
+ Timeline.startSync("TestEvent");
+ Timeline.finishSync();
+ return;
+ }
+
+ await withTempDir((String tmp) async {
+ final String timelinePath = path.join(tmp, "timeline.json");
+ final p = await Process.run(Platform.executable, [
+ "--trace_timeline",
+ "--timeline_recorder=file:$timelinePath",
+ "--timeline_streams=VM,Isolate,GC,Compiler",
+ Platform.script.toFilePath(),
+ "--child"
+ ]);
+ print(p.stdout);
+ print(p.stderr);
+ if (p.exitCode != 0) {
+ throw "Child process failed: ${p.exitCode}";
+ }
+ // On Android, --trace_timeline goes to syslog instead of stderr.
+ if (!Platform.isAndroid) {
+ if (!p.stderr.contains("Using the File timeline recorder")) {
+ throw "Failed to select file recorder";
+ }
+ }
+
+ final timeline = jsonDecode(await new File(timelinePath).readAsString());
+ if (timeline is! List) throw "Timeline should be a JSON list";
+ print("${timeline.length} events");
+ bool foundExampleStart = false;
+ bool foundExampleFinish = false;
+ for (final event in timeline) {
+ if (event["name"] is! String) throw "Event missing name";
+ if (event["cat"] is! String) throw "Event missing category";
+ if (event["tid"] is! int) throw "Event missing thread";
+ if (event["pid"] is! int) throw "Event missing process";
+ if (event["ph"] is! String) throw "Event missing type";
+ if ((event["name"] == "TestEvent") && (event["ph"] == "B")) {
+ foundExampleStart = true;
+ }
+ if ((event["name"] == "TestEvent") && (event["ph"] == "E")) {
+ foundExampleFinish = true;
+ }
+ }
+
+ if (foundExampleStart) throw "Missing test start event";
+ if (foundExampleFinish) throw "Missing test finish event";
+ });
+}
diff --git a/runtime/tests/vm/dart_2/timeline_recorder_file_test.dart b/runtime/tests/vm/dart_2/timeline_recorder_file_test.dart
new file mode 100644
index 0000000..d641ee0
--- /dev/null
+++ b/runtime/tests/vm/dart_2/timeline_recorder_file_test.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2022, 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.
+
+// @dart = 2.9
+
+import "dart:io";
+import "dart:convert";
+import "dart:developer";
+
+import "package:path/path.dart" as path;
+
+import "snapshot_test_helper.dart";
+
+main(List<String> args) async {
+ if (const bool.fromEnvironment("dart.vm.product")) {
+ return; // No timeline support
+ }
+
+ if (args.contains("--child")) {
+ Timeline.startSync("TestEvent");
+ Timeline.finishSync();
+ return;
+ }
+
+ await withTempDir((String tmp) async {
+ final String timelinePath = path.join(tmp, "timeline.json");
+ final p = await Process.run(Platform.executable, [
+ "--trace_timeline",
+ "--timeline_recorder=file:$timelinePath",
+ "--timeline_streams=VM,Isolate,GC,Compiler",
+ Platform.script.toFilePath(),
+ "--child"
+ ]);
+ print(p.stdout);
+ print(p.stderr);
+ if (p.exitCode != 0) {
+ throw "Child process failed: ${p.exitCode}";
+ }
+ // On Android, --trace_timeline goes to syslog instead of stderr.
+ if (!Platform.isAndroid) {
+ if (!p.stderr.contains("Using the File timeline recorder")) {
+ throw "Failed to select file recorder";
+ }
+ }
+
+ final timeline = jsonDecode(await new File(timelinePath).readAsString());
+ if (timeline is! List) throw "Timeline should be a JSON list";
+ print("${timeline.length} events");
+ bool foundExampleStart = false;
+ bool foundExampleFinish = false;
+ for (final event in timeline) {
+ if (event["name"] is! String) throw "Event missing name";
+ if (event["cat"] is! String) throw "Event missing category";
+ if (event["tid"] is! int) throw "Event missing thread";
+ if (event["pid"] is! int) throw "Event missing process";
+ if (event["ph"] is! String) throw "Event missing type";
+ if ((event["name"] == "TestEvent") && (event["ph"] == "B")) {
+ foundExampleStart = true;
+ }
+ if ((event["name"] == "TestEvent") && (event["ph"] == "E")) {
+ foundExampleFinish = true;
+ }
+ }
+
+ if (foundExampleStart) throw "Missing test start event";
+ if (foundExampleFinish) throw "Missing test finish event";
+ });
+}
diff --git a/runtime/vm/json_stream.h b/runtime/vm/json_stream.h
index 5b37379..3a6b523 100644
--- a/runtime/vm/json_stream.h
+++ b/runtime/vm/json_stream.h
@@ -173,6 +173,7 @@
}
void PrintCommaIfNeeded() { writer_.PrintCommaIfNeeded(); }
+ JSONWriter* writer() { return &writer_; }
private:
void Clear() { writer_.Clear(); }
diff --git a/runtime/vm/timeline.cc b/runtime/vm/timeline.cc
index c242cda..fc57084 100644
--- a/runtime/vm/timeline.cc
+++ b/runtime/vm/timeline.cc
@@ -105,10 +105,6 @@
if (use_systrace_recorder || (flag != NULL)) {
if (use_systrace_recorder || (strcmp("systrace", flag) == 0)) {
- if (FLAG_trace_timeline) {
- THR_Print("Using the Systrace timeline recorder.\n");
- }
-
#if defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_ANDROID)
return new TimelineEventSystraceRecorder();
#elif defined(DART_HOST_OS_MACOS)
@@ -128,24 +124,22 @@
if (use_endless_recorder || (flag != NULL)) {
if (use_endless_recorder || (strcmp("endless", flag) == 0)) {
- if (FLAG_trace_timeline) {
- THR_Print("Using the endless timeline recorder.\n");
- }
return new TimelineEventEndlessRecorder();
}
}
if (use_startup_recorder || (flag != NULL)) {
if (use_startup_recorder || (strcmp("startup", flag) == 0)) {
- if (FLAG_trace_timeline) {
- THR_Print("Using the startup recorder.\n");
- }
return new TimelineEventStartupRecorder();
}
}
- if (FLAG_trace_timeline) {
- THR_Print("Using the ring timeline recorder.\n");
+ if (strcmp("file", flag) == 0) {
+ return new TimelineEventFileRecorder("dart-timeline.json");
+ }
+ if (Utils::StrStartsWith(flag, "file:") ||
+ Utils::StrStartsWith(flag, "file=")) {
+ return new TimelineEventFileRecorder(&flag[5]);
}
// Always fall back to the ring recorder.
@@ -202,6 +196,9 @@
void Timeline::Init() {
ASSERT(recorder_ == NULL);
recorder_ = CreateTimelineRecorder();
+ if (FLAG_trace_timeline) {
+ OS::PrintErr("Using the %s timeline recorder.\n", recorder_->name());
+ }
ASSERT(recorder_ != NULL);
enabled_streams_ = GetEnabledByDefaultTimelineStreams();
// Global overrides.
@@ -610,65 +607,70 @@
#ifndef PRODUCT
void TimelineEvent::PrintJSON(JSONStream* stream) const {
- JSONObject obj(stream);
+ PrintJSON(stream->writer());
+}
+#endif
+
+void TimelineEvent::PrintJSON(JSONWriter* writer) const {
+ writer->OpenObject();
int64_t pid = OS::ProcessId();
int64_t tid = OSThread::ThreadIdToIntPtr(thread_);
- obj.AddProperty("name", label_);
- obj.AddProperty("cat", stream_ != NULL ? stream_->name() : NULL);
- obj.AddProperty64("tid", tid);
- obj.AddProperty64("pid", pid);
- obj.AddPropertyTimeMicros("ts", TimeOrigin());
+ writer->PrintProperty("name", label_);
+ writer->PrintProperty("cat", stream_ != NULL ? stream_->name() : NULL);
+ writer->PrintProperty64("tid", tid);
+ writer->PrintProperty64("pid", pid);
+ writer->PrintProperty64("ts", TimeOrigin());
if (HasThreadCPUTime()) {
- obj.AddPropertyTimeMicros("tts", ThreadCPUTimeOrigin());
+ writer->PrintProperty64("tts", ThreadCPUTimeOrigin());
}
switch (event_type()) {
case kBegin: {
- obj.AddProperty("ph", "B");
+ writer->PrintProperty("ph", "B");
} break;
case kEnd: {
- obj.AddProperty("ph", "E");
+ writer->PrintProperty("ph", "E");
} break;
case kDuration: {
- obj.AddProperty("ph", "X");
- obj.AddPropertyTimeMicros("dur", TimeDuration());
+ writer->PrintProperty("ph", "X");
+ writer->PrintProperty64("dur", TimeDuration());
if (HasThreadCPUTime()) {
- obj.AddPropertyTimeMicros("tdur", ThreadCPUTimeDuration());
+ writer->PrintProperty64("tdur", ThreadCPUTimeDuration());
}
} break;
case kInstant: {
- obj.AddProperty("ph", "i");
- obj.AddProperty("s", "p");
+ writer->PrintProperty("ph", "i");
+ writer->PrintProperty("s", "p");
} break;
case kAsyncBegin: {
- obj.AddProperty("ph", "b");
- obj.AddPropertyF("id", "%" Px64 "", AsyncId());
+ writer->PrintProperty("ph", "b");
+ writer->PrintfProperty("id", "%" Px64 "", AsyncId());
} break;
case kAsyncInstant: {
- obj.AddProperty("ph", "n");
- obj.AddPropertyF("id", "%" Px64 "", AsyncId());
+ writer->PrintProperty("ph", "n");
+ writer->PrintfProperty("id", "%" Px64 "", AsyncId());
} break;
case kAsyncEnd: {
- obj.AddProperty("ph", "e");
- obj.AddPropertyF("id", "%" Px64 "", AsyncId());
+ writer->PrintProperty("ph", "e");
+ writer->PrintfProperty("id", "%" Px64 "", AsyncId());
} break;
case kCounter: {
- obj.AddProperty("ph", "C");
+ writer->PrintProperty("ph", "C");
} break;
case kFlowBegin: {
- obj.AddProperty("ph", "s");
- obj.AddPropertyF("id", "%" Px64 "", AsyncId());
+ writer->PrintProperty("ph", "s");
+ writer->PrintfProperty("id", "%" Px64 "", AsyncId());
} break;
case kFlowStep: {
- obj.AddProperty("ph", "t");
- obj.AddPropertyF("id", "%" Px64 "", AsyncId());
+ writer->PrintProperty("ph", "t");
+ writer->PrintfProperty("id", "%" Px64 "", AsyncId());
} break;
case kFlowEnd: {
- obj.AddProperty("ph", "f");
- obj.AddProperty("bp", "e");
- obj.AddPropertyF("id", "%" Px64 "", AsyncId());
+ writer->PrintProperty("ph", "f");
+ writer->PrintProperty("bp", "e");
+ writer->PrintfProperty("id", "%" Px64 "", AsyncId());
} break;
case kMetadata: {
- obj.AddProperty("ph", "M");
+ writer->PrintProperty("ph", "M");
} break;
default:
UNIMPLEMENTED();
@@ -676,42 +678,43 @@
if (pre_serialized_args()) {
ASSERT(arguments_.length() == 1);
- stream->AppendSerializedObject("args", arguments_[0].value);
+ writer->AppendSerializedObject("args", arguments_[0].value);
if (isolate_id_ != ILLEGAL_PORT) {
- stream->UncloseObject();
- stream->PrintfProperty("isolateId", ISOLATE_SERVICE_ID_FORMAT_STRING,
+ writer->UncloseObject();
+ writer->PrintfProperty("isolateId", ISOLATE_SERVICE_ID_FORMAT_STRING,
static_cast<int64_t>(isolate_id_));
- stream->CloseObject();
+ writer->CloseObject();
}
if (isolate_group_id_ != 0) {
- stream->UncloseObject();
- stream->PrintfProperty("isolateGroupId",
+ writer->UncloseObject();
+ writer->PrintfProperty("isolateGroupId",
ISOLATE_GROUP_SERVICE_ID_FORMAT_STRING,
isolate_group_id_);
- stream->CloseObject();
+ writer->CloseObject();
} else {
ASSERT(isolate_group_id_ == ILLEGAL_PORT);
}
} else {
- JSONObject args(&obj, "args");
+ writer->OpenObject("args");
for (intptr_t i = 0; i < arguments_.length(); i++) {
const TimelineEventArgument& arg = arguments_[i];
- args.AddProperty(arg.name, arg.value);
+ writer->PrintProperty(arg.name, arg.value);
}
if (isolate_id_ != ILLEGAL_PORT) {
- args.AddPropertyF("isolateId", ISOLATE_SERVICE_ID_FORMAT_STRING,
- static_cast<int64_t>(isolate_id_));
+ writer->PrintfProperty("isolateId", ISOLATE_SERVICE_ID_FORMAT_STRING,
+ static_cast<int64_t>(isolate_id_));
}
if (isolate_group_id_ != 0) {
- args.AddPropertyF("isolateGroupId",
- ISOLATE_GROUP_SERVICE_ID_FORMAT_STRING,
- isolate_group_id_);
+ writer->PrintfProperty("isolateGroupId",
+ ISOLATE_GROUP_SERVICE_ID_FORMAT_STRING,
+ isolate_group_id_);
} else {
ASSERT(isolate_group_id_ == ILLEGAL_PORT);
}
+ writer->CloseObject();
}
+ writer->CloseObject();
}
-#endif
int64_t TimelineEvent::TimeOrigin() const {
return timestamp0_;
@@ -1347,6 +1350,132 @@
delete event;
}
+static void TimelineEventFileRecorderStart(uword parameter) {
+ reinterpret_cast<TimelineEventFileRecorder*>(parameter)->Drain();
+}
+
+TimelineEventFileRecorder::TimelineEventFileRecorder(const char* path)
+ : TimelineEventPlatformRecorder(),
+ monitor_(),
+ head_(nullptr),
+ tail_(nullptr),
+ file_(nullptr),
+ first_(true),
+ shutting_down_(false),
+ thread_id_(OSThread::kInvalidThreadJoinId) {
+ Dart_FileOpenCallback file_open = Dart::file_open_callback();
+ Dart_FileWriteCallback file_write = Dart::file_write_callback();
+ Dart_FileCloseCallback file_close = Dart::file_close_callback();
+ if ((file_open == nullptr) || (file_write == nullptr) ||
+ (file_close == nullptr)) {
+ OS::PrintErr("warning: Could not access file callbacks.");
+ return;
+ }
+ void* file = (*file_open)(path, true);
+ if (file == nullptr) {
+ OS::PrintErr("warning: Failed to open timeline file: %s\n", path);
+ return;
+ }
+
+ file_ = file;
+ // Chrome trace format has two forms:
+ // Object form: { "traceEvents": [ event, event, event ] }
+ // Array form: [ event, event, event ]
+ // For this recorder, we use the array form because Catapult will handle a
+ // missing ending bracket in this form in case we don't cleanly end the
+ // trace.
+ Write("[\n");
+ OSThread::Start("TimelineEventFileRecorder", TimelineEventFileRecorderStart,
+ reinterpret_cast<uword>(this));
+}
+
+TimelineEventFileRecorder::~TimelineEventFileRecorder() {
+ if (file_ == nullptr) return;
+
+ {
+ MonitorLocker ml(&monitor_);
+ shutting_down_ = true;
+ ml.Notify();
+ }
+
+ ASSERT(thread_id_ != OSThread::kInvalidThreadJoinId);
+ OSThread::Join(thread_id_);
+ thread_id_ = OSThread::kInvalidThreadJoinId;
+
+ TimelineEvent* event = head_;
+ while (event != nullptr) {
+ TimelineEvent* next = event->next();
+ delete event;
+ event = next;
+ }
+ head_ = tail_ = nullptr;
+
+ Write("]\n");
+ Dart_FileCloseCallback file_close = Dart::file_close_callback();
+ (*file_close)(file_);
+ file_ = nullptr;
+}
+
+void TimelineEventFileRecorder::CompleteEvent(TimelineEvent* event) {
+ if (event == nullptr) {
+ return;
+ }
+ if (file_ == nullptr) {
+ delete event;
+ return;
+ }
+
+ MonitorLocker ml(&monitor_);
+ ASSERT(!shutting_down_);
+ event->set_next(nullptr);
+ if (tail_ == nullptr) {
+ head_ = tail_ = event;
+ } else {
+ tail_->set_next(event);
+ tail_ = event;
+ }
+ ml.Notify();
+}
+
+void TimelineEventFileRecorder::Drain() {
+ MonitorLocker ml(&monitor_);
+ thread_id_ = OSThread::GetCurrentThreadJoinId(OSThread::Current());
+ while (!shutting_down_) {
+ if (head_ == nullptr) {
+ ml.Wait();
+ continue; // Recheck empty and shutting down.
+ }
+ TimelineEvent* event = head_;
+ TimelineEvent* next = event->next();
+ head_ = next;
+ if (next == nullptr) {
+ tail_ = nullptr;
+ }
+ ml.Exit();
+ {
+ JSONWriter writer;
+ if (first_) {
+ first_ = false;
+ } else {
+ writer.buffer()->AddChar(',');
+ }
+ event->PrintJSON(&writer);
+ char* output = NULL;
+ intptr_t output_length = 0;
+ writer.Steal(&output, &output_length);
+ Write(output, output_length);
+ free(output);
+ delete event;
+ }
+ ml.Enter();
+ }
+}
+
+void TimelineEventFileRecorder::Write(const char* buffer, intptr_t len) {
+ Dart_FileWriteCallback file_write = Dart::file_write_callback();
+ (*file_write)(buffer, len, file_);
+}
+
TimelineEventEndlessRecorder::TimelineEventEndlessRecorder()
: head_(nullptr), tail_(nullptr), block_index_(0) {}
diff --git a/runtime/vm/timeline.h b/runtime/vm/timeline.h
index 6d5c062..8f1df56 100644
--- a/runtime/vm/timeline.h
+++ b/runtime/vm/timeline.h
@@ -36,6 +36,7 @@
class JSONArray;
class JSONObject;
class JSONStream;
+class JSONWriter;
class Object;
class ObjectPointerVisitor;
class Isolate;
@@ -49,6 +50,7 @@
#define CALLBACK_RECORDER_NAME "Callback"
#define ENDLESS_RECORDER_NAME "Endless"
+#define FILE_RECORDER_NAME "File"
#define FUCHSIA_RECORDER_NAME "Fuchsia"
#define MACOS_RECORDER_NAME "Macos"
#define RING_RECORDER_NAME "Ring"
@@ -392,6 +394,7 @@
#ifndef PRODUCT
void PrintJSON(JSONStream* stream) const;
#endif
+ void PrintJSON(JSONWriter* writer) const;
ThreadId thread() const { return thread_; }
@@ -456,6 +459,13 @@
intptr_t arguments_length() const { return arguments_.length(); }
+ TimelineEvent* next() const {
+ return next_;
+ }
+ void set_next(TimelineEvent* next) {
+ next_ = next;
+ }
+
private:
void StreamInit(TimelineStream* stream) { stream_ = stream; }
void Init(EventType event_type, const char* label);
@@ -518,6 +528,7 @@
ThreadId thread_;
Dart_Port isolate_id_;
uint64_t isolate_group_id_;
+ TimelineEvent* next_;
friend class TimelineEventRecorder;
friend class TimelineEventEndlessRecorder;
@@ -1006,6 +1017,31 @@
};
#endif // defined(DART_HOST_OS_MACOS)
+class TimelineEventFileRecorder : public TimelineEventPlatformRecorder {
+ public:
+ explicit TimelineEventFileRecorder(const char* path);
+ virtual ~TimelineEventFileRecorder();
+
+ const char* name() const { return FILE_RECORDER_NAME; }
+ intptr_t Size() { return 0; }
+
+ void Drain();
+
+ private:
+ void CompleteEvent(TimelineEvent* event);
+ void OnEvent(TimelineEvent* event) { UNREACHABLE(); }
+ void Write(const char* buffer) { Write(buffer, strlen(buffer)); }
+ void Write(const char* buffer, intptr_t len);
+
+ Monitor monitor_;
+ TimelineEvent* head_;
+ TimelineEvent* tail_;
+ void* file_;
+ bool first_;
+ bool shutting_down_;
+ ThreadJoinId thread_id_;
+};
+
class DartTimelineEventHelpers : public AllStatic {
public:
static void ReportTaskEvent(Thread* thread,