Commit ceb9c812 authored by Leszek Swirski's avatar Leszek Swirski Committed by Commit Bot

[sfi] Remove SFI function literal id field

SharedFunctionInfos store their original function literal's id. This is
also their index in the Script's SFI list.

Since the function literal id is only needed for lazy compilation and live
edit, we can calculate it on-the-fly by linear search in the Script SFI list,
and save a field on the SFI.

If this regresses compile performance, we could alternatively store the
function literal id on the preparsed scope data as future work.

Bug: chromium:818642
Change-Id: I5468cea0e115921f1c864d94e567d749a4349882
Reviewed-on: https://chromium-review.googlesource.com/1082480
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: 's avatarHannes Payer <hpayer@chromium.org>
Reviewed-by: 's avatarToon Verwaest <verwaest@chromium.org>
Cr-Commit-Position: refs/heads/master@{#53523}
parent e3190c21
......@@ -620,10 +620,9 @@ Handle<JSFunction> Genesis::CreateEmptyFunction() {
empty_function->shared()->set_raw_start_position(0);
empty_function->shared()->set_raw_end_position(source->length());
empty_function->shared()->set_scope_info(*scope_info);
empty_function->shared()->set_function_literal_id(1);
empty_function->shared()->DontAdaptArguments();
SharedFunctionInfo::SetScript(handle(empty_function->shared(), isolate()),
script);
script, 1);
return empty_function;
}
......
......@@ -197,7 +197,7 @@ void UnoptimizedCompileJob::PrepareOnMainThread(Isolate* isolate) {
parse_info_->set_end_position(shared_->EndPosition());
parse_info_->set_unicode_cache(unicode_cache_.get());
parse_info_->set_language_mode(shared_->language_mode());
parse_info_->set_function_literal_id(shared_->function_literal_id());
parse_info_->set_function_literal_id(shared_->GetFunctionLiteralId(isolate));
if (V8_UNLIKELY(FLAG_runtime_stats)) {
parse_info_->set_runtime_call_stats(new (parse_info_->zone())
RuntimeCallStats());
......
......@@ -863,12 +863,14 @@ void LiveEdit::ReplaceFunctionCode(
}
void LiveEdit::FunctionSourceUpdated(Handle<JSArray> shared_info_array,
Handle<Script> script,
int new_function_literal_id) {
SharedInfoWrapper shared_info_wrapper(shared_info_array);
Handle<SharedFunctionInfo> shared_info = shared_info_wrapper.GetInfo();
shared_info->set_function_literal_id(new_function_literal_id);
shared_info_array->GetIsolate()->debug()->DeoptimizeFunction(shared_info);
SharedFunctionInfo::SetScript(shared_info, script, new_function_literal_id);
}
void LiveEdit::FixupScript(Handle<Script> script, int max_function_literal_id) {
......@@ -888,17 +890,20 @@ void LiveEdit::FixupScript(Handle<Script> script, int max_function_literal_id) {
isolate->heap()->SetRootNoScriptSharedFunctionInfos(*new_noscript_list);
// Put the SharedFunctionInfo at its new, correct location.
SharedFunctionInfo::SetScript(info, script);
SharedFunctionInfo::SetScript(info, script, iterator.CurrentIndex());
}
}
void LiveEdit::SetFunctionScript(Handle<JSValue> function_wrapper,
Handle<Object> script_handle) {
Handle<Object> script_handle,
int function_literal_id) {
Handle<SharedFunctionInfo> shared_info =
UnwrapSharedFunctionInfoFromJSValue(function_wrapper);
Isolate* isolate = function_wrapper->GetIsolate();
CHECK(script_handle->IsScript() || script_handle->IsUndefined(isolate));
SharedFunctionInfo::SetScript(shared_info, script_handle);
CHECK_IMPLIES(script_handle->IsScript(), function_literal_id >= 0);
SharedFunctionInfo::SetScript(shared_info, script_handle,
function_literal_id);
shared_info->DisableOptimization(BailoutReason::kLiveEdit);
function_wrapper->GetIsolate()->compilation_cache()->Remove(shared_info);
......
......@@ -85,11 +85,13 @@ class LiveEdit : AllStatic {
static void FixupScript(Handle<Script> script, int max_function_literal_id);
static void FunctionSourceUpdated(Handle<JSArray> shared_info_array,
Handle<Script> script,
int new_function_literal_id);
// Updates script field in FunctionSharedInfo.
static void SetFunctionScript(Handle<JSValue> function_wrapper,
Handle<Object> script_handle);
Handle<Object> script_handle,
int function_literal_id);
static void PatchFunctionPositions(Handle<JSArray> shared_info_array,
Handle<JSArray> position_change_array);
......
......@@ -207,6 +207,8 @@
var position_patch_report = new GlobalArray();
change_log.push( {position_patched: position_patch_report} );
%LiveEditResizeScriptFunctionArray(script, max_function_literal_id);
for (var i = 0; i < update_positions_list.length; i++) {
// TODO(LiveEdit): take into account whether it's source_changed or
// unchanged and whether positions changed at all.
......@@ -220,17 +222,16 @@
update_positions_list[i].live_shared_function_infos.forEach(function(
info) {
%LiveEditFunctionSourceUpdated(
info.raw_array, new_function_literal_id);
info.raw_array, script, new_function_literal_id);
});
}
}
%LiveEditFixupScript(script, max_function_literal_id);
// Link all the functions we're going to use to an actual script.
for (var i = 0; i < link_to_original_script_list.length; i++) {
%LiveEditFunctionSetScript(
link_to_original_script_list[i].info.shared_function_info, script);
link_to_original_script_list[i].info.shared_function_info, script,
link_to_original_script_list[i].info.function_literal_id);
}
preview_description.updated = true;
......@@ -260,7 +261,8 @@
// LiveEdit itself believe that any function in heap that points to a
// particular script is a regular function.
// For some functions we will restore this link later.
%LiveEditFunctionSetScript(info.shared_function_info, UNDEFINED);
%LiveEditFunctionSetScript(info.shared_function_info, UNDEFINED,
info.function_literal_id);
compile_info.push(info);
old_index_map.push(i);
}
......@@ -359,9 +361,12 @@
// may access its own text.
function LinkToOldScript(old_info_node, old_script, report_array) {
if (old_info_node.live_shared_function_infos) {
// TODO(leszeks): Passing "null" here as a marker that we should calculate
// the SFI's function literal id is ugly, it'd be better if this
// information was e.g. already on the info object.
old_info_node.live_shared_function_infos.
forEach(function (info) {
%LiveEditFunctionSetScript(info.info, old_script);
%LiveEditFunctionSetScript(info.info, old_script, null);
});
report_array.push( { name: old_info_node.info.function_name } );
......
......@@ -3313,7 +3313,8 @@ Handle<SharedFunctionInfo> Factory::NewSharedFunctionInfoForLiteral(
Handle<SharedFunctionInfo> shared = NewSharedFunctionInfoForBuiltin(
literal->name(), Builtins::kCompileLazy, kind);
SharedFunctionInfo::InitFromFunctionLiteral(shared, literal, is_toplevel);
SharedFunctionInfo::SetScript(shared, script, false);
SharedFunctionInfo::SetScript(shared, script, literal->function_literal_id(),
false);
return shared;
}
......@@ -3401,7 +3402,6 @@ Handle<SharedFunctionInfo> Factory::NewSharedFunctionInfo(
share->set_script(*undefined_value(), SKIP_WRITE_BARRIER);
share->set_debug_info(Smi::kZero, SKIP_WRITE_BARRIER);
share->set_function_identifier(*undefined_value(), SKIP_WRITE_BARRIER);
share->set_function_literal_id(FunctionLiteral::kIdTypeInvalid);
#if V8_SFI_HAS_UNIQUE_ID
share->set_unique_id(isolate()->GetNextUniqueSharedFunctionInfoId());
#endif
......
......@@ -12750,9 +12750,8 @@ void Map::SetPrototype(Handle<Map> map, Handle<Object> prototype,
map->set_prototype(*prototype, wb_mode);
}
Handle<Object> CacheInitialJSArrayMaps(
Handle<Context> native_context, Handle<Map> initial_map) {
Handle<Object> CacheInitialJSArrayMaps(Handle<Context> native_context,
Handle<Map> initial_map) {
// Replace all of the cached initial array maps in the native context with
// the appropriate transitioned elements kind maps.
Handle<Map> current_map = initial_map;
......@@ -12766,8 +12765,8 @@ Handle<Object> CacheInitialJSArrayMaps(
if (Map* maybe_elements_transition = current_map->ElementsTransitionMap()) {
new_map = handle(maybe_elements_transition);
} else {
new_map = Map::CopyAsElementsKind(
current_map, next_kind, INSERT_TRANSITION);
new_map =
Map::CopyAsElementsKind(current_map, next_kind, INSERT_TRANSITION);
}
DCHECK_EQ(next_kind, new_map->elements_kind());
native_context->set(Context::ArrayMapIndex(next_kind), *new_map);
......@@ -12869,7 +12868,6 @@ void JSFunction::SetPrototype(Handle<JSFunction> function,
SetInstancePrototype(isolate, function, construct_prototype);
}
void JSFunction::SetInitialMap(Handle<JSFunction> function, Handle<Map> map,
Handle<Object> prototype) {
if (map->prototype() != *prototype) Map::SetPrototype(map, prototype);
......@@ -13595,8 +13593,8 @@ SharedFunctionInfo* SharedFunctionInfo::GlobalIterator::Next() {
void SharedFunctionInfo::SetScript(Handle<SharedFunctionInfo> shared,
Handle<Object> script_object,
int function_literal_id,
bool reset_preparsed_scope_data) {
DCHECK_NE(shared->function_literal_id(), FunctionLiteral::kIdTypeInvalid);
if (shared->script() == *script_object) return;
Isolate* isolate = shared->GetIsolate();
......@@ -13613,15 +13611,14 @@ void SharedFunctionInfo::SetScript(Handle<SharedFunctionInfo> shared,
Handle<WeakFixedArray> list =
handle(script->shared_function_infos(), isolate);
#ifdef DEBUG
DCHECK_LT(shared->function_literal_id(), list->length());
MaybeObject* maybe_object = list->Get(shared->function_literal_id());
DCHECK_LT(function_literal_id, list->length());
MaybeObject* maybe_object = list->Get(function_literal_id);
HeapObject* heap_object;
if (maybe_object->ToWeakHeapObject(&heap_object)) {
DCHECK_EQ(heap_object, *shared);
}
#endif
list->Set(shared->function_literal_id(),
HeapObjectReference::Weak(*shared));
list->Set(function_literal_id, HeapObjectReference::Weak(*shared));
} else {
Handle<Object> list = isolate->factory()->noscript_shared_function_infos();
......@@ -13647,13 +13644,13 @@ void SharedFunctionInfo::SetScript(Handle<SharedFunctionInfo> shared,
// Due to liveedit, it might happen that the old_script doesn't know
// about the SharedFunctionInfo, so we have to guard against that.
Handle<WeakFixedArray> infos(old_script->shared_function_infos(), isolate);
if (shared->function_literal_id() < infos->length()) {
MaybeObject* raw = old_script->shared_function_infos()->Get(
shared->function_literal_id());
if (function_literal_id < infos->length()) {
MaybeObject* raw =
old_script->shared_function_infos()->Get(function_literal_id);
HeapObject* heap_object;
if (raw->ToWeakHeapObject(&heap_object) && heap_object == *shared) {
old_script->shared_function_infos()->Set(
shared->function_literal_id(),
function_literal_id,
HeapObjectReference::Strong(isolate->heap()->undefined_value()));
}
}
......@@ -13794,6 +13791,29 @@ bool SharedFunctionInfo::IsInlineable() {
int SharedFunctionInfo::SourceSize() { return EndPosition() - StartPosition(); }
int SharedFunctionInfo::GetFunctionLiteralId(Isolate* isolate) const {
Object* script_obj = script();
if (!script_obj->IsScript()) return FunctionLiteral::kIdTypeInvalid;
WeakFixedArray* shared_info_list =
Script::cast(script_obj)->shared_function_infos();
SharedFunctionInfo::ScriptIterator iterator(
isolate, Handle<WeakFixedArray>(&shared_info_list));
// TODO(leszeks): If this turns out to be a performance bottleneck when
// initialising the ParseInfo for lazy compilation, we can probably change it
// to either a binary search using StartPosition, or cache it on the
// PreParseScopeData.
for (SharedFunctionInfo* shared = iterator.Next(); shared != nullptr;
shared = iterator.Next()) {
if (shared == this) {
return iterator.CurrentIndex();
}
}
return FunctionLiteral::kIdTypeInvalid;
}
void JSFunction::CalculateInstanceSizeHelper(InstanceType instance_type,
bool has_prototype_slot,
int requested_embedder_fields,
......@@ -13933,7 +13953,6 @@ void SharedFunctionInfo::InitFromFunctionLiteral(
// FunctionKind must have already been set.
DCHECK(lit->kind() == shared_info->kind());
shared_info->set_needs_home_object(lit->scope()->NeedsHomeObject());
shared_info->set_function_literal_id(lit->function_literal_id());
DCHECK_IMPLIES(lit->requires_instance_fields_initializer(),
IsClassConstructor(lit->kind()));
shared_info->set_requires_instance_fields_initializer(
......
......@@ -43,7 +43,6 @@ BIT_FIELD_ACCESSORS(SharedFunctionInfo, raw_start_position_and_type,
BIT_FIELD_ACCESSORS(SharedFunctionInfo, raw_start_position_and_type,
is_toplevel, SharedFunctionInfo::IsTopLevelBit)
INT_ACCESSORS(SharedFunctionInfo, function_literal_id, kFunctionLiteralIdOffset)
#if V8_SFI_HAS_UNIQUE_ID
INT_ACCESSORS(SharedFunctionInfo, unique_id, kUniqueIdOffset)
#endif
......
......@@ -83,7 +83,7 @@ class SharedFunctionInfo : public HeapObject {
// function info is added to the list on the script.
V8_EXPORT_PRIVATE static void SetScript(
Handle<SharedFunctionInfo> shared, Handle<Object> script_object,
bool reset_preparsed_scope_data = true);
int function_literal_id, bool reset_preparsed_scope_data = true);
// Layout description of the optimized code map.
static const int kEntriesStart = 0;
......@@ -149,11 +149,6 @@ class SharedFunctionInfo : public HeapObject {
// function. The value is only reliable when the function has been compiled.
DECL_INT_ACCESSORS(expected_nof_properties)
// [function_literal_id] - uniquely identifies the FunctionLiteral this
// SharedFunctionInfo represents within its script, or -1 if this
// SharedFunctionInfo object doesn't correspond to a parsed FunctionLiteral.
DECL_INT_ACCESSORS(function_literal_id)
#if V8_SFI_HAS_UNIQUE_ID
// [unique_id] - For --trace-maps purposes, an identifier that's persistent
// even if the GC moves this SharedFunctionInfo.
......@@ -222,6 +217,9 @@ class SharedFunctionInfo : public HeapObject {
// [script]: Script from which the function originates.
DECL_ACCESSORS(script, Object)
// Get the function literal id associated with this function, for parsing.
int GetFunctionLiteralId(Isolate* isolate) const;
// The function is subject to debugging if a debug info is attached.
inline bool HasDebugInfo() const;
DebugInfo* GetDebugInfo() const;
......@@ -438,6 +436,7 @@ class SharedFunctionInfo : public HeapObject {
ScriptIterator(Isolate* isolate,
Handle<WeakFixedArray> shared_function_infos);
SharedFunctionInfo* Next();
int CurrentIndex() const { return index_ - 1; }
// Reset the iterator to run on |script|.
void Reset(Handle<Script> script);
......@@ -487,7 +486,6 @@ class SharedFunctionInfo : public HeapObject {
V(kFunctionIdentifierOffset, kPointerSize) \
V(kEndOfPointerFieldsOffset, 0) \
/* Raw data fields. */ \
V(kFunctionLiteralIdOffset, kInt32Size) \
V(kUniqueIdOffset, kUniqueIdFieldSize) \
V(kLengthOffset, kUInt16Size) \
V(kFormalParameterCountOffset, kUInt16Size) \
......
......@@ -54,7 +54,7 @@ ParseInfo::ParseInfo(Isolate* isolate, Handle<SharedFunctionInfo> shared)
set_function_flags(shared->flags());
set_start_position(shared->StartPosition());
set_end_position(shared->EndPosition());
function_literal_id_ = shared->function_literal_id();
function_literal_id_ = shared->GetFunctionLiteralId(isolate);
set_language_mode(shared->language_mode());
set_asm_wasm_broken(shared->is_asm_wasm_broken());
......@@ -105,42 +105,6 @@ ParseInfo::ParseInfo(Isolate* isolate, Handle<Script> script)
ParseInfo::~ParseInfo() {}
// static
ParseInfo* ParseInfo::AllocateWithoutScript(Isolate* isolate,
Handle<SharedFunctionInfo> shared) {
ParseInfo* p = new ParseInfo(isolate->allocator());
p->InitFromIsolate(isolate);
p->set_toplevel(shared->is_toplevel());
p->set_allow_lazy_parsing(FLAG_lazy_inner_functions);
p->set_is_named_expression(shared->is_named_expression());
p->set_function_flags(shared->flags());
p->set_start_position(shared->StartPosition());
p->set_end_position(shared->EndPosition());
p->function_literal_id_ = shared->function_literal_id();
p->set_language_mode(shared->language_mode());
// BUG(5946): This function exists as a workaround until we can
// get rid of %SetCode in our native functions. The ParseInfo
// is explicitly set up for the case that:
// a) you have a native built-in,
// b) it's being run for the 2nd-Nth time in an isolate,
// c) we've already compiled bytecode and therefore don't need
// to parse.
// We tolerate a ParseInfo without a Script in this case.
p->set_native(true);
p->set_eval(false);
p->set_module(false);
DCHECK_NE(shared->kind(), FunctionKind::kModule);
Handle<HeapObject> scope_info(shared->GetOuterScopeInfo(), isolate);
if (!scope_info->IsTheHole(isolate) &&
Handle<ScopeInfo>::cast(scope_info)->length() > 0) {
p->set_outer_scope_info(Handle<ScopeInfo>::cast(scope_info));
}
return p;
}
DeclarationScope* ParseInfo::scope() const { return literal()->scope(); }
bool ParseInfo::is_declaration() const {
......
......@@ -45,9 +45,6 @@ class V8_EXPORT_PRIVATE ParseInfo {
void InitFromIsolate(Isolate* isolate);
static ParseInfo* AllocateWithoutScript(Isolate* isolate,
Handle<SharedFunctionInfo> shared);
// Either returns the ast-value-factory associcated with this ParseInfo, or
// creates and returns a new factory if none exists.
AstValueFactory* GetOrCreateAstValueFactory();
......
......@@ -119,16 +119,17 @@ RUNTIME_FUNCTION(Runtime_SetCode) {
bool was_native = target_shared->native();
target_shared->set_flags(source_shared->flags());
target_shared->set_native(was_native);
target_shared->set_function_literal_id(source_shared->function_literal_id());
target_shared->set_scope_info(source_shared->scope_info());
Handle<Object> source_script(source_shared->script(), isolate);
int function_literal_id = source_shared->GetFunctionLiteralId(isolate);
if (source_script->IsScript()) {
SharedFunctionInfo::SetScript(source_shared,
isolate->factory()->undefined_value());
isolate->factory()->undefined_value(),
function_literal_id);
}
SharedFunctionInfo::SetScript(target_shared, source_script);
SharedFunctionInfo::SetScript(target_shared, source_script,
function_literal_id);
// Set the code of the target function.
target->set_code(source_shared->GetCode());
......
......@@ -103,7 +103,7 @@ RUNTIME_FUNCTION(Runtime_LiveEditReplaceScript) {
// Recreate the shared function infos array after changing the IDs of all
// SharedFunctionInfos.
RUNTIME_FUNCTION(Runtime_LiveEditFixupScript) {
RUNTIME_FUNCTION(Runtime_LiveEditResizeScriptFunctionArray) {
HandleScope scope(isolate);
CHECK(isolate->debug()->live_edit_enabled());
DCHECK_EQ(args.length(), 2);
......@@ -120,16 +120,19 @@ RUNTIME_FUNCTION(Runtime_LiveEditFixupScript) {
RUNTIME_FUNCTION(Runtime_LiveEditFunctionSourceUpdated) {
HandleScope scope(isolate);
CHECK(isolate->debug()->live_edit_enabled());
DCHECK_EQ(args.length(), 2);
DCHECK_EQ(args.length(), 3);
CONVERT_ARG_HANDLE_CHECKED(JSArray, shared_info, 0);
CONVERT_INT32_ARG_CHECKED(new_function_literal_id, 1);
CONVERT_ARG_HANDLE_CHECKED(JSValue, script_value, 1);
CONVERT_INT32_ARG_CHECKED(new_function_literal_id, 2);
CHECK(SharedInfoWrapper::IsInstance(shared_info));
LiveEdit::FunctionSourceUpdated(shared_info, new_function_literal_id);
CHECK(script_value->value()->IsScript());
Handle<Script> script(Script::cast(script_value->value()));
LiveEdit::FunctionSourceUpdated(shared_info, script, new_function_literal_id);
return isolate->heap()->undefined_value();
}
// Replaces code of SharedFunctionInfo with a new one.
RUNTIME_FUNCTION(Runtime_LiveEditReplaceFunctionCode) {
HandleScope scope(isolate);
......@@ -143,24 +146,39 @@ RUNTIME_FUNCTION(Runtime_LiveEditReplaceFunctionCode) {
return isolate->heap()->undefined_value();
}
// Connects SharedFunctionInfo to another script.
RUNTIME_FUNCTION(Runtime_LiveEditFunctionSetScript) {
HandleScope scope(isolate);
CHECK(isolate->debug()->live_edit_enabled());
DCHECK_EQ(2, args.length());
DCHECK_EQ(3, args.length());
CONVERT_ARG_HANDLE_CHECKED(Object, function_object, 0);
CONVERT_ARG_HANDLE_CHECKED(Object, script_object, 1);
CONVERT_ARG_HANDLE_CHECKED(Object, function_literal_id_arg, 2);
if (function_object->IsJSValue()) {
Handle<JSValue> function_wrapper = Handle<JSValue>::cast(function_object);
if (script_object->IsJSValue()) {
CHECK(JSValue::cast(*script_object)->value()->IsScript());
Script* script = Script::cast(JSValue::cast(*script_object)->value());
script_object = Handle<Object>(script, isolate);
}
CHECK(function_wrapper->value()->IsSharedFunctionInfo());
LiveEdit::SetFunctionScript(function_wrapper, script_object);
int32_t function_literal_id = -1;
SharedFunctionInfo* shared =
SharedFunctionInfo::cast(function_wrapper->value());
if (function_literal_id_arg->IsNull(isolate)) {
if (shared->script()->IsScript()) {
function_literal_id = shared->GetFunctionLiteralId(isolate);
}
} else {
CHECK(function_literal_id_arg->IsNumber());
function_literal_id_arg->ToInt32(&function_literal_id);
}
LiveEdit::SetFunctionScript(function_wrapper, script_object,
function_literal_id);
} else {
// Just ignore this. We may not have a SharedFunctionInfo for some functions
// and we check it in this function.
......
......@@ -291,9 +291,9 @@ namespace internal {
F(LiveEditCheckAndDropActivations, 3, 1) \
F(LiveEditCompareStrings, 2, 1) \
F(LiveEditFindSharedFunctionInfosForScript, 1, 1) \
F(LiveEditFixupScript, 2, 1) \
F(LiveEditFunctionSetScript, 2, 1) \
F(LiveEditFunctionSourceUpdated, 2, 1) \
F(LiveEditResizeScriptFunctionArray, 2, 1) \
F(LiveEditFunctionSetScript, 3, 1) \
F(LiveEditFunctionSourceUpdated, 3, 1) \
F(LiveEditGatherCompileInfo, 2, 1) \
F(LiveEditPatchFunctionPositions, 2, 1) \
F(LiveEditReplaceFunctionCode, 2, 1) \
......
......@@ -29,5 +29,7 @@ function listener(event, exec_state, event_data, data) {
Debug.setListener(listener);
f();
Debug.setListener(null);
assertNull(exception);
if (exception != null) {
throw exception;
}
assertEquals(6, counter);
......@@ -42,8 +42,7 @@ Handle<SharedFunctionInfo> CreateSharedFunctionInfo(
// Make sure we have an outer scope info, even though it's empty
shared->set_raw_outer_scope_info_or_feedback_metadata(
ScopeInfo::Empty(isolate));
shared->set_function_literal_id(1);
SharedFunctionInfo::SetScript(shared, script);
SharedFunctionInfo::SetScript(shared, script, 1);
return scope.CloseAndEscape(shared);
}
......
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