[vm] Improve code mapping in analyze_snapshot

* Make sure to iterate object_store and StubCode to collect
  more code objects
* Include name field all code objects and try to provide a meaningful
  name where possible.
* Identify stubs (is_stub: true) and assign them names
* Include pseudo-code objects for all Code-less entries in
  instructions tables.

TEST=vm/dart/analyze_snapshot_binary_test

Change-Id: I842cd5482bc407a203131da8e50041228bd9c372
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/438181
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Slava Egorov <vegorov@google.com>
diff --git a/runtime/tests/vm/dart/analyze_snapshot_binary_test.dart b/runtime/tests/vm/dart/analyze_snapshot_binary_test.dart
index 408e7dc..7f848a6 100644
--- a/runtime/tests/vm/dart/analyze_snapshot_binary_test.dart
+++ b/runtime/tests/vm/dart/analyze_snapshot_binary_test.dart
@@ -7,6 +7,7 @@
 import 'dart:ffi';
 import 'dart:async';
 
+import 'package:collection/collection.dart';
 import 'package:expect/expect.dart';
 import 'package:native_stack_traces/elf.dart';
 import 'package:path/path.dart' as path;
@@ -27,12 +28,8 @@
   bool forceDrops = false,
   bool stripUtil = false, // Note: forced true if useAsm.
   bool stripFlag = false,
-  bool disassemble = false,
 }) async {
   const isProduct = const bool.fromEnvironment('dart.vm.product');
-  if (isProduct && disassemble) {
-    Expect.isFalse(disassemble, 'no use of disassembler in PRODUCT mode');
-  }
 
   final analyzeSnapshot = path.join(buildDir, 'analyze_snapshot');
 
@@ -55,9 +52,6 @@
   if (stripUtil) {
     descriptionBuilder.write('-extstrip');
   }
-  if (disassemble) {
-    descriptionBuilder.write('-disassembled');
-  }
 
   final description = descriptionBuilder.toString();
   Expect.isTrue(
@@ -77,26 +71,32 @@
         '--no-retain-function-objects',
         '--no-retain-code-objects',
       ],
-      if (disassemble) '--disassemble', // Not defined in PRODUCT mode.
       dillPath,
     ];
 
+    final int textSectionSize;
     if (useAsm) {
       final assemblyPath = path.join(tempDir, 'test.S');
 
-      await (disassemble ? runSilent : run)(genSnapshot, <String>[
-        '--snapshot-kind=app-aot-assembly',
-        '--assembly=$assemblyPath',
-        ...commonSnapshotArgs,
-      ]);
+      textSectionSize = _findTextSectionSize(
+        await runOutput(genSnapshot, <String>[
+          '--snapshot-kind=app-aot-assembly',
+          '--assembly=$assemblyPath',
+          '--print-snapshot-sizes',
+          ...commonSnapshotArgs,
+        ], ignoreStdErr: true),
+      );
 
       await assembleSnapshot(assemblyPath, snapshotPath);
     } else {
-      await (disassemble ? runSilent : run)(genSnapshot, <String>[
-        '--snapshot-kind=app-aot-elf',
-        '--elf=$snapshotPath',
-        ...commonSnapshotArgs,
-      ]);
+      textSectionSize = _findTextSectionSize(
+        await runOutput(genSnapshot, <String>[
+          '--snapshot-kind=app-aot-elf',
+          '--elf=$snapshotPath',
+          '--print-snapshot-sizes',
+          ...commonSnapshotArgs,
+        ], ignoreStdErr: true),
+      );
     }
 
     print("Snapshot generated at $snapshotPath.");
@@ -371,6 +371,34 @@
       analyzerJson['metadata']['analyzer_version'] == 2,
       'invalid snapshot analyzer version',
     );
+
+    // Find all code objects.
+    final codeObjects = objects
+        .where((o) => o['type'] == 'Code' && o['size'] != 0)
+        .map(
+          (o) => (
+            name: o['name'] as String,
+            stub: (o['is_stub'] as bool? ?? false),
+            offset: o['offset'] as int,
+            size: o['size'] as int,
+          ),
+        )
+        .toList();
+    codeObjects.sort((a, b) => a.offset.compareTo(b.offset));
+    Expect.isNotEmpty(codeObjects);
+    Expect.isNotNull(
+      codeObjects.firstWhereOrNull((o) => o.stub && o.name == 'AllocateArray'),
+      'expected stubs to be identified',
+    );
+    final int totalSize = codeObjects.fold(0, (size, code) => size + code.size);
+    int totalGap = 0;
+    for (int i = 0; i < codeObjects.length - 1; i++) {
+      final code = codeObjects[i];
+      final nextCode = codeObjects[i + 1];
+      totalGap += code.offset + code.size - nextCode.offset;
+    }
+    Expect.isTrue(totalGap < 500);
+    Expect.isTrue((totalSize + totalGap - textSectionSize) < 500);
   });
 }
 
@@ -559,14 +587,6 @@
       testAOT(aotDillPath, stripFlag: true),
     ]);
 
-    // Since we can't force disassembler support after the fact when running
-    // in PRODUCT mode, skip any --disassemble tests. Do these tests last as
-    // they have lots of output and so the log will be truncated.
-    if (!const bool.fromEnvironment('dart.vm.product')) {
-      // Regression test for dartbug.com/41149.
-      await Future.wait([testAOT(aotDillPath, disassemble: true)]);
-    }
-
     // Test unstripped ELF generation that is then externally stripped.
     await Future.wait([testAOT(aotDillPath, stripUtil: true)]);
 
@@ -587,3 +607,10 @@
 Future<String> readFile(String file) {
   return new File(file).readAsString();
 }
+
+int _findTextSectionSize(List<String> output) {
+  const prefix = 'Instructions(CodeSize): ';
+  return int.parse(
+    output.firstWhere((l) => l.startsWith(prefix)).substring(prefix.length),
+  );
+}
diff --git a/runtime/tests/vm/dart/use_flag_test_helper.dart b/runtime/tests/vm/dart/use_flag_test_helper.dart
index 69ecb38..6d12ad9 100644
--- a/runtime/tests/vm/dart/use_flag_test_helper.dart
+++ b/runtime/tests/vm/dart/use_flag_test_helper.dart
@@ -253,14 +253,20 @@
   }
 }
 
-Future<List<String>> runOutput(String executable, List<String> args) async {
+Future<List<String>> runOutput(
+  String executable,
+  List<String> args, {
+  bool ignoreStdErr = false,
+}) async {
   final result = await runHelper(executable, args);
 
   if (result.exitCode != 0) {
     throw 'Command failed with unexpected exit code (was ${result.exitCode})';
   }
   Expect.isTrue(result.stdout.isNotEmpty);
-  Expect.isTrue(result.stderr.isEmpty);
+  if (!ignoreStdErr) {
+    Expect.isTrue(result.stderr.isEmpty);
+  }
 
   return LineSplitter.split(result.stdout).toList(growable: false);
 }
diff --git a/runtime/vm/analyze_snapshot_api_impl.cc b/runtime/vm/analyze_snapshot_api_impl.cc
index 9a60ea8..7983634 100644
--- a/runtime/vm/analyze_snapshot_api_impl.cc
+++ b/runtime/vm/analyze_snapshot_api_impl.cc
@@ -4,9 +4,9 @@
 
 #include <cstddef>
 #include <cstdint>
-#include <map>
 #include <set>
 #include <sstream>
+#include <unordered_map>
 #include <vector>
 
 #include "include/analyze_snapshot_api.h"
@@ -71,6 +71,7 @@
                               const std::vector<const Field*>& fields);
   void DumpFunction(const Function& function);
   void DumpCode(const Code& code);
+  void DumpCode(uword start_pc, uword end_pc, const char* name);
   void DumpField(const Field& field);
   void DumpString(const String& string);
   void DumpInstance(const Object& object);
@@ -85,6 +86,7 @@
   const Dart_SnapshotAnalyzerInformation& info_;
   std::vector<std::vector<const Field*>> class_fields_;
   std::vector<std::vector<const Field*>> top_level_class_fields_;
+  std::unordered_map<uword, const char*> stub_names_;
 
   JSONWriter js_;
   Thread* thread_;
@@ -250,7 +252,7 @@
 
 void SnapshotAnalyzer::DumpFunction(const Function& function) {
   js_.PrintProperty("type", "Function");
-  js_.PrintProperty("name", function.ToCString());
+  js_.PrintProperty("name", function.QualifiedScrubbedNameCString());
 
   js_.PrintProperty("signature",
                     String::Handle(function.InternalSignature()).ToCString());
@@ -262,28 +264,108 @@
   }
 }
 
+namespace {
+// Try to identify stubs which were effectively copied into the isolate
+// instructions section by comparing payloads.
+const char* TryIdentifyIsolateSpecificStubCopy(ObjectStore* object_store,
+                                               const Code& code) {
+#define MATCH(member, name)                                                    \
+  if (object_store->member() != Code::null() &&                                \
+      StubCode::name().ptr() == object_store->member() &&                      \
+      StubCode::name().Size() == code.Size() &&                                \
+      memcmp(reinterpret_cast<void*>(code.PayloadStart()),                     \
+             reinterpret_cast<void*>(StubCode::name().PayloadStart()),         \
+             code.Size()) == 0) {                                              \
+    return "_iso_stub_" #name "Stub";                                          \
+  }
+  OBJECT_STORE_STUB_CODE_LIST(MATCH)
+#undef MATCH
+
+  return nullptr;
+}
+}  // namespace
+
 void SnapshotAnalyzer::DumpCode(const Code& code) {
   js_.PrintProperty("type", "Code");
   const auto instruction_base =
       reinterpret_cast<uint64_t>(info_.vm_isolate_instructions);
 
+  if (code.IsUnknownDartCode()) {
+    js_.PrintProperty64("offset", 0);
+    js_.PrintProperty64("size", 0);
+    js_.PrintProperty("name", "UnknownDartCode");
+    js_.PrintProperty("section", "_kDartVmSnapshotInstructions");
+    return;
+  }
+
   // On different architectures the type of the underlying
   // dart::uword can result in an unsigned long long vs unsigned long
   // mismatch.
   const auto code_addr = static_cast<uint64_t>(code.PayloadStart());
-  // Invoking code.PayloadStart() for _kDartVmSnapshotInstructions
-  // when the tree has been shaken always returns 0
-  if (code_addr == 0) {
-    js_.PrintProperty64("offset", 0);
-    js_.PrintProperty64("size", 0);
-    js_.PrintProperty("section", "_kDartVmSnapshotInstructions");
+  js_.PrintProperty64("offset", code_addr - instruction_base);
+  js_.PrintProperty64("size", static_cast<uint64_t>(code.Size()));
+  js_.PrintProperty("section", "_kDartIsolateSnapshotInstructions");
+
+  if (code.owner() != Object::null()) {
+    const auto& owner = Object::Handle(code.owner());
+    js_.PrintProperty("owner", GetObjectId(owner.ptr()));
+    if (owner.IsClass()) {
+      js_.PrintfProperty("name", "new %s",
+                         Class::Cast(owner).ScrubbedNameCString());
+      js_.PrintPropertyBool("is_stub", true);
+    } else if (owner.IsAbstractType()) {
+      js_.PrintfProperty("name", "as %s",
+                         AbstractType::Cast(owner).ScrubbedNameCString());
+      js_.PrintPropertyBool("is_stub", true);
+    } else if (owner.IsFunction()) {
+      js_.PrintProperty("name", Function::Cast(owner).UserVisibleNameCString());
+    } else if (owner.IsSmi()) {
+      // This is a class id of the class which owned the function.
+      // See Precompiler::DropFunctions.
+      const auto cid = Smi::Cast(owner).Value();
+      auto class_table = thread_->isolate_group()->class_table();
+      if (class_table->IsValidIndex(cid) &&
+          class_table->At(cid) != Class::null()) {
+        const auto& cls = Class::Handle(class_table->At(cid));
+        js_.PrintProperty("owner", GetObjectId(cls.ptr()));
+        js_.PrintfProperty("name", "unknown function of %s",
+                           Class::Cast(cls).ScrubbedNameCString());
+      } else {
+        js_.PrintfProperty("name", "unknown function of class #%" Pd "", cid);
+      }
+    } else {
+      // Expected to handle all possibilities.
+      UNREACHABLE();
+    }
   } else {
-    js_.PrintProperty64("offset", code_addr - instruction_base);
-    js_.PrintProperty64("size", static_cast<uint64_t>(code.Size()));
-    js_.PrintProperty("section", "_kDartIsolateSnapshotInstructions");
+    js_.PrintPropertyBool("is_stub", true);
+
+    const auto it = stub_names_.find(code.EntryPoint());
+    if (it != stub_names_.end()) {
+      js_.PrintProperty("name", it->second);
+    } else if (auto stub_name = TryIdentifyIsolateSpecificStubCopy(
+                   thread_->isolate_group()->object_store(), code)) {
+      js_.PrintProperty("name", stub_name);
+    } else {
+      UNREACHABLE();
+    }
   }
 }
 
+void SnapshotAnalyzer::DumpCode(uword start_pc,
+                                uword end_pc,
+                                const char* name) {
+  js_.PrintProperty("type", "Code");
+  const auto instruction_base =
+      reinterpret_cast<uint64_t>(info_.vm_isolate_instructions);
+
+  js_.PrintProperty64("offset",
+                      static_cast<uint64_t>(start_pc) - instruction_base);
+  js_.PrintProperty64("size", static_cast<uint64_t>(end_pc - start_pc));
+  js_.PrintProperty("name", name);
+  js_.PrintProperty("section", "_kDartIsolateSnapshotInstructions");
+}
+
 void SnapshotAnalyzer::DumpField(const Field& field) {
   const auto& name = String::Handle(field.name());
   const auto& type = AbstractType::Handle(field.type());
@@ -383,6 +465,12 @@
 }
 
 void SnapshotAnalyzer::DumpInterestingObjects() {
+  // Collect stubs into stub_names to enable quick name lookup
+  StubCode::ForEachStub([&](const char* name, uword entry_point) {
+    stub_names_[entry_point] = name;
+    return true;
+  });
+
   Zone* zone = thread_->zone();
   auto class_table = thread_->isolate_group()->class_table();
   class_table->NumCids();
@@ -426,6 +514,29 @@
       object = class_table->At(cid);
       handle_object(object.ptr());
     }
+
+    // - All instructions tables
+    const auto& instruction_tables = GrowableObjectArray::Handle(
+        thread_->isolate_group()->object_store()->instructions_tables());
+    for (intptr_t i = 0; i < instruction_tables.Length(); i++) {
+      object = instruction_tables.At(i);
+      object = InstructionsTable::Cast(object).code_objects();
+      handle_object(object.ptr());
+    }
+
+    // - All VM stubs
+    for (intptr_t i = 0; i < StubCode::NumEntries(); i++) {
+      if (!StubCode::EntryAt(i).IsNull()) {
+        handle_object(StubCode::EntryAt(i).ptr());
+      }
+    }
+
+    // - Object store.
+    //
+    // This will include a bunch of stuff we don't care about
+    // but it will also capture things like isolate specific stubs and
+    // canonicalized types which themselves include references to stubs.
+    thread_->isolate_group()->object_store()->VisitObjectPointers(&visitor);
   }
 
   // Sometimes we have [Field] objects for fields but they are not available
@@ -490,9 +601,28 @@
     } else if (object->IsInstance()) {
       DumpInstance(*object);
     }
-
     js_.CloseObject();
   }
+
+  // Finally dump pseudo-Code objects for all entries in the instructions
+  // tables without code objects.
+  uint64_t pseudo_code_id = kStartIndex + discovered_objects.size();
+  const auto& instruction_tables = GrowableObjectArray::Handle(
+      thread_->isolate_group()->object_store()->instructions_tables());
+  auto& instructions_table = InstructionsTable::Handle();
+  for (intptr_t i = 0; i < instruction_tables.Length(); i++) {
+    instructions_table ^= instruction_tables.At(i);
+    for (intptr_t index = 0; index < instructions_table.FirstEntryWithCode();
+         index++) {
+      js_.OpenObject();
+      js_.PrintProperty64("id", pseudo_code_id);
+      DumpCode(instructions_table.EntryPointAt(index),
+               instructions_table.EntryPointAt(index + 1), "Unknown Code");
+      js_.CloseObject();
+      pseudo_code_id++;
+    }
+  }
+
   js_.CloseArray();
 }
 
diff --git a/runtime/vm/compiler/aot/precompiler.cc b/runtime/vm/compiler/aot/precompiler.cc
index 673a7ea..d3437fc 100644
--- a/runtime/vm/compiler/aot/precompiler.cc
+++ b/runtime/vm/compiler/aot/precompiler.cc
@@ -2235,6 +2235,8 @@
       function.ClearCode();
       // Wrap the owner of the code object in case the code object will be
       // serialized but the function object will not.
+      // If you are changing this code you might want to adjust
+      // SnapshotAnalyzer::DumpCode which looks at owners.
       owner = code.owner();
       owner = WeakSerializationReference::New(
           owner, Smi::Handle(Smi::New(owner.GetClassId())));
diff --git a/runtime/vm/object.h b/runtime/vm/object.h
index 929a54e..fb8f4f0 100644
--- a/runtime/vm/object.h
+++ b/runtime/vm/object.h
@@ -6070,6 +6070,12 @@
   // Returns entry point of the instructions with given index.
   uword EntryPointAt(intptr_t index) const;
 
+  ArrayPtr code_objects() const { return untag()->code_objects_; }
+
+  intptr_t FirstEntryWithCode() const {
+    return static_cast<intptr_t>(rodata()->first_entry_with_code);
+  }
+
  private:
   uword start_pc() const { return InstructionsTable::start_pc(this->ptr()); }
   static uword start_pc(InstructionsTablePtr table) {
@@ -6081,8 +6087,6 @@
     return table->untag()->end_pc_;
   }
 
-  ArrayPtr code_objects() const { return untag()->code_objects_; }
-
   void set_length(intptr_t value) const;
   void set_start_pc(uword value) const;
   void set_end_pc(uword value) const;
diff --git a/runtime/vm/stub_code.cc b/runtime/vm/stub_code.cc
index eeebe03..066f5b6 100644
--- a/runtime/vm/stub_code.cc
+++ b/runtime/vm/stub_code.cc
@@ -347,24 +347,39 @@
   }
 }
 
-const char* StubCode::NameOfStub(uword entry_point) {
+void StubCode::ForEachStub(
+    const std::function<bool(const char*, uword)>& callback) {
   for (size_t i = 0; i < ARRAY_SIZE(entries_); i++) {
-    if ((entries_[i].code != nullptr) && !entries_[i].code->IsNull() &&
-        (entries_[i].code->EntryPoint() == entry_point)) {
-      return entries_[i].name;
+    if (entries_[i].code != nullptr && !entries_[i].code->IsNull()) {
+      if (!callback(entries_[i].name, entries_[i].code->EntryPoint())) {
+        return;
+      }
     }
   }
-
   auto object_store = IsolateGroup::Current()->object_store();
 
 #define MATCH(member, name)                                                    \
-  if (object_store->member() != Code::null() &&                                \
-      entry_point == Code::EntryPointOf(object_store->member())) {             \
-    return "_iso_stub_" #name "Stub";                                          \
+  if (object_store->member() != Code::null()) {                                \
+    if (!callback("_iso_stub_" #name "Stub",                                   \
+                  Code::EntryPointOf(object_store->member()))) {               \
+      return;                                                                  \
+    }                                                                          \
   }
   OBJECT_STORE_STUB_CODE_LIST(MATCH)
 #undef MATCH
-  return nullptr;
+}
+
+const char* StubCode::NameOfStub(uword entry_point) {
+  const char* result = nullptr;
+  ForEachStub(
+      [&result, &entry_point](const char* name, uword stub_entry_point) {
+        if (stub_entry_point == entry_point) {
+          result = name;
+          return false;  // Found match.
+        }
+        return true;  // Continue searching.
+      });
+  return result;
 }
 
 }  // namespace dart
diff --git a/runtime/vm/stub_code.h b/runtime/vm/stub_code.h
index 81775a5..1863bbc 100644
--- a/runtime/vm/stub_code.h
+++ b/runtime/vm/stub_code.h
@@ -5,6 +5,8 @@
 #ifndef RUNTIME_VM_STUB_CODE_H_
 #define RUNTIME_VM_STUB_CODE_H_
 
+#include <functional>
+
 #include "vm/allocation.h"
 #include "vm/compiler/runtime_api.h"
 #include "vm/object.h"
@@ -55,6 +57,10 @@
   // Returns nullptr if no stub found.
   static const char* NameOfStub(uword entry_point);
 
+  // Callback is called for each stub until it returns false.
+  static void ForEachStub(
+      const std::function<bool(const char*, uword)>& callback);
+
 // Define the shared stub code accessors.
 #define STUB_CODE_ACCESSOR(name)                                               \
   static const Code& name() { return *entries_[k##name##Index].code; }         \