Version 2.13.0-137.0.dev

Merge commit 'dc62c9c3c22453277d357cfcc7f9b010840ac5ca' into 'dev'
diff --git a/runtime/tests/vm/dart/use_trace_precompiler_flag_test.dart b/runtime/tests/vm/dart/use_trace_precompiler_flag_test.dart
index 7d41a71..7ea2d60 100644
--- a/runtime/tests/vm/dart/use_trace_precompiler_flag_test.dart
+++ b/runtime/tests/vm/dart/use_trace_precompiler_flag_test.dart
@@ -67,47 +67,44 @@
         buffer.write(retentionFlags[j]);
         flags.add(buffer.toString());
       }
-      await testTracePrecompiler(scriptDill, flags);
+      await testTracePrecompiler(tempDir, scriptDill, flags);
     }
   });
 }
 
-const _jsonHeaders = {'JSON for function decisions: '};
-
-Future<void> testTracePrecompiler(String scriptDill, List<String> flags) async {
-  final result = await runHelper(genSnapshot, <String>[
+Future<void> testTracePrecompiler(
+    String tempDir, String scriptDill, List<String> flags) async {
+  final reasonsFile = path.join(tempDir, 'reasons.json');
+  final snapshot = path.join(tempDir, 'snapshot.so');
+  final result = await run(genSnapshot, <String>[
     ...flags,
-    '--trace-precompiler',
+    '--write-retained-reasons-to=$reasonsFile',
     '--snapshot-kind=app-aot-elf',
-    '--elf=snapshot.so',
+    '--elf=$snapshot',
     scriptDill,
   ]);
 
-  Expect.equals(result.exitCode, 0);
-
-  // Tracing output is on stderr.
-  Expect.isTrue(result.stdout.isEmpty);
-  Expect.isTrue(result.stderr.isNotEmpty);
-
-  final seenHeaders = <String>{};
-  final lines =
-      Stream.value(result.stderr as String).transform(const LineSplitter());
-  await for (final s in lines) {
-    for (final header in _jsonHeaders) {
-      if (s.startsWith(header)) {
-        // We only expect a single instance of each header.
-        Expect.isFalse(seenHeaders.contains(header),
-            'multiple instances of \"$header\" seen');
-        seenHeaders.add(header);
-        final j = s.substring(header.length);
-        // For now, just test that the JSON parses and that we get back a list.
-        Expect.isTrue(json.decode(j) is List, 'not a list of decisions');
+  final stream = Stream.fromFuture(File(reasonsFile).readAsString());
+  final decisionsJson = await json.decoder.bind(stream).first;
+  Expect.isTrue(decisionsJson is List, 'not a list of decisions');
+  Expect.isTrue((decisionsJson as List).every((o) => o is Map),
+      'not a list of decision objects');
+  final decisions = (decisionsJson as List).map((o) => o as Map);
+  for (final m in decisions) {
+    Expect.isTrue(m.containsKey("name"), 'no name field in decision');
+    Expect.isTrue(m["name"] is String, 'name field is not a string');
+    Expect.isTrue(m.containsKey("type"), 'no type field in decision');
+    Expect.isTrue(m["type"] is String, 'type field is not a string');
+    Expect.isTrue(m.containsKey("retained"), 'no retained field in decision');
+    Expect.isTrue(m["retained"] is bool, 'retained field is not a boolean');
+    if (m["retained"] as bool) {
+      Expect.isTrue(m.containsKey("reasons"), 'no reasons field in decision');
+      Expect.isTrue(m["reasons"] is List, 'reasons field is not a list');
+      final reasons = m["reasons"] as List;
+      Expect.isFalse(reasons.isEmpty, 'reasons list should not be empty');
+      for (final o in reasons) {
+        Expect.isTrue(o is String, 'reason is not a string');
       }
     }
   }
-  // Check that all headers were seen in the output.
-  for (final header in _jsonHeaders) {
-    Expect.isTrue(
-        seenHeaders.contains(header), 'no instance of \"$header\" seen');
-  }
 }
diff --git a/runtime/tests/vm/dart_2/use_trace_precompiler_flag_test.dart b/runtime/tests/vm/dart_2/use_trace_precompiler_flag_test.dart
index 7d41a71..7ea2d60 100644
--- a/runtime/tests/vm/dart_2/use_trace_precompiler_flag_test.dart
+++ b/runtime/tests/vm/dart_2/use_trace_precompiler_flag_test.dart
@@ -67,47 +67,44 @@
         buffer.write(retentionFlags[j]);
         flags.add(buffer.toString());
       }
-      await testTracePrecompiler(scriptDill, flags);
+      await testTracePrecompiler(tempDir, scriptDill, flags);
     }
   });
 }
 
-const _jsonHeaders = {'JSON for function decisions: '};
-
-Future<void> testTracePrecompiler(String scriptDill, List<String> flags) async {
-  final result = await runHelper(genSnapshot, <String>[
+Future<void> testTracePrecompiler(
+    String tempDir, String scriptDill, List<String> flags) async {
+  final reasonsFile = path.join(tempDir, 'reasons.json');
+  final snapshot = path.join(tempDir, 'snapshot.so');
+  final result = await run(genSnapshot, <String>[
     ...flags,
-    '--trace-precompiler',
+    '--write-retained-reasons-to=$reasonsFile',
     '--snapshot-kind=app-aot-elf',
-    '--elf=snapshot.so',
+    '--elf=$snapshot',
     scriptDill,
   ]);
 
-  Expect.equals(result.exitCode, 0);
-
-  // Tracing output is on stderr.
-  Expect.isTrue(result.stdout.isEmpty);
-  Expect.isTrue(result.stderr.isNotEmpty);
-
-  final seenHeaders = <String>{};
-  final lines =
-      Stream.value(result.stderr as String).transform(const LineSplitter());
-  await for (final s in lines) {
-    for (final header in _jsonHeaders) {
-      if (s.startsWith(header)) {
-        // We only expect a single instance of each header.
-        Expect.isFalse(seenHeaders.contains(header),
-            'multiple instances of \"$header\" seen');
-        seenHeaders.add(header);
-        final j = s.substring(header.length);
-        // For now, just test that the JSON parses and that we get back a list.
-        Expect.isTrue(json.decode(j) is List, 'not a list of decisions');
+  final stream = Stream.fromFuture(File(reasonsFile).readAsString());
+  final decisionsJson = await json.decoder.bind(stream).first;
+  Expect.isTrue(decisionsJson is List, 'not a list of decisions');
+  Expect.isTrue((decisionsJson as List).every((o) => o is Map),
+      'not a list of decision objects');
+  final decisions = (decisionsJson as List).map((o) => o as Map);
+  for (final m in decisions) {
+    Expect.isTrue(m.containsKey("name"), 'no name field in decision');
+    Expect.isTrue(m["name"] is String, 'name field is not a string');
+    Expect.isTrue(m.containsKey("type"), 'no type field in decision');
+    Expect.isTrue(m["type"] is String, 'type field is not a string');
+    Expect.isTrue(m.containsKey("retained"), 'no retained field in decision');
+    Expect.isTrue(m["retained"] is bool, 'retained field is not a boolean');
+    if (m["retained"] as bool) {
+      Expect.isTrue(m.containsKey("reasons"), 'no reasons field in decision');
+      Expect.isTrue(m["reasons"] is List, 'reasons field is not a list');
+      final reasons = m["reasons"] as List;
+      Expect.isFalse(reasons.isEmpty, 'reasons list should not be empty');
+      for (final o in reasons) {
+        Expect.isTrue(o is String, 'reason is not a string');
       }
     }
   }
-  // Check that all headers were seen in the output.
-  for (final header in _jsonHeaders) {
-    Expect.isTrue(
-        seenHeaders.contains(header), 'no instance of \"$header\" seen');
-  }
 }
diff --git a/runtime/vm/compiler/aot/precompiler.cc b/runtime/vm/compiler/aot/precompiler.cc
index 0beffef..9f62f59 100644
--- a/runtime/vm/compiler/aot/precompiler.cc
+++ b/runtime/vm/compiler/aot/precompiler.cc
@@ -5,6 +5,7 @@
 #include "vm/compiler/aot/precompiler.h"
 
 #include "platform/unicode.h"
+#include "platform/utils.h"
 #include "vm/canonical_tables.h"
 #include "vm/class_finalizer.h"
 #include "vm/closure_functions_cache.h"
@@ -67,6 +68,10 @@
     max_speculative_inlining_attempts,
     1,
     "Max number of attempts with speculative inlining (precompilation only)");
+DEFINE_FLAG(charp,
+            write_retained_reasons_to,
+            nullptr,
+            "Print reasons for retaining objects to the given file");
 
 DECLARE_FLAG(bool, print_flow_graph);
 DECLARE_FLAG(bool, print_flow_graph_optimized);
@@ -109,6 +114,9 @@
   // The object is the initializer for a static field.
   static constexpr const char* kStaticFieldInitializer =
       "static field initializer";
+  // The object is the initializer for a instance field.
+  static constexpr const char* kInstanceFieldInitializer =
+      "instance field initializer";
   // The object is the initializer for a late field.
   static constexpr const char* kLateFieldInitializer = "late field initializer";
   // The object is an implicit getter.
@@ -136,6 +144,149 @@
   static constexpr const char* kEntryPointPragma = "entry point pragma";
 };
 
+class RetainedReasonsWriter : public ValueObject {
+ public:
+  explicit RetainedReasonsWriter(Zone* zone)
+      : zone_(zone), retained_reasons_map_(zone) {}
+
+  void Init(const char* filename) {
+    if (filename == nullptr) return;
+    const auto file_open = Dart::file_open_callback();
+    if (file_open == nullptr) return;
+
+    const auto file = file_open(filename, /*write=*/true);
+    if (file == nullptr) {
+      OS::PrintErr("Failed to open file %s\n", filename);
+      return;
+    }
+
+    file_ = file;
+    // We open the array here so that we can also print some objects to the
+    // JSON as we go, instead of requiring all information be collected
+    // and printed at one point. This avoids having to keep otherwise
+    // unneeded information around.
+    writer_.OpenArray();
+  }
+
+  void AddDropped(const Object& obj) {
+    if (HasReason(obj)) {
+      FATAL("dropped object has reasons to retain");
+    }
+    writer_.OpenObject();
+    WriteRetainedObjectSpecificFields(obj);
+    writer_.PrintPropertyBool("retained", false);
+    writer_.CloseObject();
+  }
+
+  bool HasReason(const Object& obj) const {
+    return retained_reasons_map_.HasKey(&obj);
+  }
+
+  void AddReason(const Object& obj, const char* reason) {
+    if (auto const kv = retained_reasons_map_.Lookup(&obj)) {
+      if (kv->value->Lookup(reason) == nullptr) {
+        kv->value->Insert(reason);
+      }
+      return;
+    }
+    auto const key = &Object::ZoneHandle(zone_, obj.ptr());
+    auto const value = new (zone_) ZoneCStringSet(zone_);
+    value->Insert(reason);
+    retained_reasons_map_.Insert(RetainedReasonsTrait::Pair(key, value));
+  }
+
+  // Finalizes the JSON output and writes it.
+  void Write() {
+    if (file_ == nullptr) return;
+
+    // Add all the objects for which we have reasons to retain.
+    auto it = retained_reasons_map_.GetIterator();
+
+    for (auto kv = it.Next(); kv != nullptr; kv = it.Next()) {
+      writer_.OpenObject();
+      WriteRetainedObjectSpecificFields(*kv->key);
+      writer_.PrintPropertyBool("retained", true);
+
+      writer_.OpenArray("reasons");
+      auto it = kv->value->GetIterator();
+      for (auto cstrp = it.Next(); cstrp != nullptr; cstrp = it.Next()) {
+        ASSERT(*cstrp != nullptr);
+        writer_.PrintValue(*cstrp);
+      }
+      writer_.CloseArray();
+
+      writer_.CloseObject();
+    }
+
+    writer_.CloseArray();
+    char* output = nullptr;
+    intptr_t length = -1;
+    writer_.Steal(&output, &length);
+
+    if (const auto file_write = Dart::file_write_callback()) {
+      file_write(output, length, file_);
+    }
+
+    if (const auto file_close = Dart::file_close_callback()) {
+      file_close(file_);
+    }
+
+    free(output);
+  }
+
+ private:
+  struct RetainedReasonsTrait {
+    using Key = const Object*;
+    using Value = ZoneCStringSet*;
+
+    struct Pair {
+      Key key;
+      Value value;
+
+      Pair() : key(nullptr), value(nullptr) {}
+      Pair(Key key, Value value) : key(key), value(value) {}
+    };
+
+    static Key KeyOf(Pair kv) { return kv.key; }
+
+    static Value ValueOf(Pair kv) { return kv.value; }
+
+    static inline intptr_t Hashcode(Key key) {
+      if (key->IsFunction()) {
+        return Function::Cast(*key).Hash();
+      }
+      if (key->IsClass()) {
+        return Utils::WordHash(Class::Cast(*key).id());
+      }
+      return Utils::WordHash(key->GetClassId());
+    }
+
+    static inline bool IsKeyEqual(Pair pair, Key key) {
+      return pair.key->ptr() == key->ptr();
+    }
+  };
+
+  using RetainedReasonsMap = DirectChainedHashMap<RetainedReasonsTrait>;
+
+  void WriteRetainedObjectSpecificFields(const Object& obj) {
+    if (obj.IsFunction()) {
+      writer_.PrintProperty("type", "Function");
+      const auto& function = Function::Cast(obj);
+      writer_.PrintProperty("name",
+                            function.ToLibNamePrefixedQualifiedCString());
+      writer_.PrintProperty("kind",
+                            UntaggedFunction::KindToCString(function.kind()));
+      return;
+    }
+    FATAL("Unexpected object %s", obj.ToCString());
+  }
+
+  Zone* const zone_;
+  RetainedReasonsMap retained_reasons_map_;
+  JSONWriter writer_;
+  void* file_;
+};
+
 class PrecompileParsedFunctionHelper : public ValueObject {
  public:
   PrecompileParsedFunctionHelper(Precompiler* precompiler,
@@ -243,11 +394,11 @@
   {
     StackZone stack_zone(T);
     zone_ = stack_zone.GetZone();
+    RetainedReasonsWriter reasons_writer(zone_);
 
-    if (FLAG_trace_precompiler) {
-      // Set up the retained reasons map now that the precompiler has an
-      // appropriate zone.
-      retained_reasons_map_ = new (Z) RetainedReasonsMap(Z);
+    if (FLAG_write_retained_reasons_to != nullptr) {
+      reasons_writer.Init(FLAG_write_retained_reasons_to);
+      retained_reasons_writer_ = &reasons_writer;
     }
 
     if (FLAG_use_bare_instructions) {
@@ -428,6 +579,11 @@
     DiscardCodeObjects();
     ProgramVisitor::Dedup(T);
 
+    if (FLAG_write_retained_reasons_to != nullptr) {
+      reasons_writer.Write();
+      retained_reasons_writer_ = nullptr;
+    }
+
     zone_ = NULL;
   }
 
@@ -798,23 +954,14 @@
 }
 
 void Precompiler::AddRetainReason(const Object& obj, const char* reason) {
-  if (!FLAG_trace_precompiler || reason == nullptr) return;
-  if (auto const kv = retained_reasons_map_->Lookup(&obj)) {
-    if (kv->value->Lookup(reason) == nullptr) {
-      kv->value->Insert(reason);
-    }
-    return;
-  }
-  auto const key = &Object::ZoneHandle(Z, obj.ptr());
-  auto const value = new (Z) ZoneCStringSet(Z);
-  value->Insert(reason);
-  retained_reasons_map_->Insert(RetainedReasonsTrait::Pair(key, value));
+  if (retained_reasons_writer_ == nullptr || reason == nullptr) return;
+  retained_reasons_writer_->AddReason(obj, reason);
 }
 
 void Precompiler::AddTypesOf(const Function& function) {
   if (function.IsNull()) return;
-  if (FLAG_trace_precompiler &&
-      retained_reasons_map_->Lookup(&function) == nullptr) {
+  if (retained_reasons_writer_ != nullptr &&
+      !retained_reasons_writer_->HasReason(function)) {
     FATAL("no retaining reasons given");
   }
   if (functions_to_retain_.ContainsKey(function)) return;
@@ -1838,38 +1985,6 @@
   Code& code = Code::Handle(Z);
   Object& owner = Object::Handle(Z);
   GrowableObjectArray& retained_functions = GrowableObjectArray::Handle(Z);
-  JSONWriter json;
-
-  if (FLAG_trace_precompiler) {
-    json.OpenArray();
-  }
-
-  auto retain_function = [&](const Function& function) {
-    if (FLAG_trace_precompiler) {
-      auto const name = function.ToLibNamePrefixedQualifiedCString();
-      auto const kind = UntaggedFunction::KindToCString(function.kind());
-      json.OpenObject();
-      json.PrintProperty("name", name);
-      json.PrintProperty("kind", kind);
-      json.PrintPropertyBool("retained", true);
-      LogBlock lb;
-      THR_Print("Retaining %s function %s\n", kind, name);
-      json.OpenArray("reasons");
-      if (auto const kv = retained_reasons_map_->Lookup(&function)) {
-        auto it = kv->value->GetIterator();
-        for (auto cstrp = it.Next(); cstrp != nullptr; cstrp = it.Next()) {
-          ASSERT(*cstrp != nullptr);
-          json.PrintValue(*cstrp);
-          THR_Print("Reason: %s\n", *cstrp);
-        }
-      } else {
-        THR_Print("No reasons recorded\n");
-      }
-      json.CloseArray();
-      json.CloseObject();
-    }
-    retained_functions.Add(function);
-  };
 
   auto drop_function = [&](const Function& function) {
     if (function.HasCode()) {
@@ -1884,14 +1999,11 @@
     }
     dropped_function_count_++;
     if (FLAG_trace_precompiler) {
-      auto const name = function.ToLibNamePrefixedQualifiedCString();
-      auto const kind = UntaggedFunction::KindToCString(function.kind());
-      json.OpenObject();
-      json.PrintProperty("name", name);
-      json.PrintProperty("kind", kind);
-      json.PrintPropertyBool("retained", false);
-      json.CloseObject();
-      THR_Print("Dropping %s function %s\n", kind, name);
+      THR_Print("Dropping function %s\n",
+                function.ToLibNamePrefixedQualifiedCString());
+    }
+    if (retained_reasons_writer_ != nullptr) {
+      retained_reasons_writer_->AddDropped(function);
     }
   };
 
@@ -1910,7 +2022,7 @@
         function ^= functions.At(j);
         function.DropUncompiledImplicitClosureFunction();
         if (functions_to_retain_.ContainsKey(function)) {
-          retain_function(function);
+          retained_functions.Add(function);
         } else {
           drop_function(function);
         }
@@ -1935,7 +2047,7 @@
           if (functions_to_retain_.ContainsKey(function)) {
             retained_functions.Add(name);
             retained_functions.Add(desc);
-            retain_function(function);
+            retained_functions.Add(function);
           } else {
             drop_function(function);
           }
@@ -1957,18 +2069,13 @@
   retained_functions = GrowableObjectArray::New();
   ClosureFunctionsCache::ForAllClosureFunctions([&](const Function& function) {
     if (functions_to_retain_.ContainsKey(function)) {
-      retain_function(function);
+      retained_functions.Add(function);
     } else {
       drop_function(function);
     }
     return true;  // Continue iteration.
   });
   IG->object_store()->set_closure_functions(retained_functions);
-
-  if (FLAG_trace_precompiler) {
-    json.CloseArray();
-    THR_Print("JSON for function decisions: %s\n", json.ToCString());
-  }
 }
 
 void Precompiler::DropFields() {
@@ -1976,7 +2083,6 @@
   Class& cls = Class::Handle(Z);
   Array& fields = Array::Handle(Z);
   Field& field = Field::Handle(Z);
-  Function& function = Function::Handle(Z);
   GrowableObjectArray& retained_fields = GrowableObjectArray::Handle(Z);
   AbstractType& type = AbstractType::Handle(Z);
 
@@ -1999,12 +2105,6 @@
 #endif
         if (retain) {
           if (FLAG_trace_precompiler) {
-            function = field.InitializerFunction();
-            if (!function.IsNull()) {
-              THR_Print("Retaining initializer function for %s field %s\n",
-                        field.is_static() ? "static" : "instance",
-                        function.ToLibNamePrefixedQualifiedCString());
-            }
             THR_Print("Retaining %s field %s\n",
                       field.is_static() ? "static" : "instance",
                       field.ToCString());
@@ -2015,12 +2115,6 @@
         } else {
           dropped_field_count_++;
           if (FLAG_trace_precompiler) {
-            function = field.InitializerFunction();
-            if (!function.IsNull()) {
-              THR_Print("Dropping initializer function for %s field %s\n",
-                        field.is_static() ? "static" : "instance",
-                        function.ToLibNamePrefixedQualifiedCString());
-            }
             THR_Print("Dropping %s field %s\n",
                       field.is_static() ? "static" : "instance",
                       field.ToCString());
diff --git a/runtime/vm/compiler/aot/precompiler.h b/runtime/vm/compiler/aot/precompiler.h
index 48613ee..2af72af 100644
--- a/runtime/vm/compiler/aot/precompiler.h
+++ b/runtime/vm/compiler/aot/precompiler.h
@@ -29,6 +29,7 @@
 class Precompiler;
 class FlowGraph;
 class PrecompilerTracer;
+class RetainedReasonsWriter;
 
 class TableSelectorKeyValueTrait {
  public:
@@ -351,39 +352,6 @@
   Isolate* isolate() const { return isolate_; }
   IsolateGroup* isolate_group() const { return thread_->isolate_group(); }
 
-  struct RetainedReasonsTrait {
-    using Key = const Object*;
-    using Value = ZoneCStringSet*;
-
-    struct Pair {
-      Key key;
-      Value value;
-
-      Pair() : key(nullptr), value(nullptr) {}
-      Pair(Key key, Value value) : key(key), value(value) {}
-    };
-
-    static Key KeyOf(Pair kv) { return kv.key; }
-
-    static Value ValueOf(Pair kv) { return kv.value; }
-
-    static inline intptr_t Hashcode(Key key) {
-      if (key->IsFunction()) {
-        return Function::Cast(*key).Hash();
-      }
-      if (key->IsClass()) {
-        return Utils::WordHash(Class::Cast(*key).id());
-      }
-      return Utils::WordHash(key->GetClassId());
-    }
-
-    static inline bool IsKeyEqual(Pair pair, Key key) {
-      return pair.key->ptr() == key->ptr();
-    }
-  };
-
-  using RetainedReasonsMap = ZoneDirectChainedHashMap<RetainedReasonsTrait>;
-
   Thread* thread_;
   Zone* zone_;
   Isolate* isolate_;
@@ -412,7 +380,6 @@
   FunctionSet possibly_retained_functions_;
   FieldSet fields_to_retain_;
   FunctionSet functions_to_retain_;
-  RetainedReasonsMap* retained_reasons_map_ = nullptr;
   ClassSet classes_to_retain_;
   TypeArgumentsSet typeargs_to_retain_;
   AbstractTypeSet types_to_retain_;
@@ -428,6 +395,7 @@
 
   Phase phase_ = Phase::kPreparation;
   PrecompilerTracer* tracer_ = nullptr;
+  RetainedReasonsWriter* retained_reasons_writer_ = nullptr;
   bool is_tracing_ = false;
 };
 
diff --git a/tools/VERSION b/tools/VERSION
index 751bc90..f036980 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 13
 PATCH 0
-PRERELEASE 136
+PRERELEASE 137
 PRERELEASE_PATCH 0
\ No newline at end of file