Commit 579cf96c authored by Nikolaos Papaspyrou's avatar Nikolaos Papaspyrou Committed by V8 LUCI CQ

heap: Clean up conservative stack scanning prototype

This CL cleans up the existing experimental implementation of
conservative stack scanning. It retains the object start bitmap, to
evaluate it as a mechanism for resolving inner pointers, and the
conservative stack scanning visitor (which is currently not used).

The flag v8_enable_conservative_stack_scanning is kept and will be
used for experimental purposes. It currently does not imply any
other flag.

Bug: v8:10614
Bug: v8:12851

Change-Id: Id0ae0f437ed2601eed9ec634d2d1dd2f030d814e
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3602516Reviewed-by: 's avatarOmer Katz <omerkatz@chromium.org>
Reviewed-by: 's avatarMichael Lippautz <mlippautz@chromium.org>
Reviewed-by: 's avatarJakob Linke <jgruber@chromium.org>
Commit-Queue: Nikolaos Papaspyrou <nikolaos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#80365}
parent c8c06323
...@@ -567,9 +567,6 @@ if (v8_enable_single_generation == true) { ...@@ -567,9 +567,6 @@ if (v8_enable_single_generation == true) {
"Requires unconditional write barriers or none (which disables incremental marking)") "Requires unconditional write barriers or none (which disables incremental marking)")
} }
assert(!v8_enable_conservative_stack_scanning || v8_enable_single_generation,
"Conservative stack scanning requires single generation")
if (v8_fuchsia_use_vmex_resource) { if (v8_fuchsia_use_vmex_resource) {
assert(target_os == "fuchsia", "VMEX resource only available on Fuchsia") assert(target_os == "fuchsia", "VMEX resource only available on Fuchsia")
} }
......
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
#include "src/base/logging.h" #include "src/base/logging.h"
#include "src/base/platform/mutex.h" #include "src/base/platform/mutex.h"
#include "src/base/platform/platform.h" #include "src/base/platform/platform.h"
#include "src/base/platform/wrappers.h"
#include "src/base/sys-info.h" #include "src/base/sys-info.h"
#include "src/base/utils/random-number-generator.h" #include "src/base/utils/random-number-generator.h"
#include "src/baseline/baseline-batch-compiler.h" #include "src/baseline/baseline-batch-compiler.h"
...@@ -132,11 +133,6 @@ ...@@ -132,11 +133,6 @@
#include "src/diagnostics/unwinding-info-win64.h" #include "src/diagnostics/unwinding-info-win64.h"
#endif // V8_OS_WIN64 #endif // V8_OS_WIN64
#ifdef V8_ENABLE_CONSERVATIVE_STACK_SCANNING
#include "src/base/platform/wrappers.h"
#include "src/heap/conservative-stack-visitor.h"
#endif
#if USE_SIMULATOR #if USE_SIMULATOR
#include "src/execution/simulator-base.h" #include "src/execution/simulator-base.h"
#endif #endif
...@@ -570,11 +566,6 @@ void Isolate::Iterate(RootVisitor* v, ThreadLocalTop* thread) { ...@@ -570,11 +566,6 @@ void Isolate::Iterate(RootVisitor* v, ThreadLocalTop* thread) {
FullObjectSlot(reinterpret_cast<Address>(&(block->message_obj_)))); FullObjectSlot(reinterpret_cast<Address>(&(block->message_obj_))));
} }
#ifdef V8_ENABLE_CONSERVATIVE_STACK_SCANNING
ConservativeStackVisitor stack_visitor(this, v);
thread_local_top()->stack_.IteratePointers(&stack_visitor);
#endif
// Iterate over pointers on native execution stack. // Iterate over pointers on native execution stack.
#if V8_ENABLE_WEBASSEMBLY #if V8_ENABLE_WEBASSEMBLY
wasm::WasmCodeRefScope wasm_code_ref_scope; wasm::WasmCodeRefScope wasm_code_ref_scope;
......
...@@ -37,9 +37,6 @@ void ThreadLocalTop::Clear() { ...@@ -37,9 +37,6 @@ void ThreadLocalTop::Clear() {
current_embedder_state_ = nullptr; current_embedder_state_ = nullptr;
failed_access_check_callback_ = nullptr; failed_access_check_callback_ = nullptr;
thread_in_wasm_flag_address_ = kNullAddress; thread_in_wasm_flag_address_ = kNullAddress;
#ifdef V8_ENABLE_CONSERVATIVE_STACK_SCANNING
stack_ = ::heap::base::Stack(nullptr);
#endif
} }
void ThreadLocalTop::Initialize(Isolate* isolate) { void ThreadLocalTop::Initialize(Isolate* isolate) {
...@@ -53,10 +50,6 @@ void ThreadLocalTop::Initialize(Isolate* isolate) { ...@@ -53,10 +50,6 @@ void ThreadLocalTop::Initialize(Isolate* isolate) {
#ifdef USE_SIMULATOR #ifdef USE_SIMULATOR
simulator_ = Simulator::current(isolate); simulator_ = Simulator::current(isolate);
#endif #endif
#ifdef V8_ENABLE_CONSERVATIVE_STACK_SCANNING
stack_ = ::heap::base::Stack(base::Stack::GetStackStart());
#endif
} }
void ThreadLocalTop::Free() {} void ThreadLocalTop::Free() {}
......
...@@ -13,10 +13,6 @@ ...@@ -13,10 +13,6 @@
#include "src/objects/contexts.h" #include "src/objects/contexts.h"
#include "src/utils/utils.h" #include "src/utils/utils.h"
#ifdef V8_ENABLE_CONSERVATIVE_STACK_SCANNING
#include "src/heap/base/stack.h"
#endif
namespace v8 { namespace v8 {
class TryCatch; class TryCatch;
...@@ -33,11 +29,7 @@ class ThreadLocalTop { ...@@ -33,11 +29,7 @@ class ThreadLocalTop {
// TODO(all): This is not particularly beautiful. We should probably // TODO(all): This is not particularly beautiful. We should probably
// refactor this to really consist of just Addresses and 32-bit // refactor this to really consist of just Addresses and 32-bit
// integer fields. // integer fields.
#ifdef V8_ENABLE_CONSERVATIVE_STACK_SCANNING
static constexpr uint32_t kSizeInBytes = 26 * kSystemPointerSize;
#else
static constexpr uint32_t kSizeInBytes = 25 * kSystemPointerSize; static constexpr uint32_t kSizeInBytes = 25 * kSystemPointerSize;
#endif
// Does early low-level initialization that does not depend on the // Does early low-level initialization that does not depend on the
// isolate being present. // isolate being present.
...@@ -154,10 +146,6 @@ class ThreadLocalTop { ...@@ -154,10 +146,6 @@ class ThreadLocalTop {
// Address of the thread-local "thread in wasm" flag. // Address of the thread-local "thread in wasm" flag.
Address thread_in_wasm_flag_address_; Address thread_in_wasm_flag_address_;
#ifdef V8_ENABLE_CONSERVATIVE_STACK_SCANNING
::heap::base::Stack stack_;
#endif
}; };
} // namespace internal } // namespace internal
......
...@@ -37,25 +37,26 @@ bool ConservativeStackVisitor::CheckPage(Address address, MemoryChunk* page) { ...@@ -37,25 +37,26 @@ bool ConservativeStackVisitor::CheckPage(Address address, MemoryChunk* page) {
return false; return false;
} }
// TODO(jakehughes) Pinning is only required for the marking visitor. Other Object root = obj;
// visitors (such as verify visitor) could work without pining. This should delegate_->VisitRootPointer(Root::kHandleScope, nullptr,
// be moved to delegate_ FullObjectSlot(&root));
page->SetFlag(BasicMemoryChunk::Flag::PINNED); // Check that the delegate visitor did not modify the root slot.
DCHECK_EQ(root, obj);
Object ptr = HeapObject::FromAddress(base_ptr);
FullObjectSlot root = FullObjectSlot(&ptr);
delegate_->VisitRootPointer(Root::kHandleScope, nullptr, root);
DCHECK(root == FullObjectSlot(reinterpret_cast<Address>(&base_ptr)));
return true; return true;
} }
void ConservativeStackVisitor::VisitConservativelyIfPointer( void ConservativeStackVisitor::VisitConservativelyIfPointer(
const void* pointer) { const void* pointer) {
auto address = reinterpret_cast<Address>(pointer); auto address = reinterpret_cast<Address>(pointer);
// TODO(v8:12851): Let's figure out what this meant to do...
// This condition is always true, as the LAB invariant requires
// start <= top <= limit
#if 0
if (address > isolate_->heap()->old_space()->top() || if (address > isolate_->heap()->old_space()->top() ||
address < isolate_->heap()->old_space()->limit()) { address < isolate_->heap()->old_space()->limit()) {
return; return;
} }
#endif
for (Page* page : *isolate_->heap()->old_space()) { for (Page* page : *isolate_->heap()->old_space()) {
if (CheckPage(address, page)) { if (CheckPage(address, page)) {
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
#include "src/base/logging.h" #include "src/base/logging.h"
#include "src/base/once.h" #include "src/base/once.h"
#include "src/base/platform/mutex.h" #include "src/base/platform/mutex.h"
#include "src/base/platform/wrappers.h"
#include "src/base/utils/random-number-generator.h" #include "src/base/utils/random-number-generator.h"
#include "src/builtins/accessors.h" #include "src/builtins/accessors.h"
#include "src/codegen/assembler-inl.h" #include "src/codegen/assembler-inl.h"
...@@ -34,7 +35,6 @@ ...@@ -34,7 +35,6 @@
#include "src/execution/vm-state-inl.h" #include "src/execution/vm-state-inl.h"
#include "src/handles/global-handles-inl.h" #include "src/handles/global-handles-inl.h"
#include "src/heap/array-buffer-sweeper.h" #include "src/heap/array-buffer-sweeper.h"
#include "src/heap/base/stack.h"
#include "src/heap/basic-memory-chunk.h" #include "src/heap/basic-memory-chunk.h"
#include "src/heap/code-object-registry.h" #include "src/heap/code-object-registry.h"
#include "src/heap/code-range.h" #include "src/heap/code-range.h"
...@@ -102,12 +102,6 @@ ...@@ -102,12 +102,6 @@
#include "src/tracing/trace-event.h" #include "src/tracing/trace-event.h"
#include "src/utils/utils-inl.h" #include "src/utils/utils-inl.h"
#include "src/utils/utils.h" #include "src/utils/utils.h"
#ifdef V8_ENABLE_CONSERVATIVE_STACK_SCANNING
#include "src/heap/conservative-stack-visitor.h"
#endif
#include "src/base/platform/wrappers.h"
// Has to be the last include (doesn't have include guards): // Has to be the last include (doesn't have include guards):
#include "src/objects/object-macros.h" #include "src/objects/object-macros.h"
...@@ -5062,7 +5056,6 @@ void Heap::IterateRoots(RootVisitor* v, base::EnumSet<SkipRoot> options) { ...@@ -5062,7 +5056,6 @@ void Heap::IterateRoots(RootVisitor* v, base::EnumSet<SkipRoot> options) {
v->Synchronize(VisitorSynchronization::kStackRoots); v->Synchronize(VisitorSynchronization::kStackRoots);
} }
#ifndef V8_ENABLE_CONSERVATIVE_STACK_SCANNING
// Iterate over main thread handles in handle scopes. // Iterate over main thread handles in handle scopes.
if (!options.contains(SkipRoot::kMainThreadHandles)) { if (!options.contains(SkipRoot::kMainThreadHandles)) {
// Clear main thread handles with stale references to left-trimmed // Clear main thread handles with stale references to left-trimmed
...@@ -5072,7 +5065,6 @@ void Heap::IterateRoots(RootVisitor* v, base::EnumSet<SkipRoot> options) { ...@@ -5072,7 +5065,6 @@ void Heap::IterateRoots(RootVisitor* v, base::EnumSet<SkipRoot> options) {
isolate_->handle_scope_implementer()->Iterate(v); isolate_->handle_scope_implementer()->Iterate(v);
} }
#endif
// Iterate local handles for all local heaps. // Iterate local handles for all local heaps.
safepoint_->Iterate(v); safepoint_->Iterate(v);
......
...@@ -868,11 +868,6 @@ void MarkCompactCollector::CollectEvacuationCandidates(PagedSpace* space) { ...@@ -868,11 +868,6 @@ void MarkCompactCollector::CollectEvacuationCandidates(PagedSpace* space) {
} else { } else {
pages.push_back(std::make_pair(p->allocated_bytes(), p)); pages.push_back(std::make_pair(p->allocated_bytes(), p));
} }
// Unpin pages for the next GC
if (p->IsFlagSet(MemoryChunk::PINNED)) {
p->ClearFlag(MemoryChunk::PINNED);
}
} }
int candidate_count = 0; int candidate_count = 0;
......
...@@ -197,7 +197,7 @@ MemoryChunk::MemoryChunk(Heap* heap, BaseSpace* space, size_t chunk_size, ...@@ -197,7 +197,7 @@ MemoryChunk::MemoryChunk(Heap* heap, BaseSpace* space, size_t chunk_size,
if (heap->IsShared()) SetFlag(MemoryChunk::IN_SHARED_HEAP); if (heap->IsShared()) SetFlag(MemoryChunk::IN_SHARED_HEAP);
#ifdef V8_ENABLE_CONSERVATIVE_STACK_SCANNING #ifdef V8_ENABLE_CONSERVATIVE_STACK_SCANNING
chunk->object_start_bitmap_ = ObjectStartBitmap(chunk->area_start()); object_start_bitmap_ = ObjectStartBitmap(area_start_);
#endif #endif
#ifdef DEBUG #ifdef DEBUG
......
...@@ -99,45 +99,43 @@ Address ObjectStartBitmap::FindBasePtr(Address maybe_inner_ptr) const { ...@@ -99,45 +99,43 @@ Address ObjectStartBitmap::FindBasePtr(Address maybe_inner_ptr) const {
DCHECK_GT(object_start_bit_map_.size(), cell_index); DCHECK_GT(object_start_bit_map_.size(), cell_index);
const size_t bit = object_start_number & kCellMask; const size_t bit = object_start_number & kCellMask;
// check if maybe_inner_ptr is the base pointer // check if maybe_inner_ptr is the base pointer
uint32_t byte = load(cell_index) & ((1 << (bit + 1)) - 1); uint32_t byte =
while (!byte && cell_index) { load(cell_index) & static_cast<uint32_t>((1 << (bit + 1)) - 1);
DCHECK_LT(0u, cell_index); while (byte == 0 && cell_index > 0) {
byte = load(--cell_index); byte = load(--cell_index);
} }
const int leading_zeroes = v8::base::bits::CountLeadingZeros(byte); if (byte == 0) {
if (leading_zeroes == kBitsPerCell) { DCHECK_EQ(0, cell_index);
return kNullAddress; return kNullAddress;
} }
const int leading_zeroes = v8::base::bits::CountLeadingZeros(byte);
DCHECK_GT(kBitsPerCell, leading_zeroes);
object_start_number = object_start_number =
(cell_index * kBitsPerCell) + (kBitsPerCell - 1) - leading_zeroes; (cell_index * kBitsPerCell) + (kBitsPerCell - 1) - leading_zeroes;
Address base_ptr = StartIndexToAddress(object_start_number); return StartIndexToAddress(object_start_number);
return base_ptr;
} }
void ObjectStartBitmap::SetBit(Address base_ptr) { void ObjectStartBitmap::SetBit(Address base_ptr) {
size_t cell_index, object_bit; size_t cell_index, object_bit;
ObjectStartIndexAndBit(base_ptr, &cell_index, &object_bit); ObjectStartIndexAndBit(base_ptr, &cell_index, &object_bit);
store(cell_index, store(cell_index, load(cell_index) | static_cast<uint32_t>(1 << object_bit));
static_cast<uint32_t>(load(cell_index) | (1 << object_bit)));
} }
void ObjectStartBitmap::ClearBit(Address base_ptr) { void ObjectStartBitmap::ClearBit(Address base_ptr) {
size_t cell_index, object_bit; size_t cell_index, object_bit;
ObjectStartIndexAndBit(base_ptr, &cell_index, &object_bit); ObjectStartIndexAndBit(base_ptr, &cell_index, &object_bit);
store(cell_index, store(cell_index,
static_cast<uint32_t>(load(cell_index) & ~(1 << object_bit))); load(cell_index) & static_cast<uint32_t>(~(1 << object_bit)));
} }
bool ObjectStartBitmap::CheckBit(Address base_ptr) const { bool ObjectStartBitmap::CheckBit(Address base_ptr) const {
size_t cell_index, object_bit; size_t cell_index, object_bit;
ObjectStartIndexAndBit(base_ptr, &cell_index, &object_bit); ObjectStartIndexAndBit(base_ptr, &cell_index, &object_bit);
return load(cell_index) & (1 << object_bit); return (load(cell_index) & static_cast<uint32_t>(1 << object_bit)) != 0;
} }
void ObjectStartBitmap::store(size_t cell_index, uint32_t value) { void ObjectStartBitmap::store(size_t cell_index, uint32_t value) {
object_start_bit_map_[cell_index] = value; object_start_bit_map_[cell_index] = value;
return;
} }
uint32_t ObjectStartBitmap::load(size_t cell_index) const { uint32_t ObjectStartBitmap::load(size_t cell_index) const {
...@@ -165,15 +163,17 @@ Address ObjectStartBitmap::StartIndexToAddress( ...@@ -165,15 +163,17 @@ Address ObjectStartBitmap::StartIndexToAddress(
template <typename Callback> template <typename Callback>
inline void ObjectStartBitmap::Iterate(Callback callback) const { inline void ObjectStartBitmap::Iterate(Callback callback) const {
for (size_t cell_index = 0; cell_index < kReservedForBitmap; cell_index++) { for (size_t cell_index = 0; cell_index < kReservedForBitmap; cell_index++) {
uint32_t value = object_start_bit_map_[cell_index]; uint32_t value = load(cell_index);
while (value) { while (value != 0) {
const int trailing_zeroes = v8::base::bits::CountTrailingZeros(value); const int trailing_zeroes =
v8::base::bits::CountTrailingZerosNonZero(value);
DCHECK_GT(kBitsPerCell, trailing_zeroes);
const size_t object_start_number = const size_t object_start_number =
(cell_index * kBitsPerCell) + trailing_zeroes; (cell_index * kBitsPerCell) + trailing_zeroes;
const Address object_address = StartIndexToAddress(object_start_number); const Address object_address = StartIndexToAddress(object_start_number);
callback(object_address); callback(object_address);
// Clear current object bit in temporary value to advance iteration. // Clear current object bit in temporary value to advance iteration.
value &= ~(1 << (object_start_number & kCellMask)); value &= value - 1;
} }
} }
} }
......
...@@ -77,6 +77,7 @@ bool PagedSpaceObjectIterator::AdvanceToNextPage() { ...@@ -77,6 +77,7 @@ bool PagedSpaceObjectIterator::AdvanceToNextPage() {
DCHECK(cur_page->SweepingDone()); DCHECK(cur_page->SweepingDone());
return true; return true;
} }
Page* PagedSpace::InitializePage(MemoryChunk* chunk) { Page* PagedSpace::InitializePage(MemoryChunk* chunk) {
Page* page = static_cast<Page*>(chunk); Page* page = static_cast<Page*>(chunk);
DCHECK_EQ( DCHECK_EQ(
......
...@@ -227,10 +227,7 @@ HEAP_TEST(DoNotEvacuatePinnedPages) { ...@@ -227,10 +227,7 @@ HEAP_TEST(DoNotEvacuatePinnedPages) {
} }
HEAP_TEST(ObjectStartBitmap) { HEAP_TEST(ObjectStartBitmap) {
if (!FLAG_single_generation || !FLAG_conservative_stack_scanning) return;
#if V8_ENABLE_CONSERVATIVE_STACK_SCANNING #if V8_ENABLE_CONSERVATIVE_STACK_SCANNING
CcTest::InitializeVM(); CcTest::InitializeVM();
Isolate* isolate = CcTest::i_isolate(); Isolate* isolate = CcTest::i_isolate();
v8::HandleScope sc(CcTest::isolate()); v8::HandleScope sc(CcTest::isolate());
...@@ -239,28 +236,67 @@ HEAP_TEST(ObjectStartBitmap) { ...@@ -239,28 +236,67 @@ HEAP_TEST(ObjectStartBitmap) {
heap::SealCurrentObjects(heap); heap::SealCurrentObjects(heap);
auto* factory = isolate->factory(); auto* factory = isolate->factory();
HeapObject obj = *factory->NewStringFromStaticChars("hello");
HeapObject obj2 = *factory->NewStringFromStaticChars("world");
Page* page = Page::FromAddress(obj.ptr());
CHECK(page->object_start_bitmap()->CheckBit(obj.address())); // TODO(v8:12851): Using handles because conservative stack scanning currently
CHECK(page->object_start_bitmap()->CheckBit(obj2.address())); // does not work.
Handle<String> h1 = factory->NewStringFromStaticChars("hello");
Handle<String> h2 = factory->NewStringFromStaticChars("world");
HeapObject obj1 = *h1;
HeapObject obj2 = *h2;
Page* page1 = Page::FromAddress(obj1.ptr());
Page* page2 = Page::FromAddress(obj2.ptr());
Address obj_inner_ptr = obj.ptr() + 2; CHECK(page1->object_start_bitmap()->CheckBit(obj1.address()));
CHECK(page->object_start_bitmap()->FindBasePtr(obj_inner_ptr) == CHECK(page2->object_start_bitmap()->CheckBit(obj2.address()));
obj.address());
Address obj2_inner_ptr = obj2.ptr() + 2; for (int k = 0; k < obj1.Size(); ++k) {
CHECK(page->object_start_bitmap()->FindBasePtr(obj2_inner_ptr) == Address obj1_inner_ptr = obj1.address() + k;
obj2.address()); CHECK_EQ(obj1.address(),
page1->object_start_bitmap()->FindBasePtr(obj1_inner_ptr));
}
for (int k = 0; k < obj2.Size(); ++k) {
Address obj2_inner_ptr = obj2.address() + k;
CHECK_EQ(obj2.address(),
page2->object_start_bitmap()->FindBasePtr(obj2_inner_ptr));
}
CcTest::CollectAllGarbage(); CcTest::CollectAllGarbage();
CHECK((obj).IsString()); obj1 = *h1;
CHECK((obj2).IsString()); obj2 = *h2;
CHECK(page->object_start_bitmap()->CheckBit(obj.address())); page1 = Page::FromAddress(obj1.ptr());
CHECK(page->object_start_bitmap()->CheckBit(obj2.address())); page2 = Page::FromAddress(obj2.ptr());
CHECK(obj1.IsString());
CHECK(obj2.IsString());
// TODO(v8:12851): Bits set in the object_start_bitmap are currently not
// preserved when objects are evacuated. For this reason, in the following
// checks, FindBasePtr is expected to return null after garbage collection.
for (int k = 0; k < obj1.Size(); ++k) {
Address obj1_inner_ptr = obj1.address() + k;
CHECK_EQ(kNullAddress,
page1->object_start_bitmap()->FindBasePtr(obj1_inner_ptr));
}
for (int k = 0; k < obj2.Size(); ++k) {
Address obj2_inner_ptr = obj2.address() + k;
CHECK_EQ(kNullAddress,
page2->object_start_bitmap()->FindBasePtr(obj2_inner_ptr));
}
obj1 = *h1;
obj2 = *h2;
page1 = Page::FromAddress(obj1.ptr());
page2 = Page::FromAddress(obj2.ptr());
CHECK(obj1.IsString());
CHECK(obj2.IsString());
// TODO(v8:12851): Bits set in the object_start_bitmap are currently not
// preserved when objects are evacuated.
CHECK(!page1->object_start_bitmap()->CheckBit(obj1.address()));
CHECK(!page2->object_start_bitmap()->CheckBit(obj2.address()));
#endif #endif
} }
...@@ -270,7 +306,6 @@ static Handle<Map> CreateMap(Isolate* isolate) { ...@@ -270,7 +306,6 @@ static Handle<Map> CreateMap(Isolate* isolate) {
return isolate->factory()->NewMap(JS_OBJECT_TYPE, JSObject::kHeaderSize); return isolate->factory()->NewMap(JS_OBJECT_TYPE, JSObject::kHeaderSize);
} }
TEST(MapCompact) { TEST(MapCompact) {
FLAG_max_map_space_pages = 16; FLAG_max_map_space_pages = 16;
CcTest::InitializeVM(); CcTest::InitializeVM();
......
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