Add RPC and UI for reachable size of an object / all instances of a class.
BUG=http://dartbug.com/24945 R=johnmccutchan@google.com Review URL: https://codereview.chromium.org/1453983002 .
This commit is contained in:
@@ -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<ServiceObject> reachableSize(var dummy) {
|
||||
return cls.isolate.getReachableSize(cls).then((Instance obj) {
|
||||
reachableBytes = int.parse(obj.valueAsString);
|
||||
});
|
||||
}
|
||||
|
||||
Future<ServiceObject> retainedSize(var dummy) {
|
||||
return cls.isolate.getRetainedSize(cls).then((Instance obj) {
|
||||
retainedBytes = int.parse(obj.valueAsString);
|
||||
|
||||
@@ -180,6 +180,19 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="memberItem">
|
||||
<div class="memberName">total reachable memory size</div>
|
||||
<div class="memberValue">
|
||||
<template if="{{ reachableBytes == null }}">
|
||||
<eval-link callback="{{ reachableSize }}"
|
||||
label="[calculate]">
|
||||
</eval-link>
|
||||
</template>
|
||||
<template if="{{ reachableBytes != null }}">
|
||||
{{ reachableBytes | formatSize }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="memberItem">
|
||||
<div class="memberName">total retained memory size</div>
|
||||
<div class="memberValue">
|
||||
|
||||
@@ -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<ServiceObject> reachableSize(var dummy) {
|
||||
return object.isolate.getReachableSize(object).then((Instance obj) {
|
||||
// TODO(turnidge): Handle collected/expired objects gracefully.
|
||||
reachableBytes = int.parse(obj.valueAsString);
|
||||
});
|
||||
}
|
||||
|
||||
Future<ServiceObject> retainedSize(var dummy) {
|
||||
return object.isolate.getRetainedSize(object).then((Instance obj) {
|
||||
// TODO(turnidge): Handle collected/expired objects gracefully.
|
||||
|
||||
@@ -26,6 +26,20 @@
|
||||
<div class="memberValue">{{ object.size | formatSize }}</div>
|
||||
</div>
|
||||
|
||||
<div class="memberItem" title="Space reachable from this object, excluding class references">
|
||||
<div class="memberName">reachable size</div>
|
||||
<div class="memberValue">
|
||||
<template if="{{ reachableBytes == null }}">
|
||||
<eval-link callback="{{ reachableSize }}"
|
||||
label="[calculate]">
|
||||
</eval-link>
|
||||
</template>
|
||||
<template if="{{ reachableBytes != null }}">
|
||||
{{ reachableBytes | formatSize }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="memberItem" title="Space that would be reclaimed if references to this object were replaced with null">
|
||||
<div class="memberName">retained size</div>
|
||||
<div class="memberValue">
|
||||
|
||||
@@ -1688,6 +1688,13 @@ class Isolate extends ServiceObjectOwner with Coverage {
|
||||
return invokeRpc('evaluateInFrame', params);
|
||||
}
|
||||
|
||||
Future<ServiceObject> getReachableSize(ServiceObject target) {
|
||||
Map params = {
|
||||
'targetId': target.id,
|
||||
};
|
||||
return invokeRpc('_getReachableSize', params);
|
||||
}
|
||||
|
||||
Future<ServiceObject> getRetainedSize(ServiceObject target) {
|
||||
Map params = {
|
||||
'targetId': target.id,
|
||||
|
||||
@@ -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);
|
||||
@@ -319,6 +319,17 @@ Future<Class> getClassFromRootLib(Isolate isolate, String className) async {
|
||||
}
|
||||
|
||||
|
||||
Future<Instance> 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
+46
-4
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user