diff --git a/runtime/observatory/lib/src/elements/class_view.dart b/runtime/observatory/lib/src/elements/class_view.dart index 998f82d6a91..8c67ebcfd67 100644 --- a/runtime/observatory/lib/src/elements/class_view.dart +++ b/runtime/observatory/lib/src/elements/class_view.dart @@ -15,6 +15,7 @@ import 'package:polymer/polymer.dart'; class ClassViewElement extends ObservatoryElement { @published Class cls; @observable ServiceMap instances; + @observable int reachableBytes; @observable int retainedBytes; @observable ObservableList mostRetained; SampleBufferControlElement sampleBufferControlElement; @@ -44,6 +45,12 @@ class ClassViewElement extends ObservatoryElement { } // TODO(koda): Add no-arg "calculate-link" instead of reusing "eval-link". + Future reachableSize(var dummy) { + return cls.isolate.getReachableSize(cls).then((Instance obj) { + reachableBytes = int.parse(obj.valueAsString); + }); + } + Future retainedSize(var dummy) { return cls.isolate.getRetainedSize(cls).then((Instance obj) { retainedBytes = int.parse(obj.valueAsString); diff --git a/runtime/observatory/lib/src/elements/class_view.html b/runtime/observatory/lib/src/elements/class_view.html index 5dadba6884e..6e5988794b7 100644 --- a/runtime/observatory/lib/src/elements/class_view.html +++ b/runtime/observatory/lib/src/elements/class_view.html @@ -180,6 +180,19 @@ +
+
total reachable memory size
+
+ + +
+
total retained memory size
diff --git a/runtime/observatory/lib/src/elements/object_common.dart b/runtime/observatory/lib/src/elements/object_common.dart index eea85ffcaf0..1deb5d60231 100644 --- a/runtime/observatory/lib/src/elements/object_common.dart +++ b/runtime/observatory/lib/src/elements/object_common.dart @@ -15,10 +15,18 @@ class ObjectCommonElement extends ObservatoryElement { @published ServiceMap path; @published ServiceMap inboundReferences; @observable int retainedBytes = null; + @observable int reachableBytes = null; ObjectCommonElement.created() : super.created(); // TODO(koda): Add no-arg "calculate-link" instead of reusing "eval-link". + Future reachableSize(var dummy) { + return object.isolate.getReachableSize(object).then((Instance obj) { + // TODO(turnidge): Handle collected/expired objects gracefully. + reachableBytes = int.parse(obj.valueAsString); + }); + } + Future retainedSize(var dummy) { return object.isolate.getRetainedSize(object).then((Instance obj) { // TODO(turnidge): Handle collected/expired objects gracefully. diff --git a/runtime/observatory/lib/src/elements/object_common.html b/runtime/observatory/lib/src/elements/object_common.html index 7cad87a8fb2..14311e072e2 100644 --- a/runtime/observatory/lib/src/elements/object_common.html +++ b/runtime/observatory/lib/src/elements/object_common.html @@ -26,6 +26,20 @@
{{ object.size | formatSize }}
+
+
reachable size
+
+ + +
+
+
retained size
diff --git a/runtime/observatory/lib/src/service/object.dart b/runtime/observatory/lib/src/service/object.dart index f7775c16a62..8f5f7a40fd9 100644 --- a/runtime/observatory/lib/src/service/object.dart +++ b/runtime/observatory/lib/src/service/object.dart @@ -1688,6 +1688,13 @@ class Isolate extends ServiceObjectOwner with Coverage { return invokeRpc('evaluateInFrame', params); } + Future getReachableSize(ServiceObject target) { + Map params = { + 'targetId': target.id, + }; + return invokeRpc('_getReachableSize', params); + } + Future getRetainedSize(ServiceObject target) { Map params = { 'targetId': target.id, diff --git a/runtime/observatory/tests/service/reachable_size_test.dart b/runtime/observatory/tests/service/reachable_size_test.dart new file mode 100644 index 00000000000..5c346e351ef --- /dev/null +++ b/runtime/observatory/tests/service/reachable_size_test.dart @@ -0,0 +1,71 @@ +// Copyright (c) 2015, 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=--error_on_bad_type --error_on_bad_override + +import 'package:observatory/service_io.dart'; +import 'package:unittest/unittest.dart'; +import 'test_helper.dart'; + +class Pair { + var x, y; +} + +var p1; +var p2; + +buildGraph() { + p1 = new Pair(); + p2 = new Pair(); + + // Adds to both reachable and retained size. + p1.x = new List(); + p2.x = new List(); + + // Adds to reachable size only. + p1.y = p2.y = new List(); +} + +getReachableSize(ServiceObject obj) { + return obj.isolate.getReachableSize(obj).then((Instance obj) { + return int.parse(obj.valueAsString); + }); +} + +getRetainedSize(ServiceObject obj) { + return obj.isolate.getRetainedSize(obj).then((Instance obj) { + return int.parse(obj.valueAsString); + }); +} + +var tests = [ +(Isolate isolate) async { + Instance p1 = await rootLibraryFieldValue(isolate, "p1"); + Instance p2 = await rootLibraryFieldValue(isolate, "p2"); + + // In general, shallow <= retained <= reachable. In this program, + // 0 < shallow < retained < reachable. + + int p1_shallow = p1.size; + int p1_retained = await getRetainedSize(p1); + int p1_reachable = await getReachableSize(p1); + + expect(0, lessThan(p1_shallow)); + expect(p1_shallow, lessThan(p1_retained)); + expect(p1_retained, lessThan(p1_reachable)); + + int p2_shallow = p2.size; + int p2_retained = await getRetainedSize(p2); + int p2_reachable = await getReachableSize(p2); + + expect(0, lessThan(p2_shallow)); + expect(p2_shallow, lessThan(p2_retained)); + expect(p2_retained, lessThan(p2_reachable)); + + expect(p1_shallow, equals(p2_shallow)); + expect(p1_retained, equals(p2_retained)); + expect(p1_reachable, equals(p2_reachable)); +}, +]; + +main(args) => runIsolateTests(args, tests, testeeBefore: buildGraph); diff --git a/runtime/observatory/tests/service/test_helper.dart b/runtime/observatory/tests/service/test_helper.dart index 6248b43ffaf..58de4e660f9 100644 --- a/runtime/observatory/tests/service/test_helper.dart +++ b/runtime/observatory/tests/service/test_helper.dart @@ -319,6 +319,17 @@ Future getClassFromRootLib(Isolate isolate, String className) async { } +Future rootLibraryFieldValue(Isolate isolate, + String fieldName) async { + Library rootLib = await isolate.rootLibrary.load(); + Field field = rootLib.variables.singleWhere((v) => v.name == fieldName); + await field.load(); + Instance value = field.staticValue; + await value.load(); + return value; +} + + /// Runs [tests] in sequence, each of which should take an [Isolate] and /// return a [Future]. Code for setting up state can run before and/or /// concurrently with the tests. Uses [mainArgs] to determine whether diff --git a/runtime/vm/object_graph.cc b/runtime/vm/object_graph.cc index 2bc0d664374..ae1b1194b0d 100644 --- a/runtime/vm/object_graph.cc +++ b/runtime/vm/object_graph.cc @@ -186,7 +186,41 @@ void ObjectGraph::IterateObjectsFrom(const Object& root, RawObject* root_raw = root.raw(); stack.VisitPointer(&root_raw); stack.TraverseGraph(visitor); - // TODO(koda): Optimize if we only visited a small subgraph. + Unmarker::UnmarkAll(isolate()); +} + + +class InstanceAccumulator : public ObjectVisitor { + public: + explicit InstanceAccumulator(ObjectGraph::Stack* stack, + intptr_t class_id, + Isolate* isolate) + : ObjectVisitor(isolate), stack_(stack), class_id_(class_id) { } + + void VisitObject(RawObject* obj) { + if (obj->GetClassId() == class_id_) { + RawObject* rawobj = obj; + stack_->VisitPointer(&rawobj); + } + } + + private: + ObjectGraph::Stack* stack_; + const intptr_t class_id_; + + DISALLOW_COPY_AND_ASSIGN(InstanceAccumulator); +}; + + +void ObjectGraph::IterateObjectsFrom(intptr_t class_id, + ObjectGraph::Visitor* visitor) { + NoSafepointScope no_safepoint_scope_; + Stack stack(isolate()); + + InstanceAccumulator accumulator(&stack, class_id, isolate()); + isolate()->heap()->IterateObjects(&accumulator); + + stack.TraverseGraph(visitor); Unmarker::UnmarkAll(isolate()); } @@ -240,6 +274,13 @@ intptr_t ObjectGraph::SizeRetainedByInstance(const Object& obj) { } +intptr_t ObjectGraph::SizeReachableByInstance(const Object& obj) { + SizeVisitor total; + IterateObjectsFrom(obj, &total); + return total.size(); +} + + intptr_t ObjectGraph::SizeRetainedByClass(intptr_t class_id) { SizeVisitor total; IterateObjects(&total); @@ -251,6 +292,13 @@ intptr_t ObjectGraph::SizeRetainedByClass(intptr_t class_id) { } +intptr_t ObjectGraph::SizeReachableByClass(intptr_t class_id) { + SizeVisitor total; + IterateObjectsFrom(class_id, &total); + return total.size(); +} + + class RetainingPathVisitor : public ObjectGraph::Visitor { public: // We cannot use a GrowableObjectArray, since we must not trigger GC. diff --git a/runtime/vm/object_graph.h b/runtime/vm/object_graph.h index 0a65bae8dd3..5187d5e2d8b 100644 --- a/runtime/vm/object_graph.h +++ b/runtime/vm/object_graph.h @@ -63,12 +63,15 @@ class ObjectGraph : public StackResource { // Like 'IterateObjects', but restricted to objects reachable from 'root' // (including 'root' itself). void IterateObjectsFrom(const Object& root, Visitor* visitor); + void IterateObjectsFrom(intptr_t class_id, Visitor* visitor); // The number of bytes retained by 'obj'. intptr_t SizeRetainedByInstance(const Object& obj); + intptr_t SizeReachableByInstance(const Object& obj); // The number of bytes retained by the set of all objects of the given class. intptr_t SizeRetainedByClass(intptr_t class_id); + intptr_t SizeReachableByClass(intptr_t class_id); // Finds some retaining path from the isolate roots to 'obj'. Populates the // provided array with pairs of (object, offset from parent in words), diff --git a/runtime/vm/raw_object.h b/runtime/vm/raw_object.h index 9b335ebfff5..2c2da5835ec 100644 --- a/runtime/vm/raw_object.h +++ b/runtime/vm/raw_object.h @@ -614,6 +614,7 @@ class RawObject { friend class Scavenger; friend class ScavengerVisitor; friend class SizeExcludingClassVisitor; // GetClassId + friend class InstanceAccumulator; // GetClassId friend class RetainingPathVisitor; // GetClassId friend class SkippedCodeFunctions; // StorePointer friend class InstructionsReader; // tags_ check diff --git a/runtime/vm/service.cc b/runtime/vm/service.cc index 454726c3fd9..39f4fec6246 100644 --- a/runtime/vm/service.cc +++ b/runtime/vm/service.cc @@ -1764,16 +1764,14 @@ static bool GetRetainingPath(Thread* thread, JSONStream* js) { static const MethodParameter* get_retained_size_params[] = { ISOLATE_PARAMETER, + new IdParameter("targetId", true), NULL, }; static bool GetRetainedSize(Thread* thread, JSONStream* js) { const char* target_id = js->LookupParam("targetId"); - if (target_id == NULL) { - PrintMissingParamError(js, "targetId"); - return true; - } + ASSERT(target_id != NULL); ObjectIdRing::LookupResult lookup_result; Object& obj = Object::Handle(LookupHeapObject(thread, target_id, &lookup_result)); @@ -1806,6 +1804,48 @@ static bool GetRetainedSize(Thread* thread, JSONStream* js) { } +static const MethodParameter* get_reachable_size_params[] = { + ISOLATE_PARAMETER, + new IdParameter("targetId", true), + NULL, +}; + + +static bool GetReachableSize(Thread* thread, JSONStream* js) { + const char* target_id = js->LookupParam("targetId"); + ASSERT(target_id != NULL); + ObjectIdRing::LookupResult lookup_result; + Object& obj = Object::Handle(LookupHeapObject(thread, target_id, + &lookup_result)); + if (obj.raw() == Object::sentinel().raw()) { + if (lookup_result == ObjectIdRing::kCollected) { + PrintSentinel(js, kCollectedSentinel); + } else if (lookup_result == ObjectIdRing::kExpired) { + PrintSentinel(js, kExpiredSentinel); + } else { + PrintInvalidParamError(js, "targetId"); + } + return true; + } + // TODO(rmacnak): There is no way to get the size retained by a class object. + // SizeRetainedByClass should be a separate RPC. + if (obj.IsClass()) { + const Class& cls = Class::Cast(obj); + ObjectGraph graph(thread); + intptr_t retained_size = graph.SizeReachableByClass(cls.id()); + const Object& result = Object::Handle(Integer::New(retained_size)); + result.PrintJSON(js, true); + return true; + } + + ObjectGraph graph(thread); + intptr_t retained_size = graph.SizeReachableByInstance(obj); + const Object& result = Object::Handle(Integer::New(retained_size)); + result.PrintJSON(js, true); + return true; +} + + static const MethodParameter* evaluate_params[] = { ISOLATE_PARAMETER, NULL, @@ -3455,6 +3495,8 @@ static const ServiceMethodDescriptor service_methods_[] = { get_object_by_address_params }, { "_getPorts", GetPorts, get_ports_params }, + { "_getReachableSize", GetReachableSize, + get_reachable_size_params }, { "_getRetainedSize", GetRetainedSize, get_retained_size_params }, { "_getRetainingPath", GetRetainingPath,