Skip to content

Commit

Permalink
src: implement countObjectsWithPrototype
Browse files Browse the repository at this point in the history
This implements an internal utility for counting objects
in the heap with a specified prototype. In addition this
adds a checkIfCollectableByCounting() test helper.
  • Loading branch information
joyeecheung committed Nov 24, 2023
1 parent ca63e76 commit 1890d72
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 0 deletions.
39 changes: 39 additions & 0 deletions src/heap_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::String;
using v8::Symbol;
using v8::TryCatch;
using v8::Uint8Array;
using v8::Value;

Expand Down Expand Up @@ -474,6 +476,40 @@ void TriggerHeapSnapshot(const FunctionCallbackInfo<Value>& args) {
return args.GetReturnValue().Set(filename_v);
}

class PrototypeChainHas : public v8::QueryObjectPredicate {
public:
PrototypeChainHas(Local<Context> context, Local<Object> search)
: context_(context),
search_(search) {}

// What we can do in the filter can be quite limited, but looking up
// the prototype chain is something that the inspector console API
// queryObject() does so it is supported.
bool Filter(Local<Object> object) override {
for (Local<Value> proto = object->GetPrototype(); proto->IsObject();
proto = proto.As<Object>()->GetPrototype()) {
if (search_ == proto) return true;
}
return false;
}

private:
Local<Context> context_;
Local<Object> search_;
};

void CountObjectsWithPrototype(const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsObject());
Local<Object> proto = args[0].As<Object>();
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
PrototypeChainHas prototype_chain_has(context, proto);
std::vector<Global<Object>> out;
isolate->GetHeapProfiler()->QueryObjects(context, &prototype_chain_has, &out);
args.GetReturnValue().Set(static_cast<uint32_t>(out.size()));
}

void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
Expand All @@ -482,12 +518,15 @@ void Initialize(Local<Object> target,
SetMethod(context, target, "triggerHeapSnapshot", TriggerHeapSnapshot);
SetMethod(
context, target, "createHeapSnapshotStream", CreateHeapSnapshotStream);
SetMethod(
context, target, "countObjectsWithPrototype", CountObjectsWithPrototype);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(BuildEmbedderGraph);
registry->Register(TriggerHeapSnapshot);
registry->Register(CreateHeapSnapshotStream);
registry->Register(CountObjectsWithPrototype);
}

} // namespace heap
Expand Down
47 changes: 47 additions & 0 deletions test/common/gc.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,54 @@ async function runAndBreathe(fn, repeat, waitTime = 20) {
}
}

/**
* This requires --expose-internals.
* This function can be used to check if an object factory leaks or not by
* iterating over the heap and count objects with the specified class
* (which is checked by looking up the prototype chain).
* @param {(i: number) => number} fn The factory receiving iteration count
* and returning number of objects created. The return value should be
* precise otherwise false negatives can be produced.
* @param {Function} klass The class whose object is used to count the objects
* @param {number} count Number of iterations that this check should be done
* @param {number} waitTime Optional breathing time for GC.
*/
async function checkIfCollectableByCounting(fn, klass, count, waitTime = 20) {
const { internalBinding } = require('internal/test/binding');
const { countObjectsWithPrototype } = internalBinding('heap_utils');
const { prototype, name } = klass;
let initialCount = countObjectsWithPrototype(prototype);
console.log(`Initial count of ${name}: ${initialCount}`);
let totalCreated = 0;
for (let i = 0; i < count; ++i) {
const created = await fn(i);
totalCreated += created;
console.log(`#${i}: created ${created} ${name}, total ${totalCreated}`);
await wait(waitTime); // give GC some breathing room.
const currentCount = countObjectsWithPrototype(prototype);
const collected = totalCreated - (currentCount - initialCount);
console.log(`#${i}: counted ${currentCount} ${name}, collected ${collected}`);
if (collected > 0 ) {
console.log(`Detected ${collected} collected ${name}, finish early`);
return;
}
}

await wait(waitTime); // give GC some breathing room.
const currentCount = countObjectsWithPrototype(prototype);
const collected = totalCreated - (currentCount - initialCount);
console.log(`Last count: counted ${currentCount} ${name}, collected ${collected}`);
// Some objects with the prototype can be collected.
if (collected > 0) {
console.log(`Detected ${collected} collected ${name}`);
return;
}

throw new Error(`${name} cannot be collected`);
}

module.exports = {
checkIfCollectable,
runAndBreathe,
checkIfCollectableByCounting,
};

0 comments on commit 1890d72

Please sign in to comment.