[VM/Service] Support records in the VM Service

This makes VM Service responses involving records comply with
https://docs.google.com/document/d/1gq7ZlH9My2qIrlFY7-pceNmabOPLLeLVMt6Z44KE7_8.

service.md and package:vm_service are not yet updated by this CL. They
will be updated in a future CL that makes as much of the service comply
with the above proposal as possible without introducing breaking
changes.
e.g., the future CL will make it so that both the `decl` and `name`
properties will be populated for fields of `PlainInstance`s.

TEST=CI

Issue: https://github.com/dart-lang/sdk/issues/49724
Change-Id: Iff798e391d28f137a000352c15e180f32e3d3969
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/264782
Commit-Queue: Derek Xu <derekx@google.com>
Reviewed-by: Anna Gringauze <annagrin@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
Reviewed-by: Siva Annamalai <asiva@google.com>
This commit is contained in:
Derek Xu 2022-11-02 19:32:54 +00:00 committed by Commit Queue
parent 919a0460fb
commit 40defeeb44
3 changed files with 100 additions and 28 deletions

View file

@ -1,6 +1,9 @@
// 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=--enable-experiment=records
// @dart=2.19
// ignore_for_file: experiment_not_enabled
library get_object_rpc_test;
@ -57,6 +60,9 @@ getUint8List() => uint8List;
@pragma("vm:entry-point")
getUint64List() => uint64List;
@pragma("vm:entry-point")
getRecord() => (1, x: 2, 3.0, y: 4.0);
@pragma("vm:entry-point")
getDummyClass() => _DummyClass();
@ -642,6 +648,30 @@ var tests = <IsolateTest>[
}
},
// A record.
(VmService service, IsolateRef isolateRef) async {
final isolateId = isolateRef.id!;
final isolate = await service.getIsolate(isolateId);
// Call eval to get a Dart record.
final evalResult =
await service.invoke(isolateId, isolate.rootLib!.id!, 'getRecord', [])
as InstanceRef;
final objectId = evalResult.id!;
final result = await service.getObject(isolateId, objectId) as Instance;
expect(result.kind, '_Record');
expect(result.json!['_vmType'], 'Record');
expect(result.id, startsWith('objects/'));
expect(result.valueAsString, isNull);
expect(result.classRef!.name, '_Record');
expect(result.size, isPositive);
final fields = result.fields!;
expect(fields.length, 4);
// TODO(derekx): Include field names in this test once they are accessible
// through package:vm_service.
Set<num> fieldValues = Set.from(fields.map((f) => f.value as num));
expect(fieldValues.containsAll([1, 2, 3.0, 4.0]), true);
},
// library.
(VmService service, IsolateRef isolateRef) async {
final isolateId = isolateRef.id!;

View file

@ -26,6 +26,17 @@ static void AddNameProperties(JSONObject* jsobj,
}
}
static inline void AddValuePropertyToBoundField(const JSONObject& field,
const Object& value) {
if (value.IsBool() || value.IsSmi() || value.IsMint() || value.IsDouble()) {
// If the value is a bool, int, or double, we directly add the value to the
// response instead of adding an @Instance.
field.AddPropertyNoEscape("value", value.ToCString());
} else {
field.AddProperty("value", value);
}
}
void Object::AddCommonObjectProperties(JSONObject* jsobj,
const char* protocol_type,
bool ref) const {
@ -1244,23 +1255,29 @@ void RecordType::PrintJSONImpl(JSONStream* stream, bool ref) const {
JSONObject jsobj(stream);
PrintSharedInstanceJSON(&jsobj, ref);
jsobj.AddProperty("kind", "_RecordType");
jsobj.AddServiceId(*this);
if (ref) {
return;
}
{
JSONArray arr(&jsobj, "fields");
JSONArray jsarr(&jsobj, "fields");
String& name = String::Handle();
AbstractType& type = AbstractType::Handle();
const intptr_t num_fields = NumFields();
const intptr_t num_positional_fields = NumPositionalFields();
AbstractType& type = AbstractType::Handle();
String& name = String::Handle();
for (intptr_t i = 0; i < num_fields; ++i) {
JSONObject field(&arr);
type = FieldTypeAt(i);
field.AddProperty("type", type);
if (i >= num_positional_fields) {
name = FieldNameAt(i - num_positional_fields);
field.AddProperty("name", name.ToCString());
for (intptr_t index = 0; index < num_fields; ++index) {
JSONObject jsfield(&jsarr);
// TODO(derekx): Remove this because BoundField isn't a response type in
// the spec.
jsfield.AddProperty("type", "BoundField");
if (index < num_positional_fields) {
jsfield.AddProperty("name", index);
} else {
field.AddProperty("pos", i);
name = FieldNameAt(index - num_positional_fields);
jsfield.AddProperty("name", name.ToCString());
}
type = FieldTypeAt(index);
jsfield.AddProperty("value", type);
}
}
}
@ -1648,7 +1665,6 @@ void Record::PrintJSONImpl(JSONStream* stream, bool ref) const {
JSONObject jsobj(stream);
PrintSharedInstanceJSON(&jsobj, ref);
jsobj.AddProperty("kind", "_Record");
jsobj.AddProperty("numFields", num_fields());
jsobj.AddServiceId(*this);
if (ref) {
return;
@ -1656,30 +1672,27 @@ void Record::PrintJSONImpl(JSONStream* stream, bool ref) const {
intptr_t offset;
intptr_t count;
stream->ComputeOffsetAndCount(num_fields(), &offset, &count);
if (offset > 0) {
jsobj.AddProperty("offset", offset);
}
if (count < num_fields()) {
jsobj.AddProperty("count", count);
}
intptr_t limit = offset + count;
ASSERT(limit <= num_fields());
{
JSONArray jsarr(&jsobj, "fields");
Object& obj = Object::Handle();
String& name = String::Handle();
Object& value = Object::Handle();
const intptr_t num_positional_fields = NumPositionalFields();
const Array& field_names = Array::Handle(this->field_names());
for (intptr_t index = offset; index < limit; ++index) {
JSONObject field(&jsarr);
obj = FieldAt(index);
field.AddProperty("value", obj);
if (index >= num_positional_fields) {
name ^= field_names.At(index - num_positional_fields);
field.AddProperty("name", name);
JSONObject jsfield(&jsarr);
// TODO(derekx): Remove this because BoundField isn't a response type in
// the spec.
jsfield.AddProperty("type", "BoundField");
if (index < num_positional_fields) {
jsfield.AddProperty("name", index);
} else {
field.AddProperty("pos", index);
name ^= field_names.At(index - num_positional_fields);
jsfield.AddProperty("name", name.ToCString());
}
value = FieldAt(index);
AddValuePropertyToBoundField(jsfield, value);
}
}
}

View file

@ -2268,6 +2268,24 @@ static Breakpoint* LookupBreakpoint(Isolate* isolate,
return NULL;
}
static inline void AddParentFieldToResponseBasedOnRecord(
Array* field_names_handle,
String* name_handle,
const JSONObject& jsresponse,
const Record& record,
const intptr_t field_slot_offset) {
const intptr_t num_positional_fields = record.NumPositionalFields();
*field_names_handle = record.field_names();
const intptr_t field_index =
(field_slot_offset - Record::field_offset(0)) / Record::kBytesPerElement;
if (field_index < num_positional_fields) {
jsresponse.AddProperty("parentField", field_index);
} else {
*name_handle ^= field_names_handle->At(field_index - num_positional_fields);
jsresponse.AddProperty("parentField", name_handle->ToCString());
}
}
static void PrintInboundReferences(Thread* thread,
Object* target,
intptr_t limit,
@ -2282,6 +2300,8 @@ static void PrintInboundReferences(Thread* thread,
JSONArray elements(&jsobj, "references");
Object& source = Object::Handle();
Smi& slot_offset = Smi::Handle();
Array& field_names = Array::Handle();
String& name = String::Handle();
Class& source_class = Class::Handle();
Field& field = Field::Handle();
Array& parent_field_map = Array::Handle();
@ -2297,6 +2317,10 @@ static void PrintInboundReferences(Thread* thread,
(slot_offset.Value() - Array::element_offset(0)) /
Array::kBytesPerElement;
jselement.AddProperty("parentListIndex", element_index);
} else if (source.IsRecord()) {
AddParentFieldToResponseBasedOnRecord(&field_names, &name, jselement,
Record::Cast(source),
slot_offset.Value());
} else {
if (source.IsInstance()) {
source_class = source.clazz();
@ -2394,13 +2418,14 @@ static void PrintRetainingPath(Thread* thread,
JSONArray elements(&jsobj, "elements");
Object& element = Object::Handle();
Smi& slot_offset = Smi::Handle();
Array& field_names = Array::Handle();
String& name = String::Handle();
Class& element_class = Class::Handle();
Array& element_field_map = Array::Handle();
LinkedHashMap& map = LinkedHashMap::Handle();
Array& map_data = Array::Handle();
Field& field = Field::Handle();
WeakProperty& wp = WeakProperty::Handle();
String& name = String::Handle();
limit = Utils::Minimum(limit, length);
OffsetsTable offsets_table(thread->zone());
for (intptr_t i = 0; i < limit; ++i) {
@ -2416,6 +2441,10 @@ static void PrintRetainingPath(Thread* thread,
(slot_offset.Value() - Array::element_offset(0)) /
Array::kBytesPerElement;
jselement.AddProperty("parentListIndex", element_index);
} else if (element.IsRecord()) {
AddParentFieldToResponseBasedOnRecord(&field_names, &name, jselement,
Record::Cast(element),
slot_offset.Value());
} else if (element.IsLinkedHashMap()) {
map = static_cast<LinkedHashMapPtr>(path.At(i * 2));
map_data = map.data();