[vm/concurrency] Add number of objects / bytes copied to timeline events for inter-isolate messages

Closes https://github.com/dart-lang/sdk/issues/48591

TEST=vm/dart{,_2}/isolates/fast_object_copy_timeline_test

Change-Id: I1de3a6f0d8a31450e45f689e0d67358285204a71
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/245167
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Ryan Macnak <rmacnak@google.com>
Reviewed-by: Alexander Aprelev <aam@google.com>
diff --git a/runtime/tests/vm/dart/isolates/fast_object_copy_timeline_test.dart b/runtime/tests/vm/dart/isolates/fast_object_copy_timeline_test.dart
new file mode 100644
index 0000000..72b4932
--- /dev/null
+++ b/runtime/tests/vm/dart/isolates/fast_object_copy_timeline_test.dart
@@ -0,0 +1,120 @@
+// 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.
+
+// VMOptions=--no-enable-fast-object-copy
+// VMOptions=--enable-fast-object-copy
+
+import 'dart:io';
+import 'dart:isolate';
+import 'dart:ffi';
+import 'dart:typed_data';
+
+import 'package:expect/expect.dart';
+
+import '../timeline_utils.dart';
+
+final int wordSize = sizeOf<IntPtr>();
+final bool useCompressedPointers =
+    wordSize == 8 && (Platform.isAndroid || Platform.isIOS);
+
+final int kAllocationSize = 2 * wordSize;
+final int headerSize = wordSize;
+final int slotSize = useCompressedPointers ? 4 : wordSize;
+
+final int objectBaseSize = headerSize;
+final int arrayBaseSize = headerSize + 2 * slotSize;
+final int typedDataBaseSize = headerSize + 2 * wordSize;
+
+int objectSize(int slots) => toAllocationSize(headerSize + slots * slotSize);
+int arraySize(int elements) =>
+    toAllocationSize(headerSize + 2 * slotSize + elements * slotSize);
+int typedDataSize(int length) =>
+    toAllocationSize(headerSize + 2 * wordSize + length);
+
+int toAllocationSize(int value) =>
+    (value + kAllocationSize - 1) & ~(kAllocationSize - 1);
+
+Future main(List<String> args) async {
+  if (const bool.fromEnvironment('dart.vm.product')) {
+    return; // No timeline support
+  }
+
+  if (args.contains('--child')) {
+    final rp = ReceivePort();
+    final sendPort = rp.sendPort;
+
+    sendPort.send(Object());
+    sendPort.send(List<dynamic>.filled(2, null)
+      ..[0] = Object()
+      ..[1] = Object());
+    sendPort.send(Uint8List(11));
+
+    rp.close();
+    return;
+  }
+
+  final timelineEvents = await runAndCollectTimeline('Isolate', ['--child']);
+  final mainIsolateId = findMainIsolateId(timelineEvents);
+  final copyOperations = getCopyOperations(timelineEvents, mainIsolateId);
+
+  // We're only interested in the last 3 operations (which are done by the
+  // application).
+  copyOperations.removeRange(0, copyOperations.length - 3);
+
+  Expect.equals(1, copyOperations[0].objectsCopied);
+  Expect.equals(3, copyOperations[1].objectsCopied);
+  Expect.equals(1, copyOperations[2].objectsCopied);
+
+  Expect.equals(objectSize(0), copyOperations[0].bytesCopied);
+  Expect.equals(
+      arraySize(2) + 2 * objectSize(0), copyOperations[1].bytesCopied);
+  Expect.equals(typedDataSize(11), copyOperations[2].bytesCopied);
+}
+
+List<ObjectCopyOperation> getCopyOperations(
+    List<TimelineEvent> events, String isolateId) {
+  final copyOperations = <ObjectCopyOperation>[];
+
+  int? startTs = null;
+  int? startTts = null;
+
+  for (final e in events) {
+    if (e.isolateId != isolateId) continue;
+    if (e.name != 'CopyMutableObjectGraph') continue;
+
+    if (startTts != null) {
+      if (!e.isEnd) throw 'Missing end of copy event';
+
+      final us = e.ts - startTs!;
+      final threadUs = e.tts! - startTts;
+      copyOperations.add(ObjectCopyOperation(
+          us,
+          threadUs,
+          int.parse(e.args['AllocatedBytes']!),
+          int.parse(e.args['CopiedObjects']!)));
+
+      startTs = null;
+      startTts = null;
+      continue;
+    }
+
+    if (!e.isStart) throw 'Expected end of copy event';
+    startTs = e.ts;
+    startTts = e.tts;
+  }
+  return copyOperations;
+}
+
+class ObjectCopyOperation {
+  final int us;
+  final int threadUs;
+  final int bytesCopied;
+  final int objectsCopied;
+
+  ObjectCopyOperation(
+      this.us, this.threadUs, this.bytesCopied, this.objectsCopied);
+
+  String toString() =>
+      'ObjectCopyOperation($us, $threadUs, $bytesCopied, $objectsCopied)';
+}
diff --git a/runtime/tests/vm/dart/snapshot_test_helper.dart b/runtime/tests/vm/dart/snapshot_test_helper.dart
index 80d3344..a6890dd 100644
--- a/runtime/tests/vm/dart/snapshot_test_helper.dart
+++ b/runtime/tests/vm/dart/snapshot_test_helper.dart
@@ -124,7 +124,7 @@
 withTempDir(Future fun(String dir)) async {
   final Directory tempDir = Directory.systemTemp.createTempSync();
   try {
-    await fun(tempDir.path);
+    return await fun(tempDir.path);
   } finally {
     tempDir.deleteSync(recursive: true);
   }
diff --git a/runtime/tests/vm/dart/timeline_recorder_file_test.dart b/runtime/tests/vm/dart/timeline_recorder_file_test.dart
index 851a68a..7310754 100644
--- a/runtime/tests/vm/dart/timeline_recorder_file_test.dart
+++ b/runtime/tests/vm/dart/timeline_recorder_file_test.dart
@@ -2,13 +2,9 @@
 // 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";
+import "timeline_utils.dart";
 
 main(List<String> args) async {
   if (const bool.fromEnvironment("dart.vm.product")) {
@@ -21,48 +17,20 @@
     return;
   }
 
-  await withTempDir((String tmp) async {
-    final String timelinePath = path.join(tmp, "timeline.json");
-    final p = await Process.run(Platform.executable, [
-      ...Platform.executableArguments,
-      "--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 timelineEvents =
+      await runAndCollectTimeline('VM,Isolate,GC,Compiler', ['--child']);
 
-    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;
-      }
+  bool foundExampleStart = false;
+  bool foundExampleFinish = false;
+  for (final event in timelineEvents) {
+    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";
-  });
+  if (foundExampleStart) throw "Missing test start event";
+  if (foundExampleFinish) throw "Missing test finish event";
 }
diff --git a/runtime/tests/vm/dart/timeline_utils.dart b/runtime/tests/vm/dart/timeline_utils.dart
new file mode 100644
index 0000000..cf0cddc
--- /dev/null
+++ b/runtime/tests/vm/dart/timeline_utils.dart
@@ -0,0 +1,92 @@
+// 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 'package:path/path.dart' as path;
+
+import 'snapshot_test_helper.dart';
+
+Future<List<TimelineEvent>> runAndCollectTimeline(
+    String streams, List<String> args) async {
+  return await withTempDir((String tmp) async {
+    final String timelinePath = path.join(tmp, 'timeline.json');
+    final p = await Process.run(Platform.executable, [
+      ...Platform.executableArguments,
+      '--trace_timeline',
+      '--timeline_recorder=file:$timelinePath',
+      '--timeline_streams=$streams',
+      Platform.script.toFilePath(),
+      ...args,
+    ]);
+    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';
+
+    return parseTimeline(timeline);
+  });
+}
+
+List<TimelineEvent> parseTimeline(List l) {
+  final events = <TimelineEvent>[];
+
+  for (final event in l) {
+    events.add(TimelineEvent.from(event));
+  }
+  return events;
+}
+
+String findMainIsolateId(List<TimelineEvent> events) {
+  return events
+      .firstWhere((e) =>
+          e.name == 'InitializeIsolate' && e.args['isolateName'] == 'main')
+      .isolateId!;
+}
+
+class TimelineEvent {
+  final String name;
+  final String cat;
+  final int tid;
+  final int pid;
+  final int ts;
+  final int? tts;
+  final String ph;
+  final Map<String, String> args;
+
+  TimelineEvent._(this.name, this.cat, this.tid, this.pid, this.ts, this.tts,
+      this.ph, this.args);
+
+  factory TimelineEvent.from(Map m) {
+    return TimelineEvent._(
+      m['name'] as String,
+      m['cat'] as String,
+      m['tid'] as int,
+      m['pid'] as int,
+      m['ts'] as int,
+      m['tts'] as int?,
+      m['ph'] as String,
+      m['args'].cast<String, String>(),
+    );
+  }
+
+  bool get isStart => ph == 'B';
+  bool get isEnd => ph == 'E';
+
+  String? get isolateId => args['isolateId'];
+
+  String toString() =>
+      'TimelineEvent($name, $cat, $tid, $pid, $ts, $tts, $ph, $args)';
+}
diff --git a/runtime/tests/vm/dart_2/isolates/fast_object_copy_timeline_test.dart b/runtime/tests/vm/dart_2/isolates/fast_object_copy_timeline_test.dart
new file mode 100644
index 0000000..9030c78
--- /dev/null
+++ b/runtime/tests/vm/dart_2/isolates/fast_object_copy_timeline_test.dart
@@ -0,0 +1,122 @@
+// 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
+
+// VMOptions=--no-enable-fast-object-copy
+// VMOptions=--enable-fast-object-copy
+
+import 'dart:io';
+import 'dart:isolate';
+import 'dart:ffi';
+import 'dart:typed_data';
+
+import 'package:expect/expect.dart';
+
+import '../timeline_utils.dart';
+
+final int wordSize = sizeOf<IntPtr>();
+final bool useCompressedPointers =
+    wordSize == 8 && (Platform.isAndroid || Platform.isIOS);
+
+final int kAllocationSize = 2 * wordSize;
+final int headerSize = wordSize;
+final int slotSize = useCompressedPointers ? 4 : wordSize;
+
+final int objectBaseSize = headerSize;
+final int arrayBaseSize = headerSize + 2 * slotSize;
+final int typedDataBaseSize = headerSize + 2 * wordSize;
+
+int objectSize(int slots) => toAllocationSize(headerSize + slots * slotSize);
+int arraySize(int elements) =>
+    toAllocationSize(headerSize + 2 * slotSize + elements * slotSize);
+int typedDataSize(int length) =>
+    toAllocationSize(headerSize + 2 * wordSize + length);
+
+int toAllocationSize(int value) =>
+    (value + kAllocationSize - 1) & ~(kAllocationSize - 1);
+
+Future main(List<String> args) async {
+  if (const bool.fromEnvironment('dart.vm.product')) {
+    return; // No timeline support
+  }
+
+  if (args.contains('--child')) {
+    final rp = ReceivePort();
+    final sendPort = rp.sendPort;
+
+    sendPort.send(Object());
+    sendPort.send(List<dynamic>.filled(2, null)
+      ..[0] = Object()
+      ..[1] = Object());
+    sendPort.send(Uint8List(11));
+
+    rp.close();
+    return;
+  }
+
+  final timelineEvents = await runAndCollectTimeline('Isolate', ['--child']);
+  final mainIsolateId = findMainIsolateId(timelineEvents);
+  final copyOperations = getCopyOperations(timelineEvents, mainIsolateId);
+
+  // We're only interested in the last 3 operations (which are done by the
+  // application).
+  copyOperations.removeRange(0, copyOperations.length - 3);
+
+  Expect.equals(1, copyOperations[0].objectsCopied);
+  Expect.equals(3, copyOperations[1].objectsCopied);
+  Expect.equals(1, copyOperations[2].objectsCopied);
+
+  Expect.equals(objectSize(0), copyOperations[0].bytesCopied);
+  Expect.equals(
+      arraySize(2) + 2 * objectSize(0), copyOperations[1].bytesCopied);
+  Expect.equals(typedDataSize(11), copyOperations[2].bytesCopied);
+}
+
+List<ObjectCopyOperation> getCopyOperations(
+    List<TimelineEvent> events, String isolateId) {
+  final copyOperations = <ObjectCopyOperation>[];
+
+  int startTs = null;
+  int startTts = null;
+
+  for (final e in events) {
+    if (e.isolateId != isolateId) continue;
+    if (e.name != 'CopyMutableObjectGraph') continue;
+
+    if (startTts != null) {
+      if (!e.isEnd) throw 'Missing end of copy event';
+
+      final us = e.ts - startTs;
+      final threadUs = e.tts - startTts;
+      copyOperations.add(ObjectCopyOperation(
+          us,
+          threadUs,
+          int.parse(e.args['AllocatedBytes']),
+          int.parse(e.args['CopiedObjects'])));
+
+      startTs = null;
+      startTts = null;
+      continue;
+    }
+
+    if (!e.isStart) throw 'Expected end of copy event';
+    startTs = e.ts;
+    startTts = e.tts;
+  }
+  return copyOperations;
+}
+
+class ObjectCopyOperation {
+  final int us;
+  final int threadUs;
+  final int bytesCopied;
+  final int objectsCopied;
+
+  ObjectCopyOperation(
+      this.us, this.threadUs, this.bytesCopied, this.objectsCopied);
+
+  String toString() =>
+      'ObjectCopyOperation($us, $threadUs, $bytesCopied, $objectsCopied)';
+}
diff --git a/runtime/tests/vm/dart_2/snapshot_test_helper.dart b/runtime/tests/vm/dart_2/snapshot_test_helper.dart
index b57d04d..d790c4a 100644
--- a/runtime/tests/vm/dart_2/snapshot_test_helper.dart
+++ b/runtime/tests/vm/dart_2/snapshot_test_helper.dart
@@ -126,7 +126,7 @@
 withTempDir(Future fun(String dir)) async {
   final Directory tempDir = Directory.systemTemp.createTempSync();
   try {
-    await fun(tempDir.path);
+    return await fun(tempDir.path);
   } finally {
     tempDir.deleteSync(recursive: true);
   }
diff --git a/runtime/tests/vm/dart_2/timeline_recorder_file_test.dart b/runtime/tests/vm/dart_2/timeline_recorder_file_test.dart
index e49877c..e308ebd 100644
--- a/runtime/tests/vm/dart_2/timeline_recorder_file_test.dart
+++ b/runtime/tests/vm/dart_2/timeline_recorder_file_test.dart
@@ -4,13 +4,9 @@
 
 // @dart = 2.9
 
-import "dart:io";
-import "dart:convert";
 import "dart:developer";
 
-import "package:path/path.dart" as path;
-
-import "snapshot_test_helper.dart";
+import "timeline_utils.dart";
 
 main(List<String> args) async {
   if (const bool.fromEnvironment("dart.vm.product")) {
@@ -23,48 +19,25 @@
     return;
   }
 
-  await withTempDir((String tmp) async {
-    final String timelinePath = path.join(tmp, "timeline.json");
-    final p = await Process.run(Platform.executable, [
-      ...Platform.executableArguments,
-      "--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 timelineEvents =
+      await runAndCollectTimeline('VM,Isolate,GC,Compiler', ['--child']);
 
-    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;
-      }
+  bool foundExampleStart = false;
+  bool foundExampleFinish = false;
+  for (final event in timelineEvents) {
+    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";
-  });
+  if (foundExampleStart) throw "Missing test start event";
+  if (foundExampleFinish) throw "Missing test finish event";
 }
diff --git a/runtime/tests/vm/dart_2/timeline_utils.dart b/runtime/tests/vm/dart_2/timeline_utils.dart
new file mode 100644
index 0000000..3041293
--- /dev/null
+++ b/runtime/tests/vm/dart_2/timeline_utils.dart
@@ -0,0 +1,94 @@
+// 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 'package:path/path.dart' as path;
+
+import 'snapshot_test_helper.dart';
+
+Future<List<TimelineEvent>> runAndCollectTimeline(
+    String streams, List<String> args) async {
+  return await withTempDir((String tmp) async {
+    final String timelinePath = path.join(tmp, 'timeline.json');
+    final p = await Process.run(Platform.executable, [
+      ...Platform.executableArguments,
+      '--trace_timeline',
+      '--timeline_recorder=file:$timelinePath',
+      '--timeline_streams=$streams',
+      Platform.script.toFilePath(),
+      ...args,
+    ]);
+    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';
+
+    return parseTimeline(timeline);
+  });
+}
+
+List<TimelineEvent> parseTimeline(List l) {
+  final events = <TimelineEvent>[];
+
+  for (final event in l) {
+    events.add(TimelineEvent.from(event));
+  }
+  return events;
+}
+
+String findMainIsolateId(List<TimelineEvent> events) {
+  return events
+      .firstWhere((e) =>
+          e.name == 'InitializeIsolate' && e.args['isolateName'] == 'main')
+      .args['isolateId'];
+}
+
+class TimelineEvent {
+  final String name;
+  final String cat;
+  final int tid;
+  final int pid;
+  final int ts;
+  final int tts;
+  final String ph;
+  final Map<String, String> args;
+
+  TimelineEvent._(this.name, this.cat, this.tid, this.pid, this.ts, this.tts,
+      this.ph, this.args);
+
+  factory TimelineEvent.from(Map m) {
+    return TimelineEvent._(
+      m['name'] as String,
+      m['cat'] as String,
+      m['tid'] as int,
+      m['pid'] as int,
+      m['ts'] as int,
+      m['tts'] as int,
+      m['ph'] as String,
+      m['args'].cast<String, String>(),
+    );
+  }
+
+  bool get isStart => ph == 'B';
+  bool get isEnd => ph == 'E';
+
+  String get isolateId => args['isolateId'];
+
+  String toString() =>
+      'TimelineEvent($name, $cat, $tid, $pid, $ts, $tts, $ph, $args)';
+}
diff --git a/runtime/vm/object_graph_copy.cc b/runtime/vm/object_graph_copy.cc
index 86d3c8e..43c35b3 100644
--- a/runtime/vm/object_graph_copy.cc
+++ b/runtime/vm/object_graph_copy.cc
@@ -12,6 +12,7 @@
 #include "vm/object_store.h"
 #include "vm/snapshot.h"
 #include "vm/symbols.h"
+#include "vm/timeline.h"
 
 #define Z zone_
 
@@ -422,7 +423,7 @@
     return raw_from_to_[id + 1];
   }
 
-  void Insert(ObjectPtr from, ObjectPtr to) {
+  void Insert(ObjectPtr from, ObjectPtr to, intptr_t size) {
     ASSERT(ForwardedObject(from) == Marker());
     ASSERT(raw_from_to_.length() == raw_from_to_.length());
     const auto id = raw_from_to_.length();
@@ -430,6 +431,7 @@
     raw_from_to_.Resize(id + 2);
     raw_from_to_[id] = from;
     raw_from_to_[id + 1] = to;
+    allocated_bytes += size;
   }
 
   void AddTransferable(TransferableTypedDataPtr from,
@@ -460,6 +462,7 @@
   GrowableArray<WeakPropertyPtr> raw_weak_properties_;
   GrowableArray<WeakReferencePtr> raw_weak_references_;
   intptr_t fill_cursor_ = 0;
+  intptr_t allocated_bytes = 0;
 
   DISALLOW_COPY_AND_ASSIGN(FastForwardMap);
 };
@@ -482,13 +485,14 @@
     return from_to_[id + 1]->ptr();
   }
 
-  void Insert(ObjectPtr from, ObjectPtr to) {
+  void Insert(ObjectPtr from, ObjectPtr to, intptr_t size) {
     ASSERT(ForwardedObject(from) == Marker());
     const auto id = from_to_.length();
     SetObjectId(from, id);
     from_to_.Resize(id + 2);
     from_to_[id] = &Object::Handle(Z, from);
     from_to_[id + 1] = &Object::Handle(Z, to);
+    allocated_bytes += size;
   }
 
   void AddTransferable(const TransferableTypedData& from,
@@ -541,6 +545,7 @@
   GrowableArray<const WeakProperty*> weak_properties_;
   GrowableArray<const WeakReference*> weak_references_;
   intptr_t fill_cursor_ = 0;
+  intptr_t allocated_bytes = 0;
 
   DISALLOW_COPY_AND_ASSIGN(SlowForwardMap);
 };
@@ -780,7 +785,7 @@
       const uword alloc = new_space_->TryAllocate(thread_, size);
       if (alloc != 0) {
         ObjectPtr to(reinterpret_cast<UntaggedObject*>(alloc));
-        fast_forward_map_.Insert(from, to);
+        fast_forward_map_.Insert(from, to, size);
 
         if (IsExternalTypedDataClassId(cid)) {
           SetNewSpaceTaggingWord(to, cid, header_size);
@@ -986,7 +991,7 @@
       size = from.ptr().untag()->HeapSize();
     }
     ObjectPtr to = AllocateObject(cid, size);
-    slow_forward_map_.Insert(from.ptr(), to);
+    slow_forward_map_.Insert(from.ptr(), to, size);
     UpdateLengthField(cid, from.ptr(), to);
     if (cid == kArrayCid && !Heap::IsAllocatableInNewSpace(size)) {
       to.untag()->SetCardRememberedBitUnsynchronized();
@@ -1847,6 +1852,10 @@
     return result.ptr();
   }
 
+  intptr_t allocated_bytes() { return allocated_bytes_; }
+
+  intptr_t copied_objects() { return copied_objects_; }
+
  private:
   ObjectPtr CopyObjectGraphInternal(const Object& root,
                                     const char* volatile* exception_msg) {
@@ -1885,6 +1894,11 @@
             result_array.SetAt(2, fast_object_copy_.tmp_);
             HandlifyExternalTypedData();
             HandlifyTransferables();
+            allocated_bytes_ =
+                fast_object_copy_.fast_forward_map_.allocated_bytes;
+            copied_objects_ =
+                fast_object_copy_.fast_forward_map_.fill_cursor_ / 2 -
+                /*null_entry=*/1;
             return result_array.ptr();
           }
 
@@ -1924,6 +1938,9 @@
     result_array.SetAt(0, result);
     result_array.SetAt(1, slow_object_copy_.objects_to_rehash_);
     result_array.SetAt(2, slow_object_copy_.expandos_to_rehash_);
+    allocated_bytes_ = slow_object_copy_.slow_forward_map_.allocated_bytes;
+    copied_objects_ =
+        slow_object_copy_.slow_forward_map_.fill_cursor_ / 2 - /*null_entry=*/1;
     return result_array.ptr();
   }
 
@@ -1940,6 +1957,7 @@
     HandlifyExpandosToReHash();
     HandlifyFromToObjects();
     slow_forward_map.fill_cursor_ = fast_forward_map.fill_cursor_;
+    slow_forward_map.allocated_bytes = fast_forward_map.allocated_bytes;
   }
 
   void MakeUninitializedNewSpaceObjectsGCSafe() {
@@ -2029,12 +2047,23 @@
   Zone* zone_;
   FastObjectCopy fast_object_copy_;
   SlowObjectCopy slow_object_copy_;
+  intptr_t copied_objects_ = 0;
+  intptr_t allocated_bytes_ = 0;
 };
 
 ObjectPtr CopyMutableObjectGraph(const Object& object) {
   auto thread = Thread::Current();
+  TIMELINE_DURATION(thread, Isolate, "CopyMutableObjectGraph");
   ObjectGraphCopier copier(thread);
-  return copier.CopyObjectGraph(object);
+  ObjectPtr result = copier.CopyObjectGraph(object);
+#if defined(SUPPORT_TIMELINE)
+  if (tbes.enabled()) {
+    tbes.SetNumArguments(2);
+    tbes.FormatArgument(0, "CopiedObjects", "%" Pd, copier.copied_objects());
+    tbes.FormatArgument(1, "AllocatedBytes", "%" Pd, copier.allocated_bytes());
+  }
+#endif
+  return result;
 }
 
 }  // namespace dart