From f546ec1a5d5f3fc04969b7c3600aa258f9800154 Mon Sep 17 00:00:00 2001 From: Alexey Kozyatinskiy Date: Fri, 18 Aug 2017 17:27:52 -0700 Subject: [PATCH] [inspector] added Runtime.queryObjects Runtime.queryObjects method: 1. force gc, 2. iterate through heap and get all objects with passed constructorName or with passed constructor name in prototype chain, 3. return these objects as JSArray. Main use case is regression tests for memory leaks. R=pfeldman@chromium.org,alph@chromium.org,ulan@chromium.org Bug: v8:6732 Cq-Include-Trybots: master.tryserver.blink:linux_trusty_blink_rel;master.tryserver.chromium.linux:linux_chromium_rel_ng Change-Id: I52f0803366f14bb24376653615d870a4f21f83e7 Reviewed-on: https://chromium-review.googlesource.com/619594 Reviewed-by: Yang Guo Reviewed-by: Alexei Filippov Reviewed-by: Ulan Degenbaev Reviewed-by: Pavel Feldman Commit-Queue: Aleksey Kozyatinskiy Cr-Commit-Position: refs/heads/master@{#47478} --- src/api.cc | 9 ++ src/debug/debug-interface.h | 14 +++ src/inspector/js_protocol.json | 10 ++ src/inspector/v8-debugger.cc | 79 ++++++++++++++ src/inspector/v8-debugger.h | 3 + src/inspector/v8-runtime-agent-impl.cc | 16 +++ src/inspector/v8-runtime-agent-impl.h | 3 + src/objects.cc | 2 + src/profiler/heap-profiler.cc | 32 ++++++ src/profiler/heap-profiler.h | 4 + .../runtime/query-objects-expected.txt | 73 +++++++++++++ test/inspector/runtime/query-objects.js | 102 ++++++++++++++++++ 12 files changed, 347 insertions(+) create mode 100644 test/inspector/runtime/query-objects-expected.txt create mode 100644 test/inspector/runtime/query-objects.js diff --git a/src/api.cc b/src/api.cc index 94feb7a8a6..0f4f29eeb7 100644 --- a/src/api.cc +++ b/src/api.cc @@ -9987,6 +9987,15 @@ v8::Local debug::GeneratorObject::Cast( return ToApiHandle(Utils::OpenHandle(*value)); } +void debug::QueryObjects(v8::Local v8_context, + QueryObjectPredicate* predicate, + PersistentValueVector* objects) { + i::Isolate* isolate = reinterpret_cast(v8_context->GetIsolate()); + ENTER_V8_NO_SCRIPT_NO_EXCEPTION(isolate); + isolate->heap_profiler()->QueryObjects(Utils::OpenHandle(*v8_context), + predicate, objects); +} + Local CpuProfileNode::GetFunctionName() const { const i::ProfileNode* node = reinterpret_cast(this); i::Isolate* isolate = node->isolate(); diff --git a/src/debug/debug-interface.h b/src/debug/debug-interface.h index fc762bbf2d..5d77e99cca 100644 --- a/src/debug/debug-interface.h +++ b/src/debug/debug-interface.h @@ -391,6 +391,20 @@ class StackTraceIterator { DISALLOW_COPY_AND_ASSIGN(StackTraceIterator); }; +class QueryObjectPredicate { + public: + virtual ~QueryObjectPredicate() = default; + virtual bool Filter(v8::Local object) = 0; + + protected: + // This method can be used only inside of Filter function. + v8::MaybeLocal GetConstructor(v8::Object* object); +}; + +void QueryObjects(v8::Local context, + QueryObjectPredicate* predicate, + v8::PersistentValueVector* objects); + } // namespace debug } // namespace v8 diff --git a/src/inspector/js_protocol.json b/src/inspector/js_protocol.json index 446d51e0d2..8b2aaab930 100644 --- a/src/inspector/js_protocol.json +++ b/src/inspector/js_protocol.json @@ -343,6 +343,16 @@ { "name": "exceptionDetails", "$ref": "ExceptionDetails", "optional": true, "description": "Exception details."} ], "description": "Runs script with given id in a given context." + }, + { + "name": "queryObjects", + "parameters": [ + { "name": "constructorObjectId", "$ref": "RemoteObjectId", "description": "Identifier of the constructor to return objects for." } + ], + "returns": [ + { "name": "objects", "$ref": "RemoteObject", "description": "Array with objects." } + ], + "experimental": true } ], "events": [ diff --git a/src/inspector/v8-debugger.cc b/src/inspector/v8-debugger.cc index 55b203be71..3b352573d8 100644 --- a/src/inspector/v8-debugger.cc +++ b/src/inspector/v8-debugger.cc @@ -4,6 +4,8 @@ #include "src/inspector/v8-debugger.h" +#include + #include "src/inspector/inspected-context.h" #include "src/inspector/protocol/Protocol.h" #include "src/inspector/script-breakpoint.h" @@ -130,6 +132,65 @@ void cleanupExpiredWeakPointers(Map& map) { } } +class QueryObjectPredicate : public v8::debug::QueryObjectPredicate { + public: + QueryObjectPredicate(v8::Local context, + v8::Local constructor) + : m_context(context), m_constructor(constructor) {} + + bool Filter(v8::Local object) override { + if (CheckObject(*object)) return true; + std::vector prototypeChain; + v8::Local prototype; + // Get prototype chain for current object until first visited prototype. + for (prototype = object->GetPrototype(); IsUnvisitedPrototype(prototype); + prototype = prototype.As()->GetPrototype()) { + prototypeChain.push_back(v8::Object::Cast(*prototype)); + } + // Include first visited prototype if any. + if (prototype->IsObject()) { + prototypeChain.push_back(v8::Object::Cast(*prototype)); + } + bool hasMatched = false; + // Go from last prototype to first one, mark all prototypes as matched after + // first matched prototype. + for (auto it = prototypeChain.rbegin(); it != prototypeChain.rend(); ++it) { + hasMatched = hasMatched || CheckObject(*it); + if (hasMatched) m_matchedPrototypes.insert(*it); + m_visitedPrototypes.insert(*it); + } + return hasMatched; + } + + private: + bool CheckObject(v8::Object* object) { + if (m_matchedPrototypes.find(object) != m_matchedPrototypes.end()) + return true; + if (m_visitedPrototypes.find(object) != m_visitedPrototypes.end()) + return false; + v8::Local objectContext = object->CreationContext(); + if (objectContext != m_context) return false; + v8::Local constructor; + if (!GetConstructor(object).ToLocal(&constructor)) return false; + return constructor == m_constructor; + } + + bool IsUnvisitedPrototype(v8::Local prototypeValue) { + if (!prototypeValue->IsObject()) return false; + v8::Object* prototypeObject = v8::Object::Cast(*prototypeValue); + v8::Local prototypeContext = + prototypeObject->CreationContext(); + if (prototypeContext != m_context) return false; + return m_visitedPrototypes.find(prototypeObject) == + m_visitedPrototypes.end(); + } + + v8::Local m_context; + v8::Local m_constructor; + std::unordered_set m_visitedPrototypes; + std::unordered_set m_matchedPrototypes; +}; + } // namespace V8Debugger::V8Debugger(v8::Isolate* isolate, V8InspectorImpl* inspector) @@ -661,6 +722,24 @@ v8::MaybeLocal V8Debugger::internalProperties( return properties; } +v8::Local V8Debugger::queryObjects( + v8::Local context, v8::Local constructor) { + v8::Isolate* isolate = context->GetIsolate(); + v8::PersistentValueVector v8Objects(isolate); + QueryObjectPredicate predicate(context, constructor); + v8::debug::QueryObjects(context, &predicate, &v8Objects); + + v8::MicrotasksScope microtasksScope(isolate, + v8::MicrotasksScope::kDoNotRunMicrotasks); + v8::Local resultArray = v8::Array::New( + m_inspector->isolate(), static_cast(v8Objects.Size())); + for (size_t i = 0; i < v8Objects.Size(); ++i) { + createDataProperty(context, resultArray, static_cast(i), + v8Objects.Get(i)); + } + return resultArray; +} + std::unique_ptr V8Debugger::createStackTrace( v8::Local v8StackTrace) { return V8StackTraceImpl::create(this, currentContextGroupId(), v8StackTrace, diff --git a/src/inspector/v8-debugger.h b/src/inspector/v8-debugger.h index fe6495dc5d..f7b2b07a97 100644 --- a/src/inspector/v8-debugger.h +++ b/src/inspector/v8-debugger.h @@ -88,6 +88,9 @@ class V8Debugger : public v8::debug::DebugDelegate { v8::MaybeLocal internalProperties(v8::Local, v8::Local); + v8::Local queryObjects(v8::Local context, + v8::Local constructor); + void asyncTaskScheduled(const StringView& taskName, void* task, bool recurring); void asyncTaskCanceled(void* task); diff --git a/src/inspector/v8-runtime-agent-impl.cc b/src/inspector/v8-runtime-agent-impl.cc index 2641e1f4dc..f1861d1d37 100644 --- a/src/inspector/v8-runtime-agent-impl.cc +++ b/src/inspector/v8-runtime-agent-impl.cc @@ -42,6 +42,7 @@ #include "src/inspector/v8-inspector-impl.h" #include "src/inspector/v8-inspector-session-impl.h" #include "src/inspector/v8-stack-trace-impl.h" +#include "src/inspector/v8-value-utils.h" #include "src/tracing/trace-event.h" #include "include/v8-inspector.h" @@ -521,6 +522,21 @@ void V8RuntimeAgentImpl::runScript( EvaluateCallbackWrapper::wrap(std::move(callback))); } +Response V8RuntimeAgentImpl::queryObjects( + const String16& constructorObjectId, + std::unique_ptr* objects) { + InjectedScript::ObjectScope scope(m_session, constructorObjectId); + Response response = scope.initialize(); + if (!response.isSuccess()) return response; + if (!scope.object()->IsFunction()) { + return Response::Error("Constructor should be instance of Function"); + } + v8::Local resultArray = m_inspector->debugger()->queryObjects( + scope.context(), v8::Local::Cast(scope.object())); + return scope.injectedScript()->wrapObject( + resultArray, scope.objectGroupName(), false, false, objects); +} + void V8RuntimeAgentImpl::restore() { if (!m_state->booleanProperty(V8RuntimeAgentImplState::runtimeEnabled, false)) return; diff --git a/src/inspector/v8-runtime-agent-impl.h b/src/inspector/v8-runtime-agent-impl.h index 9caa1fba47..f5c9da6ba8 100644 --- a/src/inspector/v8-runtime-agent-impl.h +++ b/src/inspector/v8-runtime-agent-impl.h @@ -97,6 +97,9 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend { Maybe includeCommandLineAPI, Maybe returnByValue, Maybe generatePreview, Maybe awaitPromise, std::unique_ptr) override; + Response queryObjects( + const String16& constructorObjectId, + std::unique_ptr* objects) override; void reset(); void reportExecutionContextCreated(InspectedContext*); diff --git a/src/objects.cc b/src/objects.cc index 71a75e57bb..e2971fdc53 100644 --- a/src/objects.cc +++ b/src/objects.cc @@ -3541,6 +3541,8 @@ Handle JSReceiver::GetCreationContext() { while (receiver->IsJSBoundFunction()) { receiver = JSBoundFunction::cast(receiver)->bound_target_function(); } + // Externals are JSObjects with null as a constructor. + DCHECK(!receiver->IsExternal()); Object* constructor = receiver->map()->GetConstructor(); JSFunction* function; if (constructor->IsJSFunction()) { diff --git a/src/profiler/heap-profiler.cc b/src/profiler/heap-profiler.cc index 90dff7d635..f7333d6502 100644 --- a/src/profiler/heap-profiler.cc +++ b/src/profiler/heap-profiler.cc @@ -12,6 +12,19 @@ #include "src/profiler/sampling-heap-profiler.h" namespace v8 { + +v8::MaybeLocal debug::QueryObjectPredicate::GetConstructor( + v8::Object* v8_object) { + internal::Handle object(Utils::OpenHandle(v8_object)); + internal::Handle map(object->map()); + internal::Object* maybe_constructor = map->GetConstructor(); + if (maybe_constructor->IsJSFunction()) { + return Utils::ToLocal( + internal::handle(internal::JSFunction::cast(maybe_constructor))); + } + return v8::MaybeLocal(); +} + namespace internal { HeapProfiler::HeapProfiler(Heap* heap) @@ -207,6 +220,25 @@ void HeapProfiler::ClearHeapObjectMap() { Heap* HeapProfiler::heap() const { return ids_->heap(); } +void HeapProfiler::QueryObjects(Handle context, + debug::QueryObjectPredicate* predicate, + PersistentValueVector* objects) { + // We should return accurate information about live objects, so we need to + // collect all garbage first. + isolate()->heap()->CollectAllAvailableGarbage( + GarbageCollectionReason::kLowMemoryNotification); + heap()->CollectAllGarbage(Heap::kMakeHeapIterableMask, + GarbageCollectionReason::kHeapProfiler); + HeapIterator heap_iterator(heap(), HeapIterator::kFilterUnreachable); + HeapObject* heap_obj; + while ((heap_obj = heap_iterator.next()) != nullptr) { + if (!heap_obj->IsJSObject() || heap_obj->IsExternal()) continue; + v8::Local v8_obj( + Utils::ToLocal(handle(JSObject::cast(heap_obj)))); + if (!predicate->Filter(v8_obj)) continue; + objects->Append(v8_obj); + } +} } // namespace internal } // namespace v8 diff --git a/src/profiler/heap-profiler.h b/src/profiler/heap-profiler.h index dc4b6104ee..354c48ea54 100644 --- a/src/profiler/heap-profiler.h +++ b/src/profiler/heap-profiler.h @@ -76,6 +76,10 @@ class HeapProfiler { Isolate* isolate() const { return heap()->isolate(); } + void QueryObjects(Handle context, + debug::QueryObjectPredicate* predicate, + v8::PersistentValueVector* objects); + private: Heap* heap() const; diff --git a/test/inspector/runtime/query-objects-expected.txt b/test/inspector/runtime/query-objects-expected.txt new file mode 100644 index 0000000000..fb42b08973 --- /dev/null +++ b/test/inspector/runtime/query-objects-expected.txt @@ -0,0 +1,73 @@ +Checks Runtime.queryObjects + +Running test: testClass +Declare class Foo & store its constructor. +Create object with class Foo. +Query objects with Foo constructor. +Dump each object constructor name. +[ + [0] : Foo + [1] : Foo +] +Create object with class Foo. +Query objects with Foo constructor. +Dump each object constructor name. +[ + [0] : Foo + [1] : Foo + [2] : Foo +] + +Running test: testDerivedNewClass +Declare class Foo & store its constructor. +Declare class Boo extends Foo & store its constructor. +Query objects with Foo constructor. +Dump each object constructor name. +[ + [0] : Boo + [1] : Foo +] +Query objects with Boo constructor. +Dump each object constructor name. +[ + [0] : Boo +] +Create object with class Foo +Query objects with Foo constructor. +Dump each object constructor name. +[ + [0] : Boo + [1] : Foo + [2] : Foo +] +Create object with class Boo +Query objects with Foo constructor. +Dump each object constructor name. +[ + [0] : Boo + [1] : Boo + [2] : Foo + [3] : Foo +] +Query objects with Boo constructor. +Dump each object constructor name. +[ + [0] : Boo + [1] : Boo +] + +Running test: testNewFunction +Declare Foo & store it. +Create object using Foo. +Query objects with Foo constructor. +Dump each object constructor name. +[ + [0] : Foo +] +Create object using Foo. +Query objects with Foo constructor. +Dump each object constructor name. +[ + [0] : Foo + [1] : Foo +] diff --git a/test/inspector/runtime/query-objects.js b/test/inspector/runtime/query-objects.js new file mode 100644 index 0000000000..5b31ef1ae3 --- /dev/null +++ b/test/inspector/runtime/query-objects.js @@ -0,0 +1,102 @@ +// Copyright 2017 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +let {session, contextGroup, Protocol} = + InspectorTest.start('Checks Runtime.queryObjects'); + +InspectorTest.runAsyncTestSuite([ + async function testClass() { + let contextGroup = new InspectorTest.ContextGroup(); + let session = contextGroup.connect(); + let Protocol = session.Protocol; + + InspectorTest.log('Declare class Foo & store its constructor.'); + await Protocol.Runtime.evaluate({ + expression: 'class Foo{constructor(){}};' + }); + let {result:{result:{objectId}}} = await Protocol.Runtime.evaluate({ + expression: 'Foo' + }); + + for (let i = 0; i < 2; ++i) { + InspectorTest.log('Create object with class Foo.'); + Protocol.Runtime.evaluate({expression: 'new Foo()'}); + await queryObjects(session, objectId, 'Foo'); + } + + session.disconnect(); + }, + + async function testDerivedNewClass() { + let contextGroup = new InspectorTest.ContextGroup(); + let session = contextGroup.connect(); + let Protocol = session.Protocol; + + InspectorTest.log('Declare class Foo & store its constructor.'); + Protocol.Runtime.evaluate({expression: 'class Foo{};'}); + let {result:{result:{objectId}}} = await Protocol.Runtime.evaluate({ + expression: 'Foo' + }); + let fooConstructorId = objectId; + + InspectorTest.log('Declare class Boo extends Foo & store its constructor.'); + Protocol.Runtime.evaluate({expression: 'class Boo extends Foo{};'}); + ({result:{result:{objectId}}} = await Protocol.Runtime.evaluate({ + expression: 'Boo' + })); + let booConstructorId = objectId; + + await queryObjects(session, fooConstructorId, 'Foo'); + await queryObjects(session, booConstructorId, 'Boo'); + + InspectorTest.log('Create object with class Foo'); + Protocol.Runtime.evaluate({expression: 'new Foo()'}); + await queryObjects(session, fooConstructorId, 'Foo'); + + InspectorTest.log('Create object with class Boo'); + Protocol.Runtime.evaluate({expression: 'new Boo()'}); + await queryObjects(session, fooConstructorId, 'Foo'); + await queryObjects(session, booConstructorId, 'Boo'); + + session.disconnect(); + }, + + async function testNewFunction() { + let contextGroup = new InspectorTest.ContextGroup(); + let session = contextGroup.connect(); + let Protocol = session.Protocol; + + InspectorTest.log('Declare Foo & store it.'); + Protocol.Runtime.evaluate({expression: 'function Foo(){}'}); + let {result:{result:{objectId}}} = await Protocol.Runtime.evaluate({ + expression: 'Foo' + }); + + for (let i = 0; i < 2; ++i) { + InspectorTest.log('Create object using Foo.'); + Protocol.Runtime.evaluate({expression: 'new Foo()'}); + await queryObjects(session, objectId, 'Foo'); + } + session.disconnect(); + } +]); + +const constructorsNameFunction = ` +function() { + return this.map(o => o.constructor.name).sort(); +}`; + +async function queryObjects(sesion, constructorObjectId, name) { + let {result:{objects}} = await sesion.Protocol.Runtime.queryObjects({ + constructorObjectId + }); + InspectorTest.log(`Query objects with ${name} constructor.`); + let {result:{result:{value}}} = await sesion.Protocol.Runtime.callFunctionOn({ + objectId: objects.objectId, + functionDeclaration: constructorsNameFunction, + returnByValue: true + }); + InspectorTest.log('Dump each object constructor name.'); + InspectorTest.logMessage(value); +}