Skip to content

Commit

Permalink
src: support WeakReference in snapshot
Browse files Browse the repository at this point in the history
Move util::WeakReference to a separate header and implement
{de}serialization for it to be snapshotable.

PR-URL: nodejs#44193
Refs: nodejs#44014
Refs: nodejs#37476
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
  • Loading branch information
joyeecheung committed Aug 25, 2022
1 parent 8959013 commit 8dc939a
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 38 deletions.
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,7 @@
'src/node_stat_watcher.h',
'src/node_union_bytes.h',
'src/node_url.h',
'src/node_util.h',
'src/node_version.h',
'src/node_v8.h',
'src/node_v8_platform-inl.h',
Expand Down
1 change: 1 addition & 0 deletions src/node_snapshotable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "node_metadata.h"
#include "node_process.h"
#include "node_snapshot_builder.h"
#include "node_util.h"
#include "node_v8.h"
#include "node_v8_platform-inl.h"

Expand Down
3 changes: 2 additions & 1 deletion src/node_snapshotable.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class ExternalReferenceRegistry;
V(fs_binding_data, fs::BindingData) \
V(v8_binding_data, v8_utils::BindingData) \
V(blob_binding_data, BlobBindingData) \
V(process_binding_data, process::BindingData)
V(process_binding_data, process::BindingData) \
V(util_weak_reference, util::WeakReference)

enum class EmbedderObjectType : uint8_t {
#define V(PropertyName, NativeType) k_##PropertyName,
Expand Down
129 changes: 92 additions & 37 deletions src/node_util.cc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "node_util.h"
#include "base_object-inl.h"
#include "node_errors.h"
#include "node_external_reference.h"
Expand All @@ -15,7 +16,7 @@ using v8::Context;
using v8::External;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::HandleScope;
using v8::IndexFilter;
using v8::Integer;
using v8::Isolate;
Expand Down Expand Up @@ -207,52 +208,106 @@ void ArrayBufferViewHasBuffer(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(args[0].As<ArrayBufferView>()->HasBuffer());
}

class WeakReference : public BaseObject {
public:
WeakReference(Environment* env, Local<Object> object, Local<Object> target)
: BaseObject(env, object) {
MakeWeak();
WeakReference::WeakReference(Environment* env,
Local<Object> object,
Local<Object> target)
: WeakReference(env, object, target, 0) {}

WeakReference::WeakReference(Environment* env,
Local<Object> object,
Local<Object> target,
uint64_t reference_count)
: SnapshotableObject(env, object, type_int),
reference_count_(reference_count) {
MakeWeak();
if (!target.IsEmpty()) {
target_.Reset(env->isolate(), target);
target_.SetWeak();
if (reference_count_ == 0) {
target_.SetWeak();
}
}
}

static void New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args.IsConstructCall());
CHECK(args[0]->IsObject());
new WeakReference(env, args.This(), args[0].As<Object>());
bool WeakReference::PrepareForSerialization(Local<Context> context,
v8::SnapshotCreator* creator) {
if (target_.IsEmpty()) {
target_index_ = 0;
return true;
}

static void Get(const FunctionCallbackInfo<Value>& args) {
WeakReference* weak_ref = Unwrap<WeakReference>(args.Holder());
Isolate* isolate = args.GetIsolate();
if (!weak_ref->target_.IsEmpty())
args.GetReturnValue().Set(weak_ref->target_.Get(isolate));
}
// Users can still hold strong references to target in addition to the
// reference that we manage here, and they could expect that the referenced
// object remains the same as long as that external strong reference
// is alive. Since we have no way to know if there is any other reference
// keeping the target alive, the best we can do to maintain consistency is to
// simply save a reference to the target in the snapshot (effectively making
// it strong) during serialization, and restore it during deserialization.
// If there's no known counted reference from our side, we'll make the
// reference here weak upon deserialization so that it can be GC'ed if users
// do not hold additional references to it.
Local<Object> target = target_.Get(context->GetIsolate());
target_index_ = creator->AddData(context, target);
DCHECK_NE(target_index_, 0);
target_.Reset();
return true;
}

static void IncRef(const FunctionCallbackInfo<Value>& args) {
WeakReference* weak_ref = Unwrap<WeakReference>(args.Holder());
weak_ref->reference_count_++;
if (weak_ref->target_.IsEmpty()) return;
if (weak_ref->reference_count_ == 1) weak_ref->target_.ClearWeak();
}
InternalFieldInfoBase* WeakReference::Serialize(int index) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
InternalFieldInfo* info =
InternalFieldInfoBase::New<InternalFieldInfo>(type());
info->target = target_index_;
info->reference_count = reference_count_;
return info;
}

static void DecRef(const FunctionCallbackInfo<Value>& args) {
WeakReference* weak_ref = Unwrap<WeakReference>(args.Holder());
CHECK_GE(weak_ref->reference_count_, 1);
weak_ref->reference_count_--;
if (weak_ref->target_.IsEmpty()) return;
if (weak_ref->reference_count_ == 0) weak_ref->target_.SetWeak();
void WeakReference::Deserialize(Local<Context> context,
Local<Object> holder,
int index,
InternalFieldInfoBase* info) {
DCHECK_EQ(index, BaseObject::kEmbedderType);
HandleScope scope(context->GetIsolate());

InternalFieldInfo* weak_info = reinterpret_cast<InternalFieldInfo*>(info);
Local<Object> target;
if (weak_info->target != 0) {
target = context->GetDataFromSnapshotOnce<Object>(weak_info->target)
.ToLocalChecked();
}
new WeakReference(Environment::GetCurrent(context),
holder,
target,
weak_info->reference_count);
}

void WeakReference::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args.IsConstructCall());
CHECK(args[0]->IsObject());
new WeakReference(env, args.This(), args[0].As<Object>());
}

void WeakReference::Get(const FunctionCallbackInfo<Value>& args) {
WeakReference* weak_ref = Unwrap<WeakReference>(args.Holder());
Isolate* isolate = args.GetIsolate();
if (!weak_ref->target_.IsEmpty())
args.GetReturnValue().Set(weak_ref->target_.Get(isolate));
}

SET_MEMORY_INFO_NAME(WeakReference)
SET_SELF_SIZE(WeakReference)
SET_NO_MEMORY_INFO()
void WeakReference::IncRef(const FunctionCallbackInfo<Value>& args) {
WeakReference* weak_ref = Unwrap<WeakReference>(args.Holder());
weak_ref->reference_count_++;
if (weak_ref->target_.IsEmpty()) return;
if (weak_ref->reference_count_ == 1) weak_ref->target_.ClearWeak();
}

private:
Global<Object> target_;
uint64_t reference_count_ = 0;
};
void WeakReference::DecRef(const FunctionCallbackInfo<Value>& args) {
WeakReference* weak_ref = Unwrap<WeakReference>(args.Holder());
CHECK_GE(weak_ref->reference_count_, 1);
weak_ref->reference_count_--;
if (weak_ref->target_.IsEmpty()) return;
if (weak_ref->reference_count_ == 0) weak_ref->target_.SetWeak();
}

static void GuessHandleType(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Expand Down
54 changes: 54 additions & 0 deletions src/node_util.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@

#ifndef SRC_NODE_UTIL_H_
#define SRC_NODE_UTIL_H_

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "base_object.h"
#include "node_snapshotable.h"
#include "v8.h"

namespace node {
namespace util {

class WeakReference : public SnapshotableObject {
public:
SERIALIZABLE_OBJECT_METHODS();

static constexpr FastStringKey type_name{"node::util::WeakReference"};
static constexpr EmbedderObjectType type_int =
EmbedderObjectType::k_util_weak_reference;

WeakReference(Environment* env,
v8::Local<v8::Object> object,
v8::Local<v8::Object> target);
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Get(const v8::FunctionCallbackInfo<v8::Value>& args);
static void IncRef(const v8::FunctionCallbackInfo<v8::Value>& args);
static void DecRef(const v8::FunctionCallbackInfo<v8::Value>& args);

SET_MEMORY_INFO_NAME(WeakReference)
SET_SELF_SIZE(WeakReference)
SET_NO_MEMORY_INFO()

struct InternalFieldInfo : public node::InternalFieldInfoBase {
SnapshotIndex target;
uint64_t reference_count;
};

private:
WeakReference(Environment* env,
v8::Local<v8::Object> object,
v8::Local<v8::Object> target,
uint64_t reference_count);
v8::Global<v8::Object> target_;
uint64_t reference_count_ = 0;

SnapshotIndex target_index_ = 0; // 0 means target_ is not snapshotted
};

} // namespace util
} // namespace node

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#endif // SRC_NODE_UTIL_H_
1 change: 1 addition & 0 deletions src/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "node_buffer.h"
#include "node_errors.h"
#include "node_internals.h"
#include "node_util.h"
#include "string_bytes.h"
#include "uv.h"

Expand Down
20 changes: 20 additions & 0 deletions test/fixtures/snapshot/weak-reference-gc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict';

const { internalBinding } = require('internal/test/binding');
const { WeakReference } = internalBinding('util');
const {
setDeserializeMainFunction
} = require('v8').startupSnapshot
const assert = require('assert');

let obj = { hello: 'world' };
const ref = new WeakReference(obj);

setDeserializeMainFunction(() => {
obj = null;
globalThis.gc();

setImmediate(() => {
assert.strictEqual(ref.get(), undefined);
});
});
15 changes: 15 additions & 0 deletions test/fixtures/snapshot/weak-reference.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

const { internalBinding } = require('internal/test/binding');
const { WeakReference } = internalBinding('util');
const {
setDeserializeMainFunction
} = require('v8').startupSnapshot
const assert = require('assert');

let obj = { hello: 'world' };
const ref = new WeakReference(obj);

setDeserializeMainFunction(() => {
assert.strictEqual(ref.get(), obj);
});
60 changes: 60 additions & 0 deletions test/parallel/test-snapshot-weak-reference.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';

// This tests that weak references work across serialization.

require('../common');
const assert = require('assert');
const { spawnSync } = require('child_process');
const tmpdir = require('../common/tmpdir');
const fixtures = require('../common/fixtures');
const path = require('path');
const fs = require('fs');

tmpdir.refresh();
const blobPath = path.join(tmpdir.path, 'snapshot.blob');

function runTest(entry) {
console.log('running test with', entry);
{
const child = spawnSync(process.execPath, [
'--expose-internals',
'--expose-gc',
'--snapshot-blob',
blobPath,
'--build-snapshot',
entry,
], {
cwd: tmpdir.path
});
if (child.status !== 0) {
console.log(child.stderr.toString());
console.log(child.stdout.toString());
assert.strictEqual(child.status, 0);
}
const stats = fs.statSync(path.join(tmpdir.path, 'snapshot.blob'));
assert(stats.isFile());
}

{
const child = spawnSync(process.execPath, [
'--expose-internals',
'--expose-gc',
'--snapshot-blob',
blobPath,
], {
cwd: tmpdir.path,
env: {
...process.env,
}
});

const stdout = child.stdout.toString().trim();
const stderr = child.stderr.toString().trim();
console.log(`[stdout]:\n${stdout}\n`);
console.log(`[stderr]:\n${stderr}\n`);
assert.strictEqual(child.status, 0);
}
}

runTest(fixtures.path('snapshot', 'weak-reference.js'));
runTest(fixtures.path('snapshot', 'weak-reference-gc.js'));

0 comments on commit 8dc939a

Please sign in to comment.