Skip to content

Commit

Permalink
SharedArrayBuffer support in ExternalCopy
Browse files Browse the repository at this point in the history
  • Loading branch information
laverdet committed Feb 28, 2018
1 parent 48cf991 commit a026cc3
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 43 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,13 @@ can then be quickly copied into any isolate.
large object. This only applies to ArrayBuffer and TypedArray instances.

Primitive values can be copied exactly as they are. Date objects will be copied as as Dates.
ArrayBuffers, TypedArrays, and DataViews will be copied in an efficient format. All other objects
will be copied in seralized form using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm).
ArrayBuffers, TypedArrays, and DataViews will be copied in an efficient format. SharedArrayBuffers
will simply copy a reference to the existing memory and when copied into another isolate the new
SharedArrayBuffer will point to the same underlying data. After passing a SharedArrayBuffer to
ExternalCopy for the first time isolated-vm will take over management of the underlying memory
block, so a "copied" SharedArrayBuffer can outlive the isolate that created the memory originally.

All other objects will be copied in seralized form using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm).

##### `ExternalCopy.totalExternalSize` *[number]*

Expand Down
126 changes: 96 additions & 30 deletions src/external_copy.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ unique_ptr<ExternalCopy> ExternalCopy::Copy(const Local<Value>& value, bool tran
if (transfer_out) {
return ExternalCopyArrayBuffer::Transfer(array_buffer);
} else {
ArrayBuffer::Contents contents(array_buffer->GetContents());
return make_unique<ExternalCopyArrayBuffer>(contents.Data(), contents.ByteLength());
return make_unique<ExternalCopyArrayBuffer>(array_buffer);
}
} else if (value->IsSharedArrayBuffer()) {
return make_unique<ExternalCopySharedArrayBuffer>(value.As<SharedArrayBuffer>());
} else if (value->IsArrayBufferView()) {
Local<ArrayBufferView> view(Local<ArrayBufferView>::Cast(value));
Local<ArrayBufferView> view = value.As<ArrayBufferView>();
using ViewType = ExternalCopyArrayBufferView::ViewType;
ViewType type;
if (view->IsUint8Array()) {
Expand All @@ -65,14 +66,25 @@ unique_ptr<ExternalCopy> ExternalCopy::Copy(const Local<Value>& value, bool tran
} else {
assert(false);
}
if (transfer_out) {
Local<ArrayBuffer> array_buffer = view->Buffer();
// `Buffer()` returns a Local<ArrayBuffer> but it may be a Local<SharedArrayBuffer>
Local<Object> tmp = view->Buffer();
if (tmp->IsArrayBuffer()) {
Local<ArrayBuffer> array_buffer = tmp.As<ArrayBuffer>();
if (transfer_out) {
if (view->ByteOffset() != 0 || view->ByteLength() != array_buffer->ByteLength()) {
throw js_generic_error("Cannot transfer sliced TypedArray (this.byteOffset != 0 || this.byteLength != this.buffer.byteLength)");
}
return make_unique<ExternalCopyArrayBufferView>(ExternalCopyArrayBuffer::Transfer(array_buffer), type);
} else {
return make_unique<ExternalCopyArrayBufferView>(make_unique<ExternalCopyArrayBuffer>(array_buffer), type);
}
} else {
assert(tmp->IsSharedArrayBuffer());
Local<SharedArrayBuffer> array_buffer = tmp.As<SharedArrayBuffer>();
if (view->ByteOffset() != 0 || view->ByteLength() != array_buffer->ByteLength()) {
throw js_generic_error("Cannot transfer sliced TypedArray (this.byteOffset != 0 || this.byteLength != this.buffer.byteLength)");
}
return make_unique<ExternalCopyArrayBufferView>(ExternalCopyArrayBuffer::Transfer(array_buffer), type);
} else {
return make_unique<ExternalCopyArrayBufferView>(view, type);
return make_unique<ExternalCopyArrayBufferView>(make_unique<ExternalCopySharedArrayBuffer>(array_buffer), type);
}
} else if (value->IsObject()) {
Isolate* isolate = Isolate::GetCurrent();
Expand Down Expand Up @@ -442,14 +454,12 @@ ExternalCopyArrayBuffer::ExternalCopyArrayBuffer(shared_ptr<void> ptr, size_t le
value(std::move(ptr)),
length(length) {}

ExternalCopyArrayBuffer::ExternalCopyArrayBuffer(const Local<ArrayBufferView>& handle) :
ExternalCopyArrayBuffer::ExternalCopyArrayBuffer(const Local<ArrayBuffer>& handle) :
ExternalCopyBytes(handle->ByteLength() + sizeof(ExternalCopyArrayBuffer)),
value(malloc(handle->ByteLength()), std::free),
length(handle->ByteLength())
{
if (handle->CopyContents(value.get(), length) != length) {
throw js_generic_error("Failed to copy array contents");
}
std::memcpy(value.get(), handle->GetContents().Data(), length);
}

unique_ptr<ExternalCopyArrayBuffer> ExternalCopyArrayBuffer::Transfer(const Local<ArrayBuffer>& handle) {
Expand Down Expand Up @@ -523,47 +533,103 @@ size_t ExternalCopyArrayBuffer::Length() const {
}

/**
* ExternalCopyArrayBufferView implementation
* ExternalCopySharedArrayBuffer implementation
*/
ExternalCopyArrayBufferView::ExternalCopyArrayBufferView(const Local<ArrayBufferView>& handle, ViewType type) :
ExternalCopy(sizeof(ExternalCopyArrayBufferView)),
buffer(std::make_unique<ExternalCopyArrayBuffer>(handle)),
type(type) {}
ExternalCopySharedArrayBuffer::ExternalCopySharedArrayBuffer(const v8::Local<v8::SharedArrayBuffer>& handle) :
ExternalCopyBytes(handle->ByteLength() + sizeof(ExternalCopySharedArrayBuffer)),
length(handle->ByteLength())
{
// Similar to ArrayBuffer::Transfer but different enough to make it not worth abstracting out..
size_t length = handle->ByteLength();
if (length == 0) {
throw js_generic_error("Array buffer is invalid");
}
if (handle->IsExternal()) {
// Buffer lifespan is not handled by v8.. attempt to recover from isolated-vm
Holder* ptr = reinterpret_cast<Holder*>(handle->GetAlignedPointerFromInternalField(0));
if (ptr == nullptr || ptr->magic != Holder::kMagic) { // dangerous pointer dereference
throw js_generic_error("Array buffer cannot be externalized");
}
// No race conditions here because only one thread can access `Holder`
value = ptr->cc_ptr;
return;
}
// In this case the buffer is internal and should be externalized
SharedArrayBuffer::Contents contents = handle->Externalize();
value = shared_ptr<void>(contents.Data(), std::free);
new Holder(handle, value, length);
// Adjust allocator memory down, and `Holder` will adjust memory back up
auto allocator = dynamic_cast<LimitedAllocator*>(IsolateEnvironment::GetCurrent()->GetAllocator());
if (allocator != nullptr) {
allocator->AdjustAllocatedSize(-static_cast<ptrdiff_t>(length));
}
}

Local<Value> ExternalCopySharedArrayBuffer::CopyInto(bool /*transfer_in*/) {
auto ptr = GetSharedPointer();
Local<SharedArrayBuffer> array_buffer = SharedArrayBuffer::New(Isolate::GetCurrent(), ptr.get(), length);
new Holder(array_buffer, std::move(ptr), length);
return array_buffer;
}

uint32_t ExternalCopySharedArrayBuffer::WorstCaseHeapSize() const {
return length;
}

shared_ptr<void> ExternalCopySharedArrayBuffer::GetSharedPointer() const {
auto ptr = std::atomic_load(&value);
if (!ptr) {
throw js_generic_error("Array buffer is invalid");
}
return ptr;
}

ExternalCopyArrayBufferView::ExternalCopyArrayBufferView(std::unique_ptr<ExternalCopyArrayBuffer> buffer, ViewType type) :
/**
* ExternalCopyArrayBufferView implementation
*/
ExternalCopyArrayBufferView::ExternalCopyArrayBufferView(std::unique_ptr<ExternalCopyBytes> buffer, ViewType type) :
ExternalCopy(sizeof(ExternalCopyArrayBufferView)),
buffer(std::move(buffer)),
type(type) {}

Local<Value> ExternalCopyArrayBufferView::CopyInto(bool transfer_in) {
Local<ArrayBuffer> buffer = Local<ArrayBuffer>::Cast(this->buffer->CopyInto(transfer_in));
template <typename T>
Local<Value> NewTypedArrayView(Local<T> buffer, ExternalCopyArrayBufferView::ViewType type) {
size_t length = buffer->ByteLength();
switch (type) {
case ViewType::Uint8:
case ExternalCopyArrayBufferView::ViewType::Uint8:
return Uint8Array::New(buffer, 0, length);
case ViewType::Uint8Clamped:
case ExternalCopyArrayBufferView::ViewType::Uint8Clamped:
return Uint8ClampedArray::New(buffer, 0, length);
case ViewType::Int8:
case ExternalCopyArrayBufferView::ViewType::Int8:
return Int8Array::New(buffer, 0, length);
case ViewType::Uint16:
case ExternalCopyArrayBufferView::ViewType::Uint16:
return Uint16Array::New(buffer, 0, length);
case ViewType::Int16:
case ExternalCopyArrayBufferView::ViewType::Int16:
return Int16Array::New(buffer, 0, length);
case ViewType::Uint32:
case ExternalCopyArrayBufferView::ViewType::Uint32:
return Uint32Array::New(buffer, 0, length);
case ViewType::Int32:
case ExternalCopyArrayBufferView::ViewType::Int32:
return Int32Array::New(buffer, 0, length);
case ViewType::Float32:
case ExternalCopyArrayBufferView::ViewType::Float32:
return Float32Array::New(buffer, 0, length);
case ViewType::Float64:
case ExternalCopyArrayBufferView::ViewType::Float64:
return Float64Array::New(buffer, 0, length);
case ViewType::DataView:
case ExternalCopyArrayBufferView::ViewType::DataView:
return DataView::New(buffer, 0, length);
default:
throw std::exception();
}
}

Local<Value> ExternalCopyArrayBufferView::CopyInto(bool transfer_in) {
Local<Value> buffer = this->buffer->CopyInto(transfer_in);
if (buffer->IsArrayBuffer()) {
return NewTypedArrayView(buffer.As<ArrayBuffer>(), type);
} else {
return NewTypedArrayView(buffer.As<SharedArrayBuffer>(), type);
}
}

uint32_t ExternalCopyArrayBufferView::WorstCaseHeapSize() const {
// This includes the overhead of the ArrayBuffer
return 208 + buffer->WorstCaseHeapSize();
Expand Down
23 changes: 19 additions & 4 deletions src/external_copy.h
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ class ExternalCopyArrayBuffer : public ExternalCopyBytes {
public:
ExternalCopyArrayBuffer(const void* data, size_t length);
ExternalCopyArrayBuffer(std::shared_ptr<void> ptr, size_t length);
explicit ExternalCopyArrayBuffer(const v8::Local<v8::ArrayBufferView>& handle);
explicit ExternalCopyArrayBuffer(const v8::Local<v8::ArrayBuffer>& handle);

static std::unique_ptr<ExternalCopyArrayBuffer> Transfer(const v8::Local<v8::ArrayBuffer>& handle);
v8::Local<v8::Value> CopyInto(bool transfer_in = false) final;
Expand All @@ -227,6 +227,22 @@ class ExternalCopyArrayBuffer : public ExternalCopyBytes {
size_t Length() const;
};

/**
* SharedArrayBuffer instances
*/
class ExternalCopySharedArrayBuffer : public ExternalCopyBytes {
private:
std::shared_ptr<void> value;
const size_t length;

public:
explicit ExternalCopySharedArrayBuffer(const v8::Local<v8::SharedArrayBuffer>& handle);

v8::Local<v8::Value> CopyInto(bool transfer_in = false) final;
uint32_t WorstCaseHeapSize() const final;
std::shared_ptr<void> GetSharedPointer() const;
};

/**
* All types of TypedArray views w/ underlying buffer handle
*/
Expand All @@ -235,12 +251,11 @@ class ExternalCopyArrayBufferView : public ExternalCopy {
enum class ViewType { Uint8, Uint8Clamped, Int8, Uint16, Int16, Uint32, Int32, Float32, Float64, DataView };

private:
std::unique_ptr<ExternalCopyArrayBuffer> buffer;
std::unique_ptr<ExternalCopyBytes> buffer;
ViewType type;

public:
ExternalCopyArrayBufferView(const v8::Local<v8::ArrayBufferView>& handle, ViewType type);
ExternalCopyArrayBufferView(std::unique_ptr<ExternalCopyArrayBuffer> buffer, ViewType type);
ExternalCopyArrayBufferView(std::unique_ptr<ExternalCopyBytes> buffer, ViewType type);
v8::Local<v8::Value> CopyInto(bool transfer_in = false) final;
uint32_t WorstCaseHeapSize() const final;
};
Expand Down
3 changes: 2 additions & 1 deletion src/isolate/environment.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ class Runnable;
* Wrapper around Isolate with helpers to make working with multiple isolates easier.
*/
class IsolateEnvironment {
// These are here so they can adjust `extra_allocated_memory`
// These are here so they can adjust `extra_allocated_memory`. TODO: Make this a method
friend class ExternalCopyBytes;
friend class ExternalCopyArrayBuffer;
friend class ExternalCopySharedArrayBuffer;
friend class ExternalCopyString;

friend class InspectorAgent;
Expand Down
22 changes: 16 additions & 6 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@ let path = require('path');

let ret = 0;
function runTest(test, cb) {
// Copy env variables
let env = {};
for (let ii in process.env) {
env[ii] = process.env[ii];
}
env.NODE_PATH = __dirname;
let proc = spawn(
process.execPath,
[ path.join('tests', test) ],
{ env: env }
);

// Get extra args
let args = [];
let testPath = path.join('tests', test);
let content = fs.readFileSync(testPath, 'utf8');
let match = /node-args: *(.+)/.exec(content);
if (match) {
args = match[1].split(/ /g);
}
args.push(testPath);

// Launch process
let proc = spawn(process.execPath, args, { env });
proc.stdout.setEncoding('utf8');
proc.stderr.setEncoding('utf8');

Expand All @@ -27,6 +36,7 @@ function runTest(test, cb) {
});
proc.stdin.end();

// Wait for completion
process.stderr.write(`${test}: `);
proc.on('exit', function(code) {
if (stdout !== 'pass\n' || stderr !== '') {
Expand All @@ -50,7 +60,7 @@ function runTest(test, cb) {
let cb = function() {
process.exit(ret);
};
fs.readdirSync('./tests').reverse().forEach(function(file) {
fs.readdirSync('./tests').sort().reverse().forEach(function(file) {
cb = new function(cb) {
return function(err) {
if (err) return cb(err);
Expand Down
67 changes: 67 additions & 0 deletions tests/shared-array-buffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use strict';
// node-args: --harmony_sharedarraybuffer
let ivm = require('isolated-vm');

let buffer = new SharedArrayBuffer(1024 * 1024);
let array = new Uint8Array(buffer);

function runSync(env, fn) {
env.isolate.compileScriptSync('new '+ fn).runSync(env.context);
}

function makeIsolate() {
let isolate = new ivm.Isolate;
let context = isolate.createContextSync();
let global = context.globalReference();
return { isolate, context, global };
}
let env = makeIsolate();

// Check initial transfer
env.global.setSync('buffer', new ivm.ExternalCopy(buffer).copyInto({ release: true }));
env.global.setSync('ivm', ivm);

if (env.isolate.getHeapStatisticsSync().externally_allocated_size !== 1024 * 1024) {
console.log('size wrong');
}

// Check that buffer transfer + externalize size adjustments work
let env2 = makeIsolate();
env.global.setSync('global2', env2.global);
env2.global.setSync('ivm', ivm);
runSync(env, function() {
global2.setSync('buffer', new ivm.ExternalCopy(buffer).copyInto({ release: true }));
});

if (
env.isolate.getHeapStatisticsSync().externally_allocated_size !== 1024 * 1024 ||
env2.isolate.getHeapStatisticsSync().externally_allocated_size !== 1024 * 1024
) {
console.log('size wrong2');
}

// Make sure we're using the same array
env.global.setSync('array', new ivm.ExternalCopy(array).copyInto({ release: true }));
runSync(env, function() {
array[0] = 123;
});
runSync(env2, function() {
let foo = new Uint8Array(buffer);
foo[1] = 234;
buf2 = new SharedArrayBuffer(1024 * 1024);
new ivm.ExternalCopy(buf2); // externalize
});

if (array[0] !== 123 || array[1] !== 234) {
console.log('not shared');
}

if (env.isolate.getHeapStatisticsSync().externally_allocated_size !== 1024 * 1024 * 2) {
// This isn't really a good thing but I can't really think of an efficient way to fix it
console.log('size wrong3');
}
if (env2.isolate.getHeapStatisticsSync().externally_allocated_size !== 1024 * 1024 * 2) {
console.log('size wrong4');
}

console.log('pass');

0 comments on commit a026cc3

Please sign in to comment.