Commit 3c98e339 authored by gdeepti's avatar gdeepti Committed by Commit bot

[wasm] WebAssembly.Memory object can be referenced by multiple Instance objects.

Add support for WebAssembly.Memory objects to be simultaneously referenced by multiple Instance objects. GrowingMemory should maintain a consistent view of memory across instances.
 - Store a link to instances that share WebAssembly.Memory in the WasmMemoryObject, updated on instantiate.
 - Implement WasmInstanceWrapper as a wrapper around the instance object to keep track of previous/next instances, instance object is stored as a WeakCell that can be garbage collected.
 - MemoryInstanceFinalizer maintains a valid list of instances when an instance is garbage collected.
 - Refactor GrowInstanceMemory to GrowMemoryBuffer that allocates a new buffer, and UncheckedUpdateInstanceMemory that updates memory references for an instance.

 R=titzer@chromium.org, mtrofin@chromium.org, bradnelson@chromium.org

Committed: https://crrev.com/30ef8e33f3a199a27ca8512bcee314c9522d03f6
Review-Url: https://codereview.chromium.org/2471883003
Cr-Original-Commit-Position: refs/heads/master@{#41121}
Cr-Commit-Position: refs/heads/master@{#41198}
parent eb3551d0
......@@ -58,7 +58,7 @@ RUNTIME_FUNCTION(Runtime_WasmGrowMemory) {
instance = handle(owning_instance, isolate);
}
return *isolate->factory()->NewNumberFromInt(
wasm::GrowInstanceMemory(isolate, instance, delta_pages));
wasm::GrowMemory(isolate, instance, delta_pages));
}
RUNTIME_FUNCTION(Runtime_WasmThrowTypeError) {
......
......@@ -28,12 +28,6 @@ using v8::internal::wasm::ErrorThrower;
namespace v8 {
enum WasmMemoryObjectData {
kWasmMemoryBuffer,
kWasmMemoryMaximum,
kWasmMemoryInstanceObject
};
namespace {
i::Handle<i::String> v8_str(i::Isolate* isolate, const char* str) {
return isolate->factory()->NewStringFromAsciiChecked(str);
......@@ -230,6 +224,7 @@ void WebAssemblyInstance(const v8::FunctionCallbackInfo<v8::Value>& args) {
i_isolate);
} else {
thrower.TypeError("Argument 2 must be a WebAssembly.Memory");
return;
}
}
i::MaybeHandle<i::JSObject> instance =
......@@ -533,31 +528,15 @@ void WebAssemblyMemoryGrow(const v8::FunctionCallbackInfo<v8::Value>& args) {
uint32_t delta = args[0]->Uint32Value(context).FromJust();
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
i::Handle<i::JSObject> receiver =
i::Handle<i::JSObject>::cast(Utils::OpenHandle(*args.This()));
i::Handle<i::Object> instance_object(
receiver->GetInternalField(kWasmMemoryInstanceObject), i_isolate);
i::Handle<i::WasmInstanceObject> instance(
i::Handle<i::WasmInstanceObject>::cast(instance_object));
// TODO(gdeepti) Implement growing memory when shared by different
// instances.
int32_t ret = internal::wasm::GrowInstanceMemory(i_isolate, instance, delta);
i::Handle<i::Object> receiver =
i::Handle<i::Object>::cast(Utils::OpenHandle(*args.This()));
int32_t ret = i::wasm::GrowWebAssemblyMemory(i_isolate, receiver, delta);
if (ret == -1) {
v8::Local<v8::Value> e = v8::Exception::Error(
v8_str(isolate, "Unable to grow instance memory."));
isolate->ThrowException(e);
return;
}
i::MaybeHandle<i::JSArrayBuffer> buffer =
internal::wasm::GetInstanceMemory(i_isolate, instance);
if (buffer.is_null()) {
v8::Local<v8::Value> e = v8::Exception::Error(
v8_str(isolate, "WebAssembly.Memory buffer object not set."));
isolate->ThrowException(e);
return;
}
receiver->SetInternalField(kWasmMemoryBuffer, *buffer.ToHandleChecked());
v8::ReturnValue<v8::Value> return_value = args.GetReturnValue();
return_value.Set(ret);
}
......@@ -573,10 +552,9 @@ void WebAssemblyMemoryGetBuffer(
return;
}
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
i::Handle<i::JSObject> receiver =
i::Handle<i::JSObject>::cast(Utils::OpenHandle(*args.This()));
i::Handle<i::Object> buffer(receiver->GetInternalField(kWasmMemoryBuffer),
i_isolate);
i::Handle<i::WasmMemoryObject> receiver =
i::Handle<i::WasmMemoryObject>::cast(Utils::OpenHandle(*args.This()));
i::Handle<i::Object> buffer(receiver->get_buffer(), i_isolate);
DCHECK(buffer->IsJSArrayBuffer());
v8::ReturnValue<v8::Value> return_value = args.GetReturnValue();
return_value.Set(Utils::ToLocal(buffer));
......
This diff is collapsed.
......@@ -430,6 +430,12 @@ int32_t GrowInstanceMemory(Isolate* isolate,
Handle<JSArrayBuffer> NewArrayBuffer(Isolate* isolate, size_t size,
bool enable_guard_regions);
int32_t GrowWebAssemblyMemory(Isolate* isolate, Handle<Object> receiver,
uint32_t pages);
int32_t GrowMemory(Isolate* isolate, Handle<WasmInstanceObject> instance,
uint32_t pages);
void UpdateDispatchTables(Isolate* isolate, Handle<FixedArray> dispatch_tables,
int index, Handle<JSFunction> js_function);
......
......@@ -174,7 +174,8 @@ Handle<WasmMemoryObject> WasmMemoryObject::New(Isolate* isolate,
int maximum) {
Handle<JSFunction> memory_ctor(
isolate->native_context()->wasm_memory_constructor());
Handle<JSObject> memory_obj = isolate->factory()->NewJSObject(memory_ctor);
Handle<JSObject> memory_obj =
isolate->factory()->NewJSObject(memory_ctor, TENURED);
memory_obj->SetInternalField(kArrayBuffer, *buffer);
memory_obj->SetInternalField(kMaximum,
static_cast<Object*>(Smi::FromInt(maximum)));
......@@ -184,6 +185,8 @@ Handle<WasmMemoryObject> WasmMemoryObject::New(Isolate* isolate,
}
DEFINE_ACCESSORS(WasmMemoryObject, buffer, kArrayBuffer, JSArrayBuffer)
DEFINE_OPTIONAL_ACCESSORS(WasmMemoryObject, instances_link, kInstancesLink,
WasmInstanceWrapper)
uint32_t WasmMemoryObject::current_pages() {
return SafeUint32(get_buffer()->byte_length()) / wasm::WasmModule::kPageSize;
......@@ -199,10 +202,26 @@ WasmMemoryObject* WasmMemoryObject::cast(Object* object) {
return reinterpret_cast<WasmMemoryObject*>(object);
}
void WasmMemoryObject::AddInstance(WasmInstanceObject* instance) {
// TODO(gdeepti): This should be a weak list of instance objects
// for instances that share memory.
SetInternalField(kInstance, instance);
void WasmMemoryObject::AddInstance(Isolate* isolate,
Handle<WasmInstanceObject> instance) {
Handle<WasmInstanceWrapper> instance_wrapper;
if (has_instances_link()) {
Handle<WasmInstanceWrapper> current_wrapper(get_instances_link());
DCHECK(WasmInstanceWrapper::IsWasmInstanceWrapper(*current_wrapper));
DCHECK(!current_wrapper->has_previous());
instance_wrapper = WasmInstanceWrapper::New(isolate, instance);
instance_wrapper->set_next_wrapper(*current_wrapper);
current_wrapper->set_previous_wrapper(*instance_wrapper);
} else {
instance_wrapper = WasmInstanceWrapper::New(isolate, instance);
}
set_instances_link(*instance_wrapper);
instance->set_instance_wrapper(*instance_wrapper);
}
void WasmMemoryObject::ResetInstancesLink(Isolate* isolate) {
Handle<Object> undefined = isolate->factory()->undefined_value();
SetInternalField(kInstancesLink, *undefined);
}
DEFINE_ACCESSORS(WasmInstanceObject, compiled_module, kCompiledModule,
......@@ -215,6 +234,8 @@ DEFINE_OPTIONAL_ACCESSORS(WasmInstanceObject, memory_object, kMemoryObject,
WasmMemoryObject)
DEFINE_OPTIONAL_ACCESSORS(WasmInstanceObject, debug_info, kDebugInfo,
WasmDebugInfo)
DEFINE_OPTIONAL_ACCESSORS(WasmInstanceObject, instance_wrapper,
kWasmMemInstanceWrapper, WasmInstanceWrapper)
WasmModuleObject* WasmInstanceObject::module_object() {
return WasmModuleObject::cast(*get_compiled_module()->wasm_module());
......@@ -424,3 +445,34 @@ bool WasmCompiledModule::GetPositionInfo(uint32_t position,
info->line_end = function.code_end_offset;
return true;
}
Handle<WasmInstanceWrapper> WasmInstanceWrapper::New(
Isolate* isolate, Handle<WasmInstanceObject> instance) {
Handle<FixedArray> array =
isolate->factory()->NewFixedArray(kWrapperPropertyCount, TENURED);
Handle<WasmInstanceWrapper> instance_wrapper(
reinterpret_cast<WasmInstanceWrapper*>(*array), isolate);
instance_wrapper->set_instance_object(instance, isolate);
return instance_wrapper;
}
bool WasmInstanceWrapper::IsWasmInstanceWrapper(Object* obj) {
if (!obj->IsFixedArray()) return false;
FixedArray* array = FixedArray::cast(obj);
if (array->length() != kWrapperPropertyCount) return false;
if (!array->get(kWrapperInstanceObject)->IsWeakCell()) return false;
Isolate* isolate = array->GetIsolate();
if (!array->get(kNextInstanceWrapper)->IsUndefined(isolate) &&
!array->get(kNextInstanceWrapper)->IsFixedArray())
return false;
if (!array->get(kPreviousInstanceWrapper)->IsUndefined(isolate) &&
!array->get(kPreviousInstanceWrapper)->IsFixedArray())
return false;
return true;
}
void WasmInstanceWrapper::set_instance_object(Handle<JSObject> instance,
Isolate* isolate) {
Handle<WeakCell> cell = isolate->factory()->NewWeakCell(instance);
set(kWrapperInstanceObject, *cell);
}
......@@ -17,6 +17,7 @@ struct WasmModule;
class WasmCompiledModule;
class WasmDebugInfo;
class WasmInstanceObject;
class WasmInstanceWrapper;
#define DECLARE_CASTS(name) \
static bool Is##name(Object* object); \
......@@ -79,12 +80,14 @@ class WasmTableObject : public JSObject {
class WasmMemoryObject : public JSObject {
public:
// TODO(titzer): add the brand as an internal field instead of a property.
enum Fields : uint8_t { kArrayBuffer, kMaximum, kInstance, kFieldCount };
enum Fields : uint8_t { kArrayBuffer, kMaximum, kInstancesLink, kFieldCount };
DECLARE_CASTS(WasmMemoryObject);
DECLARE_ACCESSORS(buffer, JSArrayBuffer);
DECLARE_OPTIONAL_ACCESSORS(instances_link, WasmInstanceWrapper);
void AddInstance(WasmInstanceObject* object);
void AddInstance(Isolate* isolate, Handle<WasmInstanceObject> object);
void ResetInstancesLink(Isolate* isolate);
uint32_t current_pages();
int32_t maximum_pages(); // returns < 0 if there is no maximum
......@@ -105,6 +108,7 @@ class WasmInstanceObject : public JSObject {
kMemoryArrayBuffer,
kGlobalsArrayBuffer,
kDebugInfo,
kWasmMemInstanceWrapper,
kFieldCount
};
......@@ -115,6 +119,7 @@ class WasmInstanceObject : public JSObject {
DECLARE_OPTIONAL_ACCESSORS(memory_buffer, JSArrayBuffer);
DECLARE_OPTIONAL_ACCESSORS(memory_object, WasmMemoryObject);
DECLARE_OPTIONAL_ACCESSORS(debug_info, WasmDebugInfo);
DECLARE_OPTIONAL_ACCESSORS(instance_wrapper, WasmInstanceWrapper);
WasmModuleObject* module_object();
wasm::WasmModule* module();
......@@ -326,6 +331,61 @@ class WasmDebugInfo : public FixedArray {
int func_index, int byte_offset);
};
class WasmInstanceWrapper : public FixedArray {
public:
static Handle<WasmInstanceWrapper> New(Isolate* isolate,
Handle<WasmInstanceObject> instance);
static WasmInstanceWrapper* cast(Object* fixed_array) {
SLOW_DCHECK(IsWasmInstanceWrapper(fixed_array));
return reinterpret_cast<WasmInstanceWrapper*>(fixed_array);
}
static bool IsWasmInstanceWrapper(Object* obj);
bool has_instance() { return get(kWrapperInstanceObject)->IsWeakCell(); }
Handle<WasmInstanceObject> instance_object() {
Object* obj = get(kWrapperInstanceObject);
DCHECK(obj->IsWeakCell());
WeakCell* cell = WeakCell::cast(obj);
DCHECK(cell->value()->IsJSObject());
return handle(WasmInstanceObject::cast(cell->value()));
}
bool has_next() { return IsWasmInstanceWrapper(get(kNextInstanceWrapper)); }
bool has_previous() {
return IsWasmInstanceWrapper(get(kPreviousInstanceWrapper));
}
void set_instance_object(Handle<JSObject> instance, Isolate* isolate);
void set_next_wrapper(Object* obj) {
DCHECK(IsWasmInstanceWrapper(obj));
set(kNextInstanceWrapper, obj);
}
void set_previous_wrapper(Object* obj) {
DCHECK(IsWasmInstanceWrapper(obj));
set(kPreviousInstanceWrapper, obj);
}
Handle<WasmInstanceWrapper> next_wrapper() {
Object* obj = get(kNextInstanceWrapper);
DCHECK(IsWasmInstanceWrapper(obj));
return handle(WasmInstanceWrapper::cast(obj));
}
Handle<WasmInstanceWrapper> previous_wrapper() {
Object* obj = get(kPreviousInstanceWrapper);
DCHECK(IsWasmInstanceWrapper(obj));
return handle(WasmInstanceWrapper::cast(obj));
}
void reset_next_wrapper() { set_undefined(kNextInstanceWrapper); }
void reset_previous_wrapper() { set_undefined(kPreviousInstanceWrapper); }
void reset() {
for (int kID = 0; kID < kWrapperPropertyCount; kID++) set_undefined(kID);
}
private:
enum {
kWrapperInstanceObject,
kNextInstanceWrapper,
kPreviousInstanceWrapper,
kWrapperPropertyCount
};
};
#undef DECLARE_ACCESSORS
#undef DECLARE_OPTIONAL_ACCESSORS
......
......@@ -40,7 +40,6 @@ load("test/mjsunit/wasm/wasm-module-builder.js");
assertSame(memory, instance.exports.daggle);
})();
(function TestImportExport() {
print("TestImportExport");
var i1;
......@@ -229,3 +228,141 @@ load("test/mjsunit/wasm/wasm-module-builder.js");
}
assertEquals(-1, instance.exports.grow(1));
})();
(function TestMemoryGrowWebAssemblyInstances() {
print("TestMemoryGrowWebAssemblyInstances");
let memory = new WebAssembly.Memory({initial: 1, maximum: 15});
var builder = new WasmModuleBuilder();
builder.addImportedMemory("imported_mem");
builder.addFunction("mem_size", kSig_i_v)
.addBody([kExprMemorySize, kMemoryZero])
.exportAs("mem_size");
builder.addFunction("grow", kSig_i_i)
.addBody([kExprGetLocal, 0, kExprGrowMemory, kMemoryZero])
.exportFunc();
var module = new WebAssembly.Module(builder.toBuffer());
var instances = [];
for (var i = 0; i < 6; i++) {
instances.push(new WebAssembly.Instance(module, {imported_mem: memory}));
}
function verify_mem_size(expected_pages) {
assertEquals(expected_pages*kPageSize,
memory.buffer.byteLength);
for (var i = 0; i < 6; i++) {
assertEquals(expected_pages, instances[i].exports.mem_size());
}
}
// Verify initial memory size
verify_mem_size(1);
// Verify memory size with interleaving calls to Memory.grow,
// GrowMemory opcode.
var current_mem_size = 1;
for (var i = 0; i < 5; i++) {
function grow(pages) { return instances[i].exports.grow(pages); }
assertEquals(current_mem_size, memory.grow(1));
verify_mem_size(++current_mem_size);
assertEquals(current_mem_size, instances[i].exports.grow(1));
verify_mem_size(++current_mem_size);
}
assertThrows(() => memory.grow(5));
})();
(function TestImportedMemoryGrowMultipleInstances() {
print("TestImportMemoryMultipleInstances");
let memory = new WebAssembly.Memory({initial: 5, maximum: 100});
var builder = new WasmModuleBuilder();
builder.addImportedMemory("imported_mem");
builder.addFunction("mem_size", kSig_i_v)
.addBody([kExprMemorySize, kMemoryZero])
.exportFunc();
builder.addFunction("grow", kSig_i_i)
.addBody([kExprGetLocal, 0, kExprGrowMemory, kMemoryZero])
.exportFunc();
var instances = [];
for (var i = 0; i < 5; i++) {
instances.push(builder.instantiate({imported_mem: memory}));
}
function grow_instance_0(pages) { return instances[0].exports.grow(pages); }
function grow_instance_1(pages) { return instances[1].exports.grow(pages); }
function grow_instance_2(pages) { return instances[2].exports.grow(pages); }
function grow_instance_3(pages) { return instances[3].exports.grow(pages); }
function grow_instance_4(pages) { return instances[4].exports.grow(pages); }
function verify_mem_size(expected_pages) {
assertEquals(expected_pages*kPageSize, memory.buffer.byteLength);
for (var i = 0; i < 5; i++) {
assertEquals(expected_pages, instances[i].exports.mem_size());
}
}
// Verify initial memory size
verify_mem_size(5);
// Grow instance memory and buffer memory out of order and verify memory is
// updated correctly.
assertEquals(5, grow_instance_0(7));
verify_mem_size(12);
assertEquals(12, memory.grow(4));
verify_mem_size(16);
assertEquals(16, grow_instance_4(1));
verify_mem_size(17);
assertEquals(17, grow_instance_1(6));
verify_mem_size(23);
assertEquals(23, grow_instance_3(2));
verify_mem_size(25);
assertEquals(25, memory.grow(10));
verify_mem_size(35);
assertEquals(35, grow_instance_2(15));
verify_mem_size(50);
assertThrows(() => memory.grow(51));
})();
(function TestExportImportedMemoryGrowMultipleInstances() {
// TODO(gdeepti):Exported memory objects currently do not take max_size
// into account so this can grow past the maximum specified in the exported
// memory object. Assert that growing past maximum for exported objects fails.
print("TestExportImportedMemoryGrowMultipleInstances");
var instance;
{
let builder = new WasmModuleBuilder();
builder.addMemory(1, 11, true);
builder.exportMemoryAs("exported_mem");
builder.addFunction("mem_size", kSig_i_v)
.addBody([kExprMemorySize, kMemoryZero])
.exportFunc();
instance = builder.instantiate();
}
var builder = new WasmModuleBuilder();
builder.addImportedMemory("imported_mem");
builder.addFunction("mem_size", kSig_i_v)
.addBody([kExprMemorySize, kMemoryZero])
.exportFunc();
builder.addFunction("grow", kSig_i_i)
.addBody([kExprGetLocal, 0, kExprGrowMemory, kMemoryZero])
.exportFunc();
var instances = [];
for (var i = 0; i < 10; i++) {
instances.push(builder.instantiate({
imported_mem: instance.exports.exported_mem}));
}
function verify_mem_size(expected_pages) {
for (var i = 0; i < 10; i++) {
assertEquals(expected_pages, instances[i].exports.mem_size());
}
}
var current_mem_size = 1;
for (var i = 0; i < 10; i++) {
function grow(pages) { return instances[i].exports.grow(pages); }
assertEquals(current_mem_size, instances[i].exports.grow(1));
verify_mem_size(++current_mem_size);
}
})();
......@@ -215,6 +215,7 @@ assertFalse(WebAssembly.validate(bytes(88, 88, 88, 88, 88, 88, 88, 88)));
})();
(function MustBeMemory() {
print("MustBeMemory...");
var memory = new ArrayBuffer(65536);
var module = new WebAssembly.Module(buffer);
assertThrows(() => new WebAssembly.Instance(module, null, memory), TypeError);
......
// Copyright 2016 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.
// Flags: --expose-wasm --expose-gc
load("test/mjsunit/wasm/wasm-constants.js");
load("test/mjsunit/wasm/wasm-module-builder.js");
// This test verifies that when instances are exported, Gc'ed, the other
// instances in the chain still maintain a consistent view of the memory.
(function ValidateSharedInstanceMemory() {
print("ValidateSharedInstanceMemory");
let memory = new WebAssembly.Memory({initial: 5, maximum: 100});
var builder = new WasmModuleBuilder();
builder.addImportedMemory("imported_mem");
builder.addFunction("mem_size", kSig_i_v)
.addBody([kExprMemorySize, kMemoryZero])
.exportFunc();
builder.addFunction("grow", kSig_i_i)
.addBody([kExprGetLocal, 0, kExprGrowMemory, kMemoryZero])
.exportFunc();
var instances = [];
for (var i = 0; i < 5; i++) {
instances.push(builder.instantiate({imported_mem: memory}));
}
function grow_instance_0(pages) { return instances[0].exports.grow(pages); }
function grow_instance_1(pages) { return instances[1].exports.grow(pages); }
function grow_instance_2(pages) { return instances[2].exports.grow(pages); }
function grow_instance_3(pages) { return instances[3].exports.grow(pages); }
function grow_instance_4(pages) { return instances[4].exports.grow(pages); }
var start_index = 0;
var end_index = 5;
function verify_mem_size(expected_pages) {
assertEquals(expected_pages*kPageSize, memory.buffer.byteLength);
for (var i = start_index; i < end_index; i++) {
assertEquals(expected_pages, instances[i].exports.mem_size());
}
}
// Verify initial memory size of all instances, grow and verify that all
// instances are updated correctly.
verify_mem_size(5);
assertEquals(5, memory.grow(6));
verify_mem_size(11);
instances[1] = null;
gc();
// i[0] - i[2] - i[3] - i[4]
start_index = 2;
verify_mem_size(11);
assertEquals(11, instances[0].exports.mem_size());
assertEquals(11, grow_instance_2(10));
assertEquals(21*kPageSize, memory.buffer.byteLength);
verify_mem_size(21);
assertEquals(21, instances[0].exports.mem_size());
instances[4] = null;
gc();
// i[0] - i[2] - i[3]
assertEquals(21, instances[0].exports.mem_size());
assertEquals(21, instances[2].exports.mem_size());
assertEquals(21, instances[3].exports.mem_size());
assertEquals(21, memory.grow(2));
assertEquals(23*kPageSize, memory.buffer.byteLength);
assertEquals(23, instances[0].exports.mem_size());
assertEquals(23, instances[2].exports.mem_size());
assertEquals(23, instances[3].exports.mem_size());
instances[0] = null;
gc();
// i[2] - i[3]
assertEquals(23, instances[2].exports.mem_size());
assertEquals(23, instances[3].exports.mem_size());
assertEquals(23, grow_instance_3(5));
assertEquals(28*kPageSize, memory.buffer.byteLength);
assertEquals(28, instances[2].exports.mem_size());
assertEquals(28, instances[3].exports.mem_size());
// Instantiate a new instance and verify that it can be grown correctly.
instances.push(builder.instantiate({imported_mem: memory}));
function grow_instance_5(pages) { return instances[5].exports.grow(pages); }
// i[2] - i[3] - i[5]
assertEquals(28, instances[2].exports.mem_size());
assertEquals(28, instances[3].exports.mem_size());
assertEquals(28, instances[5].exports.mem_size());
assertEquals(28, grow_instance_5(2));
assertEquals(30*kPageSize, memory.buffer.byteLength);
assertEquals(30, instances[2].exports.mem_size());
assertEquals(30, instances[3].exports.mem_size());
assertEquals(30, instances[5].exports.mem_size());
assertEquals(30, memory.grow(5));
assertEquals(35*kPageSize, memory.buffer.byteLength);
assertEquals(35, instances[2].exports.mem_size());
assertEquals(35, instances[3].exports.mem_size());
assertEquals(35, instances[5].exports.mem_size());
})();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment