Commit 22fab0ba authored by Théotime Grohens's avatar Théotime Grohens Committed by Commit Bot

[dataview] Implement Torque/CSA getters for DataView

This CL fully implements the DataView getters for the Uint8, Int8,
Uint16, Int16, Uint32 and Int32 types in Torque, and removes
the runtime implementation that is not needed anymore.

There should be a light but visible performance increase compared to
the former runtime implementation.

Change-Id: I7d85097fd5953b9629f3ac6bed93b068889712b2
Reviewed-on: https://chromium-review.googlesource.com/1078349
Commit-Queue: Théotime Grohens <theotime@google.com>
Reviewed-by: 's avatarJakob Gruber <jgruber@chromium.org>
Reviewed-by: 's avatarTobias Tebbi <tebbi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#53553}
parent 670c96d3
......@@ -1290,6 +1290,7 @@ v8_source_set("v8_initializers") {
"src/builtins/builtins-constructor-gen.h",
"src/builtins/builtins-constructor.h",
"src/builtins/builtins-conversion-gen.cc",
"src/builtins/builtins-data-view-gen.h",
"src/builtins/builtins-date-gen.cc",
"src/builtins/builtins-debug-gen.cc",
"src/builtins/builtins-function-gen.cc",
......
......@@ -11,6 +11,7 @@ type Smi extends Tagged generates 'TNode<Smi>';
type HeapObject extends Tagged generates 'TNode<HeapObject>';
type Object = Smi | HeapObject;
type int32 generates 'TNode<Int32T>' constexpr 'int32_t';
type uint32 generates 'TNode<Uint32T>' constexpr 'uint32_t';
type intptr generates 'TNode<IntPtrT>' constexpr 'intptr_t';
type float64 generates 'TNode<Float64T>' constexpr 'double';
type bool generates 'TNode<BoolT>' constexpr 'bool';
......@@ -116,6 +117,8 @@ const kBadSortComparisonFunction: constexpr MessageTemplate =
'MessageTemplate::kBadSortComparisonFunction';
const kIncompatibleMethodReceiver: constexpr MessageTemplate =
'MessageTemplate::kIncompatibleMethodReceiver';
const kInvalidDataViewAccessorOffset: constexpr MessageTemplate =
'MessageTemplate::kInvalidDataViewAccessorOffset';
const Hole: Oddball = 'TheHoleConstant()';
const Null: Oddball = 'NullConstant()';
......@@ -208,12 +211,19 @@ extern operator '!=' macro WordNotEqual(Object, Object): bool;
extern operator '+' macro SmiAdd(Smi, Smi): Smi;
extern operator '-' macro SmiSub(Smi, Smi): Smi;
extern operator '&' macro SmiAnd(Smi, Smi): Smi;
extern operator '>>>' macro SmiShr(Smi, constexpr int31): Smi;
extern operator '+' macro IntPtrAdd(intptr, intptr): intptr;
extern operator '-' macro IntPtrSub(intptr, intptr): intptr;
extern operator '>>>' macro WordShr(intptr, intptr): intptr;
extern operator '+' macro Int32Add(int32, int32): int32;
extern operator '*' macro Int32Mul(int32, int32): int32;
extern operator '%' macro Int32Mod(int32, int32): int32;
extern operator '&' macro Word32And(int32, int32): int32;
extern operator '<<' macro Word32Shl(int32, int32): int32;
extern operator '+' macro NumberAdd(Number, Number): Number;
extern operator '-' macro NumberSub(Number, Number): Number;
extern operator 'min' macro NumberMin(Number, Number): Number;
......@@ -273,6 +283,8 @@ extern operator
extern implicit operator 'convert<>' macro SmiFromInt32(ElementsKind): Smi;
extern operator 'convert<>' macro ChangeInt32ToTagged(int32): Number;
extern operator 'convert<>' macro ChangeUint32ToTagged(uint32): Number;
extern operator 'convert<>' macro UncheckedCastInt32ToUint32(int32): uint32;
extern operator 'convert<>' macro TruncateWordToWord32(intptr): int32;
extern operator 'convert<>' macro SmiTag(intptr): Smi;
extern operator 'convert<>' macro SmiFromInt32(int32): Smi;
......
......@@ -6,6 +6,7 @@
#define V8_BUILTINS_BUILTINS_DATA_VIEW_GEN_H_
#include "src/code-stub-assembler.h"
#include "src/elements-kind.h"
namespace v8 {
namespace internal {
......@@ -22,8 +23,26 @@ class DataViewBuiltinsAssembler : public CodeStubAssembler {
TNode<Smi> LoadDataViewByteLength(TNode<JSDataView> data_view) {
return LoadObjectField<Smi>(data_view, JSDataView::kByteLengthOffset);
}
TNode<Int32T> LoadUint8(TNode<RawPtrT> data_pointer, TNode<IntPtrT> offset) {
return UncheckedCast<Int32T>(
Load(MachineType::Uint8(), data_pointer, offset));
}
TNode<Int32T> LoadInt8(TNode<RawPtrT> data_pointer, TNode<IntPtrT> offset) {
return UncheckedCast<Int32T>(
Load(MachineType::Int8(), data_pointer, offset));
}
TNode<Uint32T> UncheckedCastInt32ToUint32(TNode<Int32T> value) {
return Unsigned(value);
}
};
int32_t DataViewElementSize(ElementsKind elements_kind) {
return ElementsKindToByteSize(elements_kind);
}
} // namespace internal
} // namespace v8
......
......@@ -8,6 +8,8 @@ module data_view {
JSArrayBufferView): JSArrayBuffer;
extern operator '.byte_length' macro LoadDataViewByteLength(JSDataView): Smi;
extern operator '.byte_offset' macro LoadDataViewByteOffset(JSDataView): Smi;
extern operator '.backing_store' macro LoadArrayBufferBackingStore(
JSArrayBuffer): RawPtr;
macro WasNeutered(view: JSArrayBufferView): bool {
return IsDetachedBuffer(view.buffer);
......@@ -39,8 +41,7 @@ module data_view {
if (WasNeutered(data_view)) {
// TODO(bmeurer): According to the ES6 spec, we should throw a TypeError
// here if the JSArrayBuffer of the {data_view} was neutered.
let zero: Smi = 0;
return zero;
return 0;
}
return data_view.byte_length;
}
......@@ -53,30 +54,157 @@ module data_view {
if (WasNeutered(data_view)) {
// TODO(bmeurer): According to the ES6 spec, we should throw a TypeError
// here if the JSArrayBuffer of the {data_view} was neutered.
let zero: Smi = 0;
return zero;
return 0;
}
return data_view.byte_offset;
}
extern macro BranchIfToBooleanIsTrue(Object): never labels Taken, NotTaken;
// TODO(theotime): This function should be moved to base.tq, we can't call
// functions that were defined in another Torque file for now.
macro ToBoolean(obj: Object): bool {
try {
BranchIfToBooleanIsTrue(obj) otherwise Taken, NotTaken;
} label Taken {
return true;
} label NotTaken {
return false;
}
}
extern macro LoadUint8(RawPtr, intptr): int32;
extern macro LoadInt8(RawPtr, intptr): int32;
macro LoadDataViewUint8(data_pointer: RawPtr, offset: intptr): Smi {
return convert<Smi>(LoadUint8(data_pointer, offset));
}
macro LoadDataViewInt8(data_pointer: RawPtr, offset: intptr): Smi {
return convert<Smi>(LoadInt8(data_pointer, offset));
}
macro LoadDataView16(data_pointer: RawPtr, offset: intptr,
requested_little_endian: bool,
signed: constexpr bool): Number {
let b0: int32;
let b1: int32;
let result: int32;
// Sign-extend the most significant byte by loading it as an Int8.
if (requested_little_endian) {
b0 = LoadUint8(data_pointer, offset);
b1 = LoadInt8(data_pointer, offset + 1);
result = (b1 << 8) + b0;
} else {
b0 = LoadInt8(data_pointer, offset);
b1 = LoadUint8(data_pointer, offset + 1);
result = (b0 << 8) + b1;
}
if constexpr (signed) {
return convert<Smi>(result);
} else {
// Bit-mask the higher bits to prevent sign extension if we're unsigned.
return convert<Smi>(result & 0xFFFF);
}
}
macro LoadDataView32(data_pointer: RawPtr, offset: intptr,
requested_little_endian: bool,
signed: constexpr bool): Number {
let b0: int32 = LoadUint8(data_pointer, offset);
let b1: int32 = LoadUint8(data_pointer, offset + 1);
let b2: int32 = LoadUint8(data_pointer, offset + 2);
let b3: int32 = LoadUint8(data_pointer, offset + 3);
let result: int32;
if (requested_little_endian) {
let low_part: int32 = (b1 << 8) + b0;
let high_part: int32 = (b3 << 8) + b2;
result = (high_part << 16) + low_part;
} else {
let high_part: int32 = (b0 << 8) + b1;
let low_part: int32 = (b2 << 8) + b3;
result = (high_part << 16) + low_part;
}
if constexpr (signed) {
return convert<Number>(result);
} else {
return convert<Number>(convert<uint32>(result));
}
}
// Context, receiver, byteOffset, littleEndian
extern runtime DataViewGetInt8(Context, Object, Object, Object): Object;
extern runtime DataViewGetUint8(Context, Object, Object, Object): Object;
extern runtime DataViewGetInt16(Context, Object, Object, Object): Object;
extern runtime DataViewGetUint16(Context, Object, Object, Object): Object;
extern runtime DataViewGetInt32(Context, Object, Object, Object): Object;
extern runtime DataViewGetUint32(Context, Object, Object, Object): Object;
extern runtime DataViewGetFloat32(Context, Object, Object, Object): Object;
extern runtime DataViewGetFloat64(Context, Object, Object, Object): Object;
extern runtime DataViewGetBigInt64(Context, Object, Object, Object): Object;
extern runtime DataViewGetBigUint64(Context, Object, Object, Object): Object;
extern runtime DataViewGetFloat32(Context, Object, Object, Object): Number;
extern runtime DataViewGetFloat64(Context, Object, Object, Object): Number;
extern runtime DataViewGetBigInt64(Context, Object, Object, Object): Number;
extern runtime DataViewGetBigUint64(Context, Object, Object, Object): Number;
extern macro ToSmiIndex(Object, Context): Smi labels RangeError;
extern macro DataViewElementSize(constexpr ElementsKind): constexpr int32;
macro DataViewGet(context: Context,
receiver: Object,
offset: Object,
requested_little_endian: Object,
kind: constexpr ElementsKind): Number {
// TODO(theotime): add more specific method name to match
// the former implementation.
let data_view: JSDataView = ValidateDataView(
context, receiver, 'get DataView.prototype.get');
let getIndex: Smi;
try {
getIndex = ToSmiIndex(offset, context) otherwise RangeError;
}
label RangeError {
ThrowRangeError(context, kInvalidDataViewAccessorOffset);
}
let littleEndian: bool = ToBoolean(requested_little_endian);
let buffer: JSArrayBuffer = data_view.buffer;
if (IsDetachedBuffer(buffer)) {
ThrowTypeError(context, kDetachedOperation, 'DataView.prototype.get');
}
let viewOffset: Smi = data_view.byte_offset;
let viewSize: Smi = data_view.byte_length;
let elementSize: Smi = convert<Smi>(DataViewElementSize(kind));
if (getIndex + elementSize > viewSize ||
getIndex + elementSize < getIndex) {
// TODO(theotime): do we really need to check for overflow here?
ThrowRangeError(context, kInvalidDataViewAccessorOffset);
}
let bufferIndex: intptr = convert<intptr>(getIndex + viewOffset);
let data_pointer: RawPtr = buffer.backing_store;
if constexpr (kind == UINT8_ELEMENTS) {
return LoadDataViewUint8(data_pointer, bufferIndex);
} else if constexpr (kind == INT8_ELEMENTS) {
return LoadDataViewInt8(data_pointer, bufferIndex);
} else if constexpr (kind == UINT16_ELEMENTS) {
return LoadDataView16(data_pointer, bufferIndex, littleEndian, false);
} else if constexpr (kind == INT16_ELEMENTS) {
return LoadDataView16(data_pointer, bufferIndex, littleEndian, true);
} else if constexpr (kind == UINT32_ELEMENTS) {
return LoadDataView32(data_pointer, bufferIndex, littleEndian, false);
} else if constexpr (kind == INT32_ELEMENTS) {
return LoadDataView32(data_pointer, bufferIndex, littleEndian, true);
} else {
unreachable;
}
}
javascript builtin DataViewPrototypeGetInt8(
context: Context, receiver: Object, ...arguments): Object {
let offset: Object = arguments.length > 0 ?
arguments[0] :
Undefined;
return DataViewGetInt8(context, receiver, offset, Undefined);
return DataViewGet(context, receiver, offset, Undefined, INT8_ELEMENTS);
}
javascript builtin DataViewPrototypeGetUint8(
......@@ -84,7 +212,7 @@ module data_view {
let offset: Object = arguments.length > 0 ?
arguments[0] :
Undefined;
return DataViewGetUint8(context, receiver, offset, Undefined);
return DataViewGet(context, receiver, offset, Undefined, UINT8_ELEMENTS);
}
javascript builtin DataViewPrototypeGetInt16(
......@@ -95,7 +223,8 @@ module data_view {
let is_little_endian : Object = arguments.length > 1 ?
arguments[1] :
Undefined;
return DataViewGetInt16(context, receiver, offset, is_little_endian);
return DataViewGet(context, receiver, offset, is_little_endian,
INT16_ELEMENTS);
}
javascript builtin DataViewPrototypeGetUint16(
......@@ -106,7 +235,8 @@ module data_view {
let is_little_endian : Object = arguments.length > 1 ?
arguments[1] :
Undefined;
return DataViewGetUint16(context, receiver, offset, is_little_endian);
return DataViewGet(context, receiver, offset, is_little_endian,
UINT16_ELEMENTS);
}
javascript builtin DataViewPrototypeGetInt32(
......@@ -117,7 +247,8 @@ module data_view {
let is_little_endian : Object = arguments.length > 1 ?
arguments[1] :
Undefined;
return DataViewGetInt32(context, receiver, offset, is_little_endian);
return DataViewGet(context, receiver, offset, is_little_endian,
INT32_ELEMENTS);
}
javascript builtin DataViewPrototypeGetUint32(
......@@ -128,7 +259,8 @@ module data_view {
let is_little_endian : Object = arguments.length > 1 ?
arguments[1] :
Undefined;
return DataViewGetUint32(context, receiver, offset, is_little_endian);
return DataViewGet(context, receiver, offset, is_little_endian,
UINT32_ELEMENTS);
}
javascript builtin DataViewPrototypeGetFloat32(
......@@ -175,7 +307,6 @@ module data_view {
return DataViewGetBigUint64(context, receiver, offset, is_little_endian);
}
// Context, receiver, byteOffset, value, littleEndian
extern runtime
DataViewSetInt8(Context, Object, Object, Object, Object): Object;
......
......@@ -11418,6 +11418,12 @@ TNode<JSArrayBuffer> CodeStubAssembler::LoadArrayBufferViewBuffer(
JSArrayBufferView::kBufferOffset);
}
TNode<RawPtrT> CodeStubAssembler::LoadArrayBufferBackingStore(
TNode<JSArrayBuffer> array_buffer) {
return LoadObjectField<RawPtrT>(array_buffer,
JSArrayBuffer::kBackingStoreOffset);
}
CodeStubArguments::CodeStubArguments(
CodeStubAssembler* assembler, Node* argc, Node* fp,
CodeStubAssembler::ParameterMode param_mode, ReceiverMode receiver_mode)
......
......@@ -2445,6 +2445,7 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler {
Node* IsDetachedBuffer(Node* buffer);
TNode<JSArrayBuffer> LoadArrayBufferViewBuffer(
TNode<JSArrayBufferView> array_buffer_view);
TNode<RawPtrT> LoadArrayBufferBackingStore(TNode<JSArrayBuffer> array_buffer);
TNode<IntPtrT> ElementOffsetFromIndex(Node* index, ElementsKind kind,
ParameterMode mode, int base_size = 0);
......
......@@ -58,7 +58,9 @@ struct WordT : IntegralT {
: MachineRepresentation::kWord64;
};
struct RawPtrT : WordT {};
struct RawPtrT : WordT {
static constexpr MachineType kMachineType = MachineType::Pointer();
};
template <class To>
struct RawPtr : RawPtrT {};
......
......@@ -353,12 +353,6 @@ bool IntrinsicHasNoSideEffect(Runtime::FunctionId id) {
V(TrySliceSimpleNonFastElements) \
V(TypedArrayGetBuffer) \
/* DataView */ \
V(DataViewGetInt8) \
V(DataViewGetUint8) \
V(DataViewGetInt16) \
V(DataViewGetUint16) \
V(DataViewGetInt32) \
V(DataViewGetUint32) \
V(DataViewGetFloat32) \
V(DataViewGetFloat64) \
V(DataViewGetBigInt64) \
......
......@@ -49,6 +49,9 @@ int ElementsKindToShiftSize(ElementsKind elements_kind) {
UNREACHABLE();
}
int ElementsKindToByteSize(ElementsKind elements_kind) {
return 1 << ElementsKindToShiftSize(elements_kind);
}
int GetDefaultHeaderSizeForElementsKind(ElementsKind elements_kind) {
STATIC_ASSERT(FixedArray::kHeaderSize == FixedDoubleArray::kHeaderSize);
......
......@@ -74,6 +74,7 @@ const int kFastElementsKindPackedToHoley =
HOLEY_SMI_ELEMENTS - PACKED_SMI_ELEMENTS;
int ElementsKindToShiftSize(ElementsKind elements_kind);
int ElementsKindToByteSize(ElementsKind elements_kind);
int GetDefaultHeaderSizeForElementsKind(ElementsKind elements_kind);
const char* ElementsKindToString(ElementsKind kind);
......
......@@ -256,12 +256,6 @@ MaybeHandle<Object> SetViewValue(Isolate* isolate, Handle<JSDataView> data_view,
return *result; \
}
DATA_VIEW_PROTOTYPE_GET(Int8, int8_t)
DATA_VIEW_PROTOTYPE_GET(Uint8, uint8_t)
DATA_VIEW_PROTOTYPE_GET(Int16, int16_t)
DATA_VIEW_PROTOTYPE_GET(Uint16, uint16_t)
DATA_VIEW_PROTOTYPE_GET(Int32, int32_t)
DATA_VIEW_PROTOTYPE_GET(Uint32, uint32_t)
DATA_VIEW_PROTOTYPE_GET(Float32, float)
DATA_VIEW_PROTOTYPE_GET(Float64, double)
DATA_VIEW_PROTOTYPE_GET(BigInt64, int64_t)
......
......@@ -569,12 +569,6 @@ namespace internal {
F(TypedArraySortFast, 1, 1)
#define FOR_EACH_INTRINSIC_DATAVIEW(F) \
F(DataViewGetInt8, 1, 1) \
F(DataViewGetUint8, 1, 1) \
F(DataViewGetInt16, 2, 1) \
F(DataViewGetUint16, 2, 1) \
F(DataViewGetInt32, 2, 1) \
F(DataViewGetUint32, 2, 1) \
F(DataViewGetFloat32, 2, 1) \
F(DataViewGetFloat64, 2, 1) \
F(DataViewGetBigInt64, 2, 1) \
......
......@@ -297,7 +297,7 @@ class SourceProcessor(SourceFileProcessor):
m = pattern.match(line)
if m:
runtime_functions.append(m.group(1))
if len(runtime_functions) < 500:
if len(runtime_functions) < 475:
print ("Runtime functions list is suspiciously short. "
"Consider updating the presubmit script.")
sys.exit(1)
......
......@@ -266,33 +266,33 @@ KNOWN_MAPS = {
("RO_SPACE", 0x05019): (172, "Tuple2Map"),
("RO_SPACE", 0x05211): (170, "ScriptMap"),
("RO_SPACE", 0x053d9): (162, "InterceptorInfoMap"),
("RO_SPACE", 0x09cb9): (154, "AccessorInfoMap"),
("RO_SPACE", 0x09ec9): (153, "AccessCheckInfoMap"),
("RO_SPACE", 0x09f31): (155, "AccessorPairMap"),
("RO_SPACE", 0x09f99): (156, "AliasedArgumentsEntryMap"),
("RO_SPACE", 0x0a001): (157, "AllocationMementoMap"),
("RO_SPACE", 0x0a069): (158, "AllocationSiteMap"),
("RO_SPACE", 0x0a0d1): (159, "AsyncGeneratorRequestMap"),
("RO_SPACE", 0x0a139): (160, "DebugInfoMap"),
("RO_SPACE", 0x0a1a1): (161, "FunctionTemplateInfoMap"),
("RO_SPACE", 0x0a209): (163, "InterpreterDataMap"),
("RO_SPACE", 0x0a271): (164, "ModuleInfoEntryMap"),
("RO_SPACE", 0x0a2d9): (165, "ModuleMap"),
("RO_SPACE", 0x0a341): (166, "ObjectTemplateInfoMap"),
("RO_SPACE", 0x0a3a9): (167, "PromiseCapabilityMap"),
("RO_SPACE", 0x0a411): (168, "PromiseReactionMap"),
("RO_SPACE", 0x0a479): (169, "PrototypeInfoMap"),
("RO_SPACE", 0x0a4e1): (171, "StackFrameInfoMap"),
("RO_SPACE", 0x0a549): (173, "Tuple3Map"),
("RO_SPACE", 0x0a5b1): (174, "WasmCompiledModuleMap"),
("RO_SPACE", 0x0a619): (175, "WasmDebugInfoMap"),
("RO_SPACE", 0x0a681): (176, "WasmExportedFunctionDataMap"),
("RO_SPACE", 0x0a6e9): (177, "WasmSharedModuleDataMap"),
("RO_SPACE", 0x0a751): (178, "CallableTaskMap"),
("RO_SPACE", 0x0a7b9): (179, "CallbackTaskMap"),
("RO_SPACE", 0x0a821): (180, "PromiseFulfillReactionJobTaskMap"),
("RO_SPACE", 0x0a889): (181, "PromiseRejectReactionJobTaskMap"),
("RO_SPACE", 0x0a8f1): (182, "PromiseResolveThenableJobTaskMap"),
("RO_SPACE", 0x09d21): (154, "AccessorInfoMap"),
("RO_SPACE", 0x09f31): (153, "AccessCheckInfoMap"),
("RO_SPACE", 0x09f99): (155, "AccessorPairMap"),
("RO_SPACE", 0x0a001): (156, "AliasedArgumentsEntryMap"),
("RO_SPACE", 0x0a069): (157, "AllocationMementoMap"),
("RO_SPACE", 0x0a0d1): (158, "AllocationSiteMap"),
("RO_SPACE", 0x0a139): (159, "AsyncGeneratorRequestMap"),
("RO_SPACE", 0x0a1a1): (160, "DebugInfoMap"),
("RO_SPACE", 0x0a209): (161, "FunctionTemplateInfoMap"),
("RO_SPACE", 0x0a271): (163, "InterpreterDataMap"),
("RO_SPACE", 0x0a2d9): (164, "ModuleInfoEntryMap"),
("RO_SPACE", 0x0a341): (165, "ModuleMap"),
("RO_SPACE", 0x0a3a9): (166, "ObjectTemplateInfoMap"),
("RO_SPACE", 0x0a411): (167, "PromiseCapabilityMap"),
("RO_SPACE", 0x0a479): (168, "PromiseReactionMap"),
("RO_SPACE", 0x0a4e1): (169, "PrototypeInfoMap"),
("RO_SPACE", 0x0a549): (171, "StackFrameInfoMap"),
("RO_SPACE", 0x0a5b1): (173, "Tuple3Map"),
("RO_SPACE", 0x0a619): (174, "WasmCompiledModuleMap"),
("RO_SPACE", 0x0a681): (175, "WasmDebugInfoMap"),
("RO_SPACE", 0x0a6e9): (176, "WasmExportedFunctionDataMap"),
("RO_SPACE", 0x0a751): (177, "WasmSharedModuleDataMap"),
("RO_SPACE", 0x0a7b9): (178, "CallableTaskMap"),
("RO_SPACE", 0x0a821): (179, "CallbackTaskMap"),
("RO_SPACE", 0x0a889): (180, "PromiseFulfillReactionJobTaskMap"),
("RO_SPACE", 0x0a8f1): (181, "PromiseRejectReactionJobTaskMap"),
("RO_SPACE", 0x0a959): (182, "PromiseResolveThenableJobTaskMap"),
("MAP_SPACE", 0x02201): (1057, "ExternalMap"),
("MAP_SPACE", 0x02259): (1072, "JSMessageObjectMap"),
}
......
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