Commit 70c3a714 authored by adamk@chromium.org's avatar adamk@chromium.org

ES6 Map/Set iterators/forEach improvements

This changes how Map/Set interacts with its iterators. When the
underlying table is rehashed or cleared, we create a new table (like
before) but we add a reference from the old table to the new table. We
also add an array describing how to transition the iterator from the
old table to the new table.

When Next is called on the iterator it checks if there is a newer table
that it should transition to. If there is, it updates the index based
on the previously recorded changes and finally changes itself to point
at the new table.

With these changes Map/Set no longer keeps the iterators alive. Also,
as before, the iterators keep the underlying table(s) alive but not the
actual Map/Set.

BUG=v8:1793
LOG=Y
R=mstarzinger@chromium.org, rossberg@chromium.org

Review URL: https://codereview.chromium.org/289503002

Patch from Erik Arvidsson <arv@chromium.org>.

git-svn-id: https://v8.googlecode.com/svn/branches/bleeding_edge@21389 ce2b1a6d-e550-0410-aec6-3dcde31c8c00
parent 014bf8b4
...@@ -106,12 +106,8 @@ function SetForEach(f, receiver) { ...@@ -106,12 +106,8 @@ function SetForEach(f, receiver) {
var iterator = %SetCreateIterator(this, ITERATOR_KIND_VALUES); var iterator = %SetCreateIterator(this, ITERATOR_KIND_VALUES);
var entry; var entry;
try { while (!(entry = %SetIteratorNext(iterator)).done) {
while (!(entry = %SetIteratorNext(iterator)).done) { %_CallFunction(receiver, entry.value, entry.value, this, f);
%_CallFunction(receiver, entry.value, entry.value, this, f);
}
} finally {
%SetIteratorClose(iterator);
} }
} }
...@@ -219,12 +215,8 @@ function MapForEach(f, receiver) { ...@@ -219,12 +215,8 @@ function MapForEach(f, receiver) {
var iterator = %MapCreateIterator(this, ITERATOR_KIND_ENTRIES); var iterator = %MapCreateIterator(this, ITERATOR_KIND_ENTRIES);
var entry; var entry;
try { while (!(entry = %MapIteratorNext(iterator)).done) {
while (!(entry = %MapIteratorNext(iterator)).done) { %_CallFunction(receiver, entry.value[1], entry.value[0], this, f);
%_CallFunction(receiver, entry.value[1], entry.value[0], this, f);
}
} finally {
%MapIteratorClose(iterator);
} }
} }
......
...@@ -702,13 +702,7 @@ void JSSetIterator::JSSetIteratorVerify() { ...@@ -702,13 +702,7 @@ void JSSetIterator::JSSetIteratorVerify() {
VerifyHeapPointer(table()); VerifyHeapPointer(table());
CHECK(table()->IsOrderedHashTable() || table()->IsUndefined()); CHECK(table()->IsOrderedHashTable() || table()->IsUndefined());
CHECK(index()->IsSmi()); CHECK(index()->IsSmi());
CHECK(count()->IsSmi());
CHECK(kind()->IsSmi()); CHECK(kind()->IsSmi());
VerifyHeapPointer(next_iterator());
CHECK(next_iterator()->IsJSSetIterator() || next_iterator()->IsUndefined());
VerifyHeapPointer(table());
CHECK(previous_iterator()->IsJSSetIterator()
|| previous_iterator()->IsUndefined());
} }
...@@ -718,13 +712,7 @@ void JSMapIterator::JSMapIteratorVerify() { ...@@ -718,13 +712,7 @@ void JSMapIterator::JSMapIteratorVerify() {
VerifyHeapPointer(table()); VerifyHeapPointer(table());
CHECK(table()->IsOrderedHashTable() || table()->IsUndefined()); CHECK(table()->IsOrderedHashTable() || table()->IsUndefined());
CHECK(index()->IsSmi()); CHECK(index()->IsSmi());
CHECK(count()->IsSmi());
CHECK(kind()->IsSmi()); CHECK(kind()->IsSmi());
VerifyHeapPointer(next_iterator());
CHECK(next_iterator()->IsJSMapIterator() || next_iterator()->IsUndefined());
VerifyHeapPointer(table());
CHECK(previous_iterator()->IsJSMapIterator()
|| previous_iterator()->IsUndefined());
} }
......
...@@ -5683,12 +5683,7 @@ ACCESSORS(JSMap, table, Object, kTableOffset) ...@@ -5683,12 +5683,7 @@ ACCESSORS(JSMap, table, Object, kTableOffset)
ORDERED_HASH_TABLE_ITERATOR_ACCESSORS(table, Object, kTableOffset) ORDERED_HASH_TABLE_ITERATOR_ACCESSORS(table, Object, kTableOffset)
ORDERED_HASH_TABLE_ITERATOR_ACCESSORS(index, Smi, kIndexOffset) ORDERED_HASH_TABLE_ITERATOR_ACCESSORS(index, Smi, kIndexOffset)
ORDERED_HASH_TABLE_ITERATOR_ACCESSORS(count, Smi, kCountOffset)
ORDERED_HASH_TABLE_ITERATOR_ACCESSORS(kind, Smi, kKindOffset) ORDERED_HASH_TABLE_ITERATOR_ACCESSORS(kind, Smi, kKindOffset)
ORDERED_HASH_TABLE_ITERATOR_ACCESSORS(next_iterator, Object,
kNextIteratorOffset)
ORDERED_HASH_TABLE_ITERATOR_ACCESSORS(previous_iterator, Object,
kPreviousIteratorOffset)
#undef ORDERED_HASH_TABLE_ITERATOR_ACCESSORS #undef ORDERED_HASH_TABLE_ITERATOR_ACCESSORS
......
...@@ -743,14 +743,8 @@ void OrderedHashTableIterator<Derived, TableType>:: ...@@ -743,14 +743,8 @@ void OrderedHashTableIterator<Derived, TableType>::
table()->ShortPrint(out); table()->ShortPrint(out);
PrintF(out, "\n - index = "); PrintF(out, "\n - index = ");
index()->ShortPrint(out); index()->ShortPrint(out);
PrintF(out, "\n - count = ");
count()->ShortPrint(out);
PrintF(out, "\n - kind = "); PrintF(out, "\n - kind = ");
kind()->ShortPrint(out); kind()->ShortPrint(out);
PrintF(out, "\n - next_iterator = ");
next_iterator()->ShortPrint(out);
PrintF(out, "\n - previous_iterator = ");
previous_iterator()->ShortPrint(out);
PrintF(out, "\n"); PrintF(out, "\n");
} }
......
This diff is collapsed.
...@@ -4153,16 +4153,27 @@ class ObjectHashTable: public HashTable<ObjectHashTable, ...@@ -4153,16 +4153,27 @@ class ObjectHashTable: public HashTable<ObjectHashTable,
// [0]: bucket count // [0]: bucket count
// [1]: element count // [1]: element count
// [2]: deleted element count // [2]: deleted element count
// [3]: live iterators (doubly-linked list) // [3..(3 + NumberOfBuckets() - 1)]: "hash table", where each item is an
// [4..(NumberOfBuckets() - 1)]: "hash table", where each item is an offset // offset into the data table (see below) where the
// into the data table (see below) where the // first item in this bucket is stored.
// first item in this bucket is stored. // [3 + NumberOfBuckets()..length]: "data table", an array of length
// [4 + NumberOfBuckets()..length]: "data table", an array of length
// Capacity() * kEntrySize, where the first entrysize // Capacity() * kEntrySize, where the first entrysize
// items are handled by the derived class and the // items are handled by the derived class and the
// item at kChainOffset is another entry into the // item at kChainOffset is another entry into the
// data table indicating the next entry in this hash // data table indicating the next entry in this hash
// bucket. // bucket.
//
// When we transition the table to a new version we obsolete it and reuse parts
// of the memory to store information how to transition an iterator to the new
// table:
//
// Memory layout for obsolete table:
// [0]: bucket count
// [1]: Next newer table
// [2]: Number of removed holes or -1 when the table was cleared.
// [3..(3 + NumberOfRemovedHoles() - 1)]: The indexes of the removed holes.
// [3 + NumberOfRemovedHoles()..length]: Not used
//
template<class Derived, class Iterator, int entrysize> template<class Derived, class Iterator, int entrysize>
class OrderedHashTable: public FixedArray { class OrderedHashTable: public FixedArray {
public: public:
...@@ -4199,10 +4210,6 @@ class OrderedHashTable: public FixedArray { ...@@ -4199,10 +4210,6 @@ class OrderedHashTable: public FixedArray {
return Smi::cast(get(kNumberOfBucketsIndex))->value(); return Smi::cast(get(kNumberOfBucketsIndex))->value();
} }
Object* iterators() { return get(kIteratorsIndex); }
void set_iterators(Object* value) { set(kIteratorsIndex, value); }
// Returns the index into the data table where the new entry // Returns the index into the data table where the new entry
// should be placed. The table is assumed to have enough space // should be placed. The table is assumed to have enough space
// for a new entry. // for a new entry.
...@@ -4219,6 +4226,20 @@ class OrderedHashTable: public FixedArray { ...@@ -4219,6 +4226,20 @@ class OrderedHashTable: public FixedArray {
Object* KeyAt(int entry) { return get(EntryToIndex(entry)); } Object* KeyAt(int entry) { return get(EntryToIndex(entry)); }
bool IsObsolete() {
return !get(kNextTableIndex)->IsSmi();
}
// The next newer table. This is only valid if the table is obsolete.
Derived* NextTable() {
return Derived::cast(get(kNextTableIndex));
}
// When the table is obsolete we store the indexes of the removed holes.
int RemovedIndexAt(int index) {
return Smi::cast(get(kRemovedHolesIndex + index))->value();
}
static const int kNotFound = -1; static const int kNotFound = -1;
static const int kMinCapacity = 4; static const int kMinCapacity = 4;
...@@ -4255,11 +4276,21 @@ class OrderedHashTable: public FixedArray { ...@@ -4255,11 +4276,21 @@ class OrderedHashTable: public FixedArray {
return Smi::cast(get(kHashTableStartIndex + bucket))->value(); return Smi::cast(get(kHashTableStartIndex + bucket))->value();
} }
void SetNextTable(Derived* next_table) {
set(kNextTableIndex, next_table);
}
void SetRemovedIndexAt(int index, int removed_index) {
return set(kRemovedHolesIndex + index, Smi::FromInt(removed_index));
}
static const int kNumberOfBucketsIndex = 0; static const int kNumberOfBucketsIndex = 0;
static const int kNumberOfElementsIndex = kNumberOfBucketsIndex + 1; static const int kNumberOfElementsIndex = kNumberOfBucketsIndex + 1;
static const int kNumberOfDeletedElementsIndex = kNumberOfElementsIndex + 1; static const int kNumberOfDeletedElementsIndex = kNumberOfElementsIndex + 1;
static const int kIteratorsIndex = kNumberOfDeletedElementsIndex + 1; static const int kHashTableStartIndex = kNumberOfDeletedElementsIndex + 1;
static const int kHashTableStartIndex = kIteratorsIndex + 1;
static const int kNextTableIndex = kNumberOfElementsIndex;
static const int kRemovedHolesIndex = kHashTableStartIndex;
static const int kEntrySize = entrysize + 1; static const int kEntrySize = entrysize + 1;
static const int kChainOffset = entrysize; static const int kChainOffset = entrysize;
...@@ -9956,17 +9987,15 @@ class JSMap: public JSObject { ...@@ -9956,17 +9987,15 @@ class JSMap: public JSObject {
// OrderedHashTableIterator is an iterator that iterates over the keys and // OrderedHashTableIterator is an iterator that iterates over the keys and
// values of an OrderedHashTable. // values of an OrderedHashTable.
// //
// The hash table has a reference to the iterator and the iterators themselves // The iterator has a reference to the underlying OrderedHashTable data,
// have references to the [next_iterator] and [previous_iterator], thus creating // [table], as well as the current [index] the iterator is at.
// a double linked list.
// //
// When the hash table changes the iterators are called to update their [index] // When the OrderedHashTable is rehashed it adds a reference from the old table
// and [count]. The hash table calls [EntryRemoved], [TableCompacted] as well // to the new table as well as storing enough data about the changes so that the
// as [TableCleared]. // iterator [index] can be adjusted accordingly.
// //
// When an iterator is done it closes itself. It removes itself from the double // When the [Next] result from the iterator is requested, the iterator checks if
// linked list and it sets its [table] to undefined, no longer keeping the // there is a newer table that it needs to transition to.
// [table] alive.
template<class Derived, class TableType> template<class Derived, class TableType>
class OrderedHashTableIterator: public JSObject { class OrderedHashTableIterator: public JSObject {
public: public:
...@@ -9976,29 +10005,17 @@ class OrderedHashTableIterator: public JSObject { ...@@ -9976,29 +10005,17 @@ class OrderedHashTableIterator: public JSObject {
// [index]: The index into the data table. // [index]: The index into the data table.
DECL_ACCESSORS(index, Smi) DECL_ACCESSORS(index, Smi)
// [count]: The logical index into the data table, ignoring the holes.
DECL_ACCESSORS(count, Smi)
// [kind]: The kind of iteration this is. One of the [Kind] enum values. // [kind]: The kind of iteration this is. One of the [Kind] enum values.
DECL_ACCESSORS(kind, Smi) DECL_ACCESSORS(kind, Smi)
// [next_iterator]: Used as a double linked list for the live iterators.
DECL_ACCESSORS(next_iterator, Object)
// [previous_iterator]: Used as a double linked list for the live iterators.
DECL_ACCESSORS(previous_iterator, Object)
#ifdef OBJECT_PRINT #ifdef OBJECT_PRINT
void OrderedHashTableIteratorPrint(FILE* out); void OrderedHashTableIteratorPrint(FILE* out);
#endif #endif
static const int kTableOffset = JSObject::kHeaderSize; static const int kTableOffset = JSObject::kHeaderSize;
static const int kIndexOffset = kTableOffset + kPointerSize; static const int kIndexOffset = kTableOffset + kPointerSize;
static const int kCountOffset = kIndexOffset + kPointerSize; static const int kKindOffset = kIndexOffset + kPointerSize;
static const int kKindOffset = kCountOffset + kPointerSize; static const int kSize = kKindOffset + kPointerSize;
static const int kNextIteratorOffset = kKindOffset + kPointerSize;
static const int kPreviousIteratorOffset = kNextIteratorOffset + kPointerSize;
static const int kSize = kPreviousIteratorOffset + kPointerSize;
enum Kind { enum Kind {
kKindKeys = 1, kKindKeys = 1,
...@@ -10006,25 +10023,6 @@ class OrderedHashTableIterator: public JSObject { ...@@ -10006,25 +10023,6 @@ class OrderedHashTableIterator: public JSObject {
kKindEntries = 3 kKindEntries = 3
}; };
// Called by the underlying [table] when an entry is removed.
void EntryRemoved(int index);
// Called by the underlying [table] when it is compacted/rehashed.
void TableCompacted() {
// All holes have been removed so index is now same as count.
set_index(count());
}
// Called by the underlying [table] when it is cleared.
void TableCleared() {
set_index(Smi::FromInt(0));
set_count(Smi::FromInt(0));
}
// Removes the iterator from the double linked list and removes its reference
// back to the [table].
void Close();
// Returns an iterator result object: {value: any, done: boolean} and moves // Returns an iterator result object: {value: any, done: boolean} and moves
// the index to the next valid entry. Closes the iterator if moving past the // the index to the next valid entry. Closes the iterator if moving past the
// end. // end.
...@@ -10035,16 +10033,9 @@ class OrderedHashTableIterator: public JSObject { ...@@ -10035,16 +10033,9 @@ class OrderedHashTableIterator: public JSObject {
Handle<Map> map, Handle<TableType> table, int kind); Handle<Map> map, Handle<TableType> table, int kind);
private: private:
// Ensures [index] is not pointing to a hole. // Transitions the iterator to the non obsolote backing store. This is a NOP
void Seek(); // if the [table] is not obsolete.
void Transition();
// Moves [index] to next valid entry. Closes the iterator if moving past the
// end.
void MoveNext();
bool Closed() {
return table()->IsUndefined();
}
DISALLOW_IMPLICIT_CONSTRUCTORS(OrderedHashTableIterator); DISALLOW_IMPLICIT_CONSTRUCTORS(OrderedHashTableIterator);
}; };
...@@ -10066,7 +10057,8 @@ class JSSetIterator: public OrderedHashTableIterator<JSSetIterator, ...@@ -10066,7 +10057,8 @@ class JSSetIterator: public OrderedHashTableIterator<JSSetIterator,
static inline JSSetIterator* cast(Object* obj); static inline JSSetIterator* cast(Object* obj);
static Handle<Object> ValueForKind( static Handle<Object> ValueForKind(
Handle<JSSetIterator> iterator, int entry_index); Handle<JSSetIterator> iterator,
int entry_index);
private: private:
DISALLOW_IMPLICIT_CONSTRUCTORS(JSSetIterator); DISALLOW_IMPLICIT_CONSTRUCTORS(JSSetIterator);
...@@ -10089,7 +10081,8 @@ class JSMapIterator: public OrderedHashTableIterator<JSMapIterator, ...@@ -10089,7 +10081,8 @@ class JSMapIterator: public OrderedHashTableIterator<JSMapIterator,
static inline JSMapIterator* cast(Object* obj); static inline JSMapIterator* cast(Object* obj);
static Handle<Object> ValueForKind( static Handle<Object> ValueForKind(
Handle<JSMapIterator> iterator, int entry_index); Handle<JSMapIterator> iterator,
int entry_index);
private: private:
DISALLOW_IMPLICIT_CONSTRUCTORS(JSMapIterator); DISALLOW_IMPLICIT_CONSTRUCTORS(JSMapIterator);
......
...@@ -1600,15 +1600,6 @@ RUNTIME_FUNCTION(Runtime_SetIteratorNext) { ...@@ -1600,15 +1600,6 @@ RUNTIME_FUNCTION(Runtime_SetIteratorNext) {
} }
RUNTIME_FUNCTION(Runtime_SetIteratorClose) {
HandleScope scope(isolate);
ASSERT(args.length() == 1);
CONVERT_ARG_HANDLE_CHECKED(JSSetIterator, holder, 0);
holder->Close();
return isolate->heap()->undefined_value();
}
RUNTIME_FUNCTION(Runtime_MapInitialize) { RUNTIME_FUNCTION(Runtime_MapInitialize) {
HandleScope scope(isolate); HandleScope scope(isolate);
ASSERT(args.length() == 1); ASSERT(args.length() == 1);
...@@ -1709,15 +1700,6 @@ RUNTIME_FUNCTION(Runtime_MapIteratorNext) { ...@@ -1709,15 +1700,6 @@ RUNTIME_FUNCTION(Runtime_MapIteratorNext) {
} }
RUNTIME_FUNCTION(Runtime_MapIteratorClose) {
HandleScope scope(isolate);
ASSERT(args.length() == 1);
CONVERT_ARG_HANDLE_CHECKED(JSMapIterator, holder, 0);
holder->Close();
return isolate->heap()->undefined_value();
}
static Handle<JSWeakCollection> WeakCollectionInitialize( static Handle<JSWeakCollection> WeakCollectionInitialize(
Isolate* isolate, Isolate* isolate,
Handle<JSWeakCollection> weak_collection) { Handle<JSWeakCollection> weak_collection) {
......
...@@ -276,7 +276,6 @@ namespace internal { ...@@ -276,7 +276,6 @@ namespace internal {
F(SetCreateIterator, 2, 1) \ F(SetCreateIterator, 2, 1) \
\ \
F(SetIteratorNext, 1, 1) \ F(SetIteratorNext, 1, 1) \
F(SetIteratorClose, 1, 1) \
\ \
/* Harmony maps */ \ /* Harmony maps */ \
F(MapInitialize, 1, 1) \ F(MapInitialize, 1, 1) \
...@@ -289,7 +288,6 @@ namespace internal { ...@@ -289,7 +288,6 @@ namespace internal {
F(MapCreateIterator, 2, 1) \ F(MapCreateIterator, 2, 1) \
\ \
F(MapIteratorNext, 1, 1) \ F(MapIteratorNext, 1, 1) \
F(MapIteratorClose, 1, 1) \
\ \
/* Harmony weak maps and sets */ \ /* Harmony weak maps and sets */ \
F(WeakCollectionInitialize, 1, 1) \ F(WeakCollectionInitialize, 1, 1) \
......
...@@ -852,3 +852,138 @@ for (var i = 9; i >= 0; i--) { ...@@ -852,3 +852,138 @@ for (var i = 9; i >= 0; i--) {
}); });
assertEquals(4950, accumulated); assertEquals(4950, accumulated);
})(); })();
(function TestMapForEachAllRemovedTransition() {
var map = new Map;
map.set(0, 0);
var buffer = [];
map.forEach(function(v) {
buffer.push(v);
if (v === 0) {
for (var i = 1; i < 4; i++) {
map.set(i, i);
}
}
if (v === 3) {
for (var i = 0; i < 4; i++) {
map.delete(i);
}
for (var i = 4; i < 8; i++) {
map.set(i, i);
}
}
});
assertArrayEquals([0, 1, 2, 3, 4, 5, 6, 7], buffer);
})();
(function TestMapForEachClearTransition() {
var map = new Map;
map.set(0, 0);
var i = 0;
var buffer = [];
map.forEach(function(v) {
buffer.push(v);
if (++i < 5) {
for (var j = 0; j < 5; j++) {
map.clear();
map.set(i, i);
}
}
});
assertArrayEquals([0, 1, 2, 3, 4], buffer);
})();
(function TestMapForEachNestedNonTrivialTransition() {
var map = new Map;
map.set(0, 0);
map.set(1, 1);
map.set(2, 2);
map.set(3, 3);
map.delete(0);
var i = 0;
var buffer = [];
map.forEach(function(v) {
if (++i > 10) return;
buffer.push(v);
if (v == 3) {
map.delete(1);
for (var j = 4; j < 10; j++) {
map.set(j, j);
}
for (var j = 4; j < 10; j += 2) {
map.delete(j);
}
map.delete(2);
for (var j = 10; j < 20; j++) {
map.set(j, j);
}
for (var j = 10; j < 20; j += 2) {
map.delete(j);
}
map.delete(3);
}
});
assertArrayEquals([1, 2, 3, 5, 7, 9, 11, 13, 15, 17], buffer);
})();
(function TestMapForEachAllRemovedTransitionNoClear() {
var map = new Map;
map.set(0, 0);
var buffer = [];
map.forEach(function(v) {
buffer.push(v);
if (v === 0) {
for (var i = 1; i < 8; i++) {
map.set(i, i);
}
}
if (v === 4) {
for (var i = 0; i < 8; i++) {
map.delete(i);
}
}
});
assertArrayEquals([0, 1, 2, 3, 4], buffer);
})();
(function TestMapForEachNoMoreElementsAfterTransition() {
var map = new Map;
map.set(0, 0);
var buffer = [];
map.forEach(function(v) {
buffer.push(v);
if (v === 0) {
for (var i = 1; i < 16; i++) {
map.set(i, i);
}
}
if (v === 4) {
for (var i = 5; i < 16; i++) {
map.delete(i);
}
}
});
assertArrayEquals([0, 1, 2, 3, 4], buffer);
})();
// Copyright 2014 the V8 project authors. All rights reserved.
// AUTO-GENERATED BY tools/generate-runtime-tests.py, DO NOT MODIFY
// Flags: --allow-natives-syntax --harmony
var _holder = %MapCreateIterator(new Map(), 3);
%MapIteratorClose(_holder);
// Copyright 2014 the V8 project authors. All rights reserved.
// AUTO-GENERATED BY tools/generate-runtime-tests.py, DO NOT MODIFY
// Flags: --allow-natives-syntax --harmony
var _holder = %SetCreateIterator(new Set(), 2);
%SetIteratorClose(_holder);
...@@ -47,8 +47,8 @@ EXPAND_MACROS = [ ...@@ -47,8 +47,8 @@ EXPAND_MACROS = [
# that the parser doesn't bit-rot. Change the values as needed when you add, # that the parser doesn't bit-rot. Change the values as needed when you add,
# remove or change runtime functions, but make sure we don't lose our ability # remove or change runtime functions, but make sure we don't lose our ability
# to parse them! # to parse them!
EXPECTED_FUNCTION_COUNT = 361 EXPECTED_FUNCTION_COUNT = 359
EXPECTED_FUZZABLE_COUNT = 328 EXPECTED_FUZZABLE_COUNT = 326
EXPECTED_CCTEST_COUNT = 6 EXPECTED_CCTEST_COUNT = 6
EXPECTED_UNKNOWN_COUNT = 5 EXPECTED_UNKNOWN_COUNT = 5
EXPECTED_BUILTINS_COUNT = 824 EXPECTED_BUILTINS_COUNT = 824
......
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