blob: 9c2ee447f0b042fc71fcfc946d2e12c1ea504f95 [file] [log] [blame]
// Copyright (c) 2021, 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.
#include <cstddef>
#include <cstdint>
#include <set>
#include <sstream>
#include <unordered_map>
#include <vector>
#include "include/analyze_snapshot_api.h"
#include "vm/compiler/runtime_api.h"
#include "vm/dart.h"
#include "vm/dart_api_impl.h"
#include "vm/globals.h"
#include "vm/json_writer.h"
#include "vm/object.h"
#include "vm/object_store.h"
#include "vm/thread.h"
namespace dart {
namespace snapshot_analyzer {
constexpr intptr_t kSnapshotAnalyzerVersion = 2;
constexpr intptr_t kStartIndex = 1;
class FieldVisitor : public ObjectPointerVisitor {
public:
explicit FieldVisitor(IsolateGroup* isolate_group)
: ObjectPointerVisitor(isolate_group) {}
void init(std::function<void(ObjectPtr)>* fun) { callback_ = fun; }
void VisitPointers(ObjectPtr* first, ObjectPtr* last) override {
for (ObjectPtr* current = first; current <= last; current++) {
(*callback_)(*current);
}
}
#if defined(DART_COMPRESSED_POINTERS)
void VisitCompressedPointers(uword heap_base,
CompressedObjectPtr* first,
CompressedObjectPtr* last) override {
for (CompressedObjectPtr* current = first; current <= last; current++) {
(*callback_)(current->Decompress(heap_base));
}
}
#endif
private:
std::function<void(ObjectPtr object)>* callback_ = nullptr;
};
class SnapshotAnalyzer {
public:
explicit SnapshotAnalyzer(const Dart_SnapshotAnalyzerInformation& info)
: info_(info),
class_fields_(IsolateGroup::Current()->class_table()->NumCids()),
top_level_class_fields_(
IsolateGroup::Current()->class_table()->NumTopLevelCids()) {}
// Saves JSON format snapshot information in the output character buffer.
void DumpSnapshotInformation(char** buffer, intptr_t* buffer_length);
private:
void DumpLibrary(const Library& library);
void DumpArray(const Array& array, const char* name);
void DumpClass(const Class& klass);
void DumpClassInstanceSlots(const Class& klass,
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);
void DumpType(const Type& type);
void DumpObjectPool(const ObjectPool& pool);
void DumpInterestingObjects();
void DumpMetadata();
intptr_t GetObjectId(ObjectPtr obj) { return heap_->GetObjectId(obj); }
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_;
Heap* heap_;
};
void SnapshotAnalyzer::DumpLibrary(const Library& library) {
js_.PrintProperty("type", "Library");
js_.PrintProperty("url", String::Handle(library.url()).ToCString());
js_.PrintProperty("toplevel_class",
GetObjectId(Object::RawCast(library.toplevel_class())));
}
void SnapshotAnalyzer::DumpArray(const Array& array, const char* name) {
js_.OpenArray(name);
for (intptr_t i = 0; i < array.Length(); ++i) {
js_.PrintValue64(GetObjectId(array.At(i)));
}
js_.CloseArray();
}
void SnapshotAnalyzer::DumpClass(const Class& klass) {
js_.PrintProperty("type", "Class");
auto& class_fields =
klass.IsTopLevel() ? top_level_class_fields_ : class_fields_;
const auto& fields =
class_fields[klass.IsTopLevel()
? ClassTable::IndexFromTopLevelCid(klass.id())
: klass.id()];
js_.PrintProperty("class_id", klass.id());
js_.PrintProperty("name", String::Handle(klass.Name()).ToCString());
js_.PrintProperty("super_class", GetObjectId(klass.SuperClass()));
Zone* zone = thread_->zone();
Array& array = Array::Handle(zone);
// We use [fields] instead of [Class.fields()] as a [Field] object may not
// appear in [Class.fields()] but may still be available (e.g. for
// `LateInitizializationError`) so we can include all fields of the class we
// found.
js_.OpenArray("fields");
for (uintptr_t i = 0; i < fields.size(); ++i) {
js_.PrintValue64(GetObjectId(fields[i]->ptr()));
}
js_.CloseArray();
// Here we write information about every slot in an instance. Even if there's
// no corresponding [Field] object, we will write out an entry describing the
// slot (e.g. whether it's boxed or not, ...)
//
// So this information is always available, whereas the "fields" above only
// writes non-tree shaken [Field] obejcts.
if (!klass.IsTopLevel()) {
DumpClassInstanceSlots(klass, fields);
}
array = klass.functions();
if (!array.IsNull()) {
DumpArray(array, "functions");
}
array = klass.interfaces();
if (!array.IsNull()) {
DumpArray(array, "interfaces");
}
Library& library = Library::Handle(klass.library());
if (!library.IsNull()) {
js_.PrintProperty("library", GetObjectId(klass.library()));
}
}
void SnapshotAnalyzer::DumpClassInstanceSlots(
const Class& klass,
const std::vector<const Field*>& fields) {
const auto& super_class = Class::Handle(klass.SuperClass());
auto& field = Field::Handle();
auto& type = AbstractType::Handle();
auto class_table = thread_->isolate_group()->class_table();
const auto bitmap = class_table->GetUnboxedFieldsMapAt(klass.id());
const intptr_t start_offset = super_class.IsNull()
? Instance::NextFieldOffset()
: super_class.host_next_field_offset();
const intptr_t end_offset = klass.host_next_field_offset();
js_.OpenArray("instance_slots");
intptr_t offset = start_offset;
while (offset < end_offset) {
const bool is_reference = !bitmap.Get(offset / kCompressedWordSize);
js_.OpenObject();
js_.PrintProperty("offset", offset);
js_.PrintPropertyBool("is_reference", is_reference);
if (offset == klass.host_type_arguments_field_offset()) {
RELEASE_ASSERT(is_reference);
js_.PrintProperty("slot_type", "type_arguments_field");
js_.CloseObject();
offset += kCompressedWordSize;
continue;
}
// Try to see if the corresponding [Field] object was not tree shaken.
bool found = false;
for (uintptr_t i = 0; i < fields.size(); ++i) {
field = fields[i]->ptr();
if (field.is_static()) continue;
if (field.HostOffset() == offset) {
found = true;
break;
}
}
if (found) {
type = field.type();
intptr_t slots = 0;
if (field.is_unboxed()) {
if (type.IsDoubleType()) {
slots = sizeof(double) / kCompressedWordSize;
} else if (type.IsIntType()) {
slots = sizeof(int64_t) / kCompressedWordSize;
} else if (type.IsFloat32x4Type()) {
slots = sizeof(simd128_value_t) / kCompressedWordSize;
} else if (type.IsFloat64x2Type()) {
slots = sizeof(simd128_value_t) / kCompressedWordSize;
} else {
// Rare: Could be that the field type isn't telling us the unboxed
// type but field is still unboxed (e.g. `dynamic` field which TFA
// inferred to be of certain type).
//
// In this case we treat it as unknown field below (as we don't know
// it's size).
slots = -1;
}
} else {
slots = 1;
}
if (!field.is_unboxed() || slots > 0) {
js_.PrintProperty("slot_type", "instance_field");
js_.PrintProperty64("field", GetObjectId(field.ptr()));
js_.CloseObject();
offset += slots * kCompressedWordSize;
continue;
}
}
// This slot is either an unknown reference field or part of an unknown
// unboxed field (64-bit integer/double or 128-bit float32x4/float64x2).
// We cannot know the size or type of the unboxed field as the [Field]
// object was tree shaken.
js_.PrintProperty("slot_type", "unknown_slot");
js_.CloseObject();
offset += kCompressedWordSize;
}
js_.CloseArray();
}
void SnapshotAnalyzer::DumpFunction(const Function& function) {
js_.PrintProperty("type", "Function");
js_.PrintProperty("name", function.QualifiedScrubbedNameCString());
js_.PrintProperty("signature",
String::Handle(function.InternalSignature()).ToCString());
js_.PrintProperty("code", GetObjectId(function.CurrentCode()));
js_.PrintProperty("owner_class", GetObjectId(function.Owner()));
if (function.IsClosureFunction()) {
js_.PrintProperty("parent_function",
GetObjectId(function.parent_function()));
}
}
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());
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_.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());
js_.PrintProperty("type", "Field");
js_.PrintProperty("name", name.ToCString());
js_.PrintProperty64("type_class", GetObjectId(field.type()));
js_.PrintProperty("owner_class", GetObjectId(field.Owner()));
if (field.is_static()) {
js_.PrintProperty("instance", GetObjectId(field.StaticValue()));
js_.PrintProperty64("static_field_offset",
FieldTable::FieldOffsetFor(field.field_id()));
}
if (field.HasInitializerFunction()) {
js_.PrintProperty("initializer_function",
GetObjectId(field.InitializerFunction()));
}
js_.PrintPropertyBool("is_reference", !field.is_unboxed());
if (field.is_unboxed()) {
const char* unboxed_type = nullptr;
if (type.IsDoubleType()) {
unboxed_type = "double";
} else if (type.IsIntType()) {
unboxed_type = "int";
} else if (type.IsFloat32x4Type()) {
unboxed_type = "Float32x4";
} else if (type.IsFloat64x2Type()) {
unboxed_type = "Float64x2";
} else {
unboxed_type = "unknown";
}
js_.PrintProperty("unboxed_type", unboxed_type);
}
js_.OpenArray("flags");
if (field.is_final()) js_.PrintValue("final");
if (field.is_static()) {
js_.PrintValue("static");
if (field.is_shared()) js_.PrintValue("shared");
}
if (field.is_instance()) {
if (field.is_late()) js_.PrintValue("late");
}
js_.CloseArray();
}
void SnapshotAnalyzer::DumpString(const String& string) {
js_.PrintProperty("type", "String");
js_.PrintProperty("value", string.ToCString());
}
void SnapshotAnalyzer::DumpInstance(const Object& object) {
js_.PrintProperty("type", "Instance");
js_.PrintProperty("class", GetObjectId(object.clazz()));
FieldVisitor visitor(thread_->isolate_group());
// Two phase algorithm, first discover all relevant objects
// and assign ids, then write them out.
std::function<void(ObjectPtr)> print_reference = [&](ObjectPtr value) {
if (!value.IsHeapObject()) return;
intptr_t index = GetObjectId(value);
js_.PrintValue64(index);
};
visitor.init(&print_reference);
js_.OpenArray("references");
object.ptr().untag()->VisitPointers(&visitor);
js_.CloseArray();
}
void SnapshotAnalyzer::DumpType(const Type& type) {
js_.PrintProperty("type", "Type");
js_.PrintProperty("type_class", GetObjectId(type.type_class()));
const TypeArguments& arguments = TypeArguments::Handle(type.arguments());
js_.OpenArray("type_arguments");
for (intptr_t i = 0; i < arguments.Length(); ++i) {
js_.PrintValue64(GetObjectId(arguments.TypeAt(i)));
}
js_.CloseArray();
}
void SnapshotAnalyzer::DumpObjectPool(const ObjectPool& pool) {
js_.PrintProperty("type", "ObjectPool");
js_.OpenArray("references");
for (intptr_t i = 0; i < pool.Length(); ++i) {
if (pool.TypeAt(i) == ObjectPool::EntryType::kTaggedObject) {
// We write (index, offset, value) triplets.
js_.PrintValue64(i);
js_.PrintValue64(pool.OffsetFromIndex(i));
js_.PrintValue64(GetObjectId(pool.ObjectAt(i)));
}
}
js_.CloseArray();
}
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();
heap_->ResetObjectIdTable();
std::vector<const Object*> discovered_objects;
Object& object = Object::Handle(zone);
{
NoSafepointScope ns(thread_);
FieldVisitor visitor(thread_->isolate_group());
std::function<void(ObjectPtr)> handle_object = [&](ObjectPtr value) {
if (!value.IsHeapObject()) return;
// Ensure we never handle an object more than once.
if (heap_->GetObjectId(value) != 0) return;
heap_->SetObjectId(value, kStartIndex + discovered_objects.size());
discovered_objects.push_back(&Object::Handle(zone, value));
// Ensure all references of this object are visited first.
value->untag()->VisitPointers(&visitor);
};
visitor.init(&handle_object);
// BEGIN Visit all things we are interested in.
// - All constants reachable via object pool
object = thread_->isolate_group()->object_store()->global_object_pool();
handle_object(object.ptr());
// - All libraries
object = thread_->isolate_group()->object_store()->libraries();
object = GrowableObjectArray::Cast(object).data();
object.ptr().untag()->VisitPointers(&visitor);
// - All classes
auto class_table = thread_->isolate_group()->class_table();
for (intptr_t cid = 0; cid < class_table->NumCids(); ++cid) {
if (!class_table->HasValidClassAt(cid)) continue;
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
// from [Class.fields] (e.g. late final fields where the slow path uses
// [Field] from object pool to throw a nice error).
//
// So we manually look for all [Field]s and associate them with classes
// instead of relying on the [Class.fields] array.
auto& owner = Class::Handle();
for (uintptr_t i = 0; i < discovered_objects.size(); ++i) {
const Object& object = *discovered_objects[i];
if (object.IsField()) {
const auto& field = Field::Cast(object);
owner = field.Owner();
auto& array =
owner.IsTopLevel() ? top_level_class_fields_ : class_fields_;
const intptr_t index = owner.IsTopLevel()
? ClassTable::IndexFromTopLevelCid(owner.id())
: owner.id();
array[index].push_back(&field);
}
}
// Print information about objects
js_.OpenArray("objects");
// The 0 object id is used in the VM's weak hashmap implementation
// to indicate no value.
js_.OpenObject();
js_.PrintProperty("type", "NoValue");
js_.CloseObject();
for (size_t id = 0; id < discovered_objects.size(); ++id) {
const auto* object = discovered_objects[id];
js_.OpenObject();
// TODO(balid): Remove this as it can be inferred from the array position.
// Used for manual debugging at the moment.
js_.PrintProperty64("id", id + kStartIndex);
// Order matters here, Strings are a subtype of Instance, for example.
if (object->IsNull()) {
js_.PrintProperty("type", "Null");
} else if (object->IsLibrary()) {
DumpLibrary(Library::Cast(*object));
} else if (object->IsObjectPool()) {
DumpObjectPool(ObjectPool::Cast(*object));
} else if (object->IsClass()) {
DumpClass(Class::Cast(*object));
} else if (object->IsFunction()) {
DumpFunction(Function::Cast(*object));
} else if (object->IsCode()) {
DumpCode(Code::Cast(*object));
} else if (object->IsField()) {
DumpField(Field::Cast(*object));
} else if (object->IsString()) {
DumpString(String::Cast(*object));
} else if (object->IsArray()) {
js_.PrintProperty("type", "Array");
const Array& array = Array::Handle(Array::RawCast(object->ptr()));
DumpArray(array, "elements");
} else if (object->IsType()) {
DumpType(Type::Cast(*object));
} 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();
}
void SnapshotAnalyzer::DumpMetadata() {
js_.OpenObject("metadata");
js_.OpenObject("offsets");
js_.OpenObject("thread");
// TODO(balid): Use `dart::compiler::target::` versions.
js_.PrintProperty("isolate", Thread::isolate_offset());
js_.PrintProperty("isolate_group", Thread::isolate_group_offset());
js_.PrintProperty("dispatch_table_array",
Thread::dispatch_table_array_offset());
js_.CloseObject();
js_.OpenObject("isolate_group");
js_.PrintProperty("class_table", IsolateGroup::class_table_offset());
js_.PrintProperty("cached_class_table",
IsolateGroup::cached_class_table_table_offset());
js_.PrintProperty("object_store_offset", IsolateGroup::object_store_offset());
js_.CloseObject();
js_.CloseObject();
js_.PrintProperty64("word_size", dart::compiler::target::kWordSize);
js_.PrintProperty64("compressed_word_size",
dart::compiler::target::kCompressedWordSize);
js_.PrintProperty64("analyzer_version", kSnapshotAnalyzerVersion);
js_.CloseObject();
}
void SnapshotAnalyzer::DumpSnapshotInformation(char** buffer,
intptr_t* buffer_length) {
thread_ = Thread::Current();
heap_ = thread_->isolate_group()->heap();
DARTSCOPE(thread_);
// Open empty object so output is valid/parsable JSON.
js_.OpenObject();
js_.OpenObject("snapshot_data");
// Base addresses of the snapshot data, useful to calculate relative offsets.
js_.PrintfProperty("vm_data", "%p", info_.vm_snapshot_data);
js_.PrintfProperty("vm_instructions", "%p", info_.vm_snapshot_instructions);
js_.PrintfProperty("isolate_data", "%p", info_.vm_isolate_data);
js_.PrintfProperty("isolate_instructions", "%p",
info_.vm_isolate_instructions);
js_.CloseObject();
{
// Debug builds assert that our thread has a lock before accessing
// vm internal fields.
SafepointReadRwLocker ml(thread_, thread_->isolate_group()->program_lock());
DumpInterestingObjects();
DumpMetadata();
}
// Close our empty object.
js_.CloseObject();
// Give ownership to caller.
js_.Steal(buffer, buffer_length);
}
void Dart_DumpSnapshotInformationAsJson(
const Dart_SnapshotAnalyzerInformation& info,
char** out,
intptr_t* out_len) {
SnapshotAnalyzer analyzer(info);
analyzer.DumpSnapshotInformation(out, out_len);
}
} // namespace snapshot_analyzer
} // namespace dart