Commit 9fef1cd9 authored by Yedidya Feldblum's avatar Yedidya Feldblum Committed by Facebook Github Bot

Move expensive Singleton code to .cpp

Summary:
[Folly] Move expensive `Singleton` code to `.cpp`.

Including string manipulation, `ostream::operator<<` operations, `throw` statements, etc,

Reviewed By: ot

Differential Revision: D6632052

fbshipit-source-id: 82e0033d5042b02951cf3b05c55cf62c97cc2b6d
parent 48b9743d
...@@ -66,8 +66,7 @@ void SingletonHolder<T>::registerSingleton(CreateFunc c, TeardownFunc t) { ...@@ -66,8 +66,7 @@ void SingletonHolder<T>::registerSingleton(CreateFunc c, TeardownFunc t) {
template <typename T> template <typename T>
void SingletonHolder<T>::registerSingletonMock(CreateFunc c, TeardownFunc t) { void SingletonHolder<T>::registerSingletonMock(CreateFunc c, TeardownFunc t) {
if (state_ == SingletonHolderState::NotRegistered) { if (state_ == SingletonHolderState::NotRegistered) {
LOG(FATAL) << "Registering mock before singleton was registered: " detail::singletonWarnRegisterMockEarlyAndAbort(type());
<< type().name();
} }
if (state_ == SingletonHolderState::Living) { if (state_ == SingletonHolderState::Living) {
destroyInstance(); destroyInstance();
...@@ -97,10 +96,7 @@ T* SingletonHolder<T>::get() { ...@@ -97,10 +96,7 @@ T* SingletonHolder<T>::get() {
createInstance(); createInstance();
if (instance_weak_.expired()) { if (instance_weak_.expired()) {
throw std::runtime_error( detail::singletonThrowGetInvokedAfterDestruction(type());
"Raw pointer to a singleton requested after its destruction."
" Singleton type is: " +
type().name());
} }
return instance_ptr_; return instance_ptr_;
...@@ -161,14 +157,7 @@ void SingletonHolder<T>::destroyInstance() { ...@@ -161,14 +157,7 @@ void SingletonHolder<T>::destroyInstance() {
teardown_(instance_ptr_); teardown_(instance_ptr_);
} else { } else {
print_destructor_stack_trace_->store(true); print_destructor_stack_trace_->store(true);
LOG(ERROR) << "Singleton of type " << type().name() << " has a " detail::singletonWarnDestroyInstanceLeak(type(), instance_ptr_);
<< "living reference at destroyInstances time; beware! Raw "
<< "pointer is " << instance_ptr_ << ". It is very likely "
<< "that some other singleton is holding a shared_ptr to it. "
<< "This singleton will be leaked (even if a shared_ptr to it "
<< "is eventually released)."
<< "Make sure dependencies between these singletons are "
<< "properly defined.";
} }
} }
} }
...@@ -199,7 +188,7 @@ template <typename T> ...@@ -199,7 +188,7 @@ template <typename T>
void SingletonHolder<T>::createInstance() { void SingletonHolder<T>::createInstance() {
if (creating_thread_.load(std::memory_order_acquire) == if (creating_thread_.load(std::memory_order_acquire) ==
std::this_thread::get_id()) { std::this_thread::get_id()) {
LOG(FATAL) << "circular singleton dependency: " << type().name(); detail::singletonWarnCreateCircularDependencyAndAbort(type());
} }
std::lock_guard<std::mutex> entry_lock(mutex_); std::lock_guard<std::mutex> entry_lock(mutex_);
...@@ -208,12 +197,7 @@ void SingletonHolder<T>::createInstance() { ...@@ -208,12 +197,7 @@ void SingletonHolder<T>::createInstance() {
} }
if (state_.load(std::memory_order_acquire) == if (state_.load(std::memory_order_acquire) ==
SingletonHolderState::NotRegistered) { SingletonHolderState::NotRegistered) {
auto ptr = SingletonVault::stackTraceGetter().load(); detail::singletonWarnCreateUnregisteredAndAbort(type());
LOG(FATAL) << "Creating instance for unregistered singleton: "
<< type().name() << "\n"
<< "Stacktrace:"
<< "\n"
<< (ptr ? (*ptr)() : "(not available)");
} }
if (state_.load(std::memory_order_acquire) == SingletonHolderState::Living) { if (state_.load(std::memory_order_acquire) == SingletonHolderState::Living) {
...@@ -232,20 +216,9 @@ void SingletonHolder<T>::createInstance() { ...@@ -232,20 +216,9 @@ void SingletonHolder<T>::createInstance() {
auto state = vault_.state_.rlock(); auto state = vault_.state_.rlock();
if (vault_.type_ != SingletonVault::Type::Relaxed && if (vault_.type_ != SingletonVault::Type::Relaxed &&
!state->registrationComplete) { !state->registrationComplete) {
auto stack_trace_getter = SingletonVault::stackTraceGetter().load(); detail::singletonWarnCreateBeforeRegistrationCompleteAndAbort(type());
auto stack_trace = stack_trace_getter ? stack_trace_getter() : "";
if (!stack_trace.empty()) {
stack_trace = "Stack trace:\n" + stack_trace;
}
LOG(FATAL) << "Singleton " << type().name() << " requested before "
<< "registrationComplete() call.\n"
<< "This usually means that either main() never called "
<< "folly::init, or singleton was requested before main() "
<< "(which is not allowed).\n"
<< stack_trace;
} }
if (state->state == SingletonVault::SingletonVaultState::Quiescing) { if (state->state == detail::SingletonVaultState::Type::Quiescing) {
return; return;
} }
...@@ -260,18 +233,7 @@ void SingletonHolder<T>::createInstance() { ...@@ -260,18 +233,7 @@ void SingletonHolder<T>::createInstance() {
T*) mutable { T*) mutable {
destroy_baton->post(); destroy_baton->post();
if (print_destructor_stack_trace->load()) { if (print_destructor_stack_trace->load()) {
std::string output = "Singleton " + type.name() + " was released.\n"; detail::singletonPrintDestructionStackTrace(type);
auto stack_trace_getter = SingletonVault::stackTraceGetter().load();
auto stack_trace = stack_trace_getter ? stack_trace_getter() : "";
if (stack_trace.empty()) {
output += "Failed to get release stack trace.";
} else {
output += "Release stack trace:\n";
output += stack_trace;
}
LOG(ERROR) << output;
} }
}); });
......
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
#include <iostream> #include <iostream>
#include <string> #include <string>
#include <folly/Demangle.h>
#include <folly/Format.h>
#include <folly/ScopeGuard.h> #include <folly/ScopeGuard.h>
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) #if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__)
...@@ -57,6 +59,15 @@ SingletonVault::Type SingletonVault::defaultVaultType() { ...@@ -57,6 +59,15 @@ SingletonVault::Type SingletonVault::defaultVaultType() {
namespace detail { namespace detail {
std::string TypeDescriptor::name() const {
auto ret = demangle(ti_.name());
if (tag_ti_ != std::type_index(typeid(DefaultTag))) {
ret += "/";
ret += demangle(tag_ti_.name());
}
return ret.toStdString();
}
[[noreturn]] void singletonWarnDoubleRegistrationAndAbort( [[noreturn]] void singletonWarnDoubleRegistrationAndAbort(
const TypeDescriptor& type) { const TypeDescriptor& type) {
// Ensure the availability of std::cerr // Ensure the availability of std::cerr
...@@ -67,6 +78,111 @@ namespace detail { ...@@ -67,6 +78,111 @@ namespace detail {
<< type.name() << ">\n"; << type.name() << ">\n";
std::abort(); std::abort();
} }
[[noreturn]] void singletonWarnLeakyDoubleRegistrationAndAbort(
const TypeDescriptor& type) {
// Ensure the availability of std::cerr
std::ios_base::Init ioInit;
std::cerr << "Double registration of singletons of the same "
"underlying type; check for multiple definitions "
"of type folly::LeakySingleton<"
<< type.name() << ">\n";
std::abort();
}
[[noreturn]] void singletonWarnLeakyInstantiatingNotRegisteredAndAbort(
const TypeDescriptor& type) {
auto ptr = SingletonVault::stackTraceGetter().load();
LOG(FATAL) << "Creating instance for unregistered singleton: "
<< type.name() << "\n"
<< "Stacktrace:"
<< "\n" << (ptr ? (*ptr)() : "(not available)");
}
[[noreturn]] void singletonWarnRegisterMockEarlyAndAbort(
const TypeDescriptor& type) {
LOG(FATAL) << "Registering mock before singleton was registered: "
<< type.name();
}
void singletonWarnDestroyInstanceLeak(
const TypeDescriptor& type,
const void* ptr) {
LOG(ERROR) << "Singleton of type " << type.name() << " has a "
<< "living reference at destroyInstances time; beware! Raw "
<< "pointer is " << ptr << ". It is very likely "
<< "that some other singleton is holding a shared_ptr to it. "
<< "This singleton will be leaked (even if a shared_ptr to it "
<< "is eventually released)."
<< "Make sure dependencies between these singletons are "
<< "properly defined.";
}
[[noreturn]] void singletonWarnCreateCircularDependencyAndAbort(
const TypeDescriptor& type) {
LOG(FATAL) << "circular singleton dependency: " << type.name();
}
[[noreturn]] void singletonWarnCreateUnregisteredAndAbort(
const TypeDescriptor& type) {
auto ptr = SingletonVault::stackTraceGetter().load();
LOG(FATAL) << "Creating instance for unregistered singleton: "
<< type.name() << "\n"
<< "Stacktrace:"
<< "\n"
<< (ptr ? (*ptr)() : "(not available)");
}
[[noreturn]] void singletonWarnCreateBeforeRegistrationCompleteAndAbort(
const TypeDescriptor& type) {
auto stack_trace_getter = SingletonVault::stackTraceGetter().load();
auto stack_trace = stack_trace_getter ? stack_trace_getter() : "";
if (!stack_trace.empty()) {
stack_trace = "Stack trace:\n" + stack_trace;
}
LOG(FATAL) << "Singleton " << type.name() << " requested before "
<< "registrationComplete() call.\n"
<< "This usually means that either main() never called "
<< "folly::init, or singleton was requested before main() "
<< "(which is not allowed).\n"
<< stack_trace;
}
void singletonPrintDestructionStackTrace(const TypeDescriptor& type) {
std::string output = "Singleton " + type.name() + " was released.\n";
auto stack_trace_getter = SingletonVault::stackTraceGetter().load();
auto stack_trace = stack_trace_getter ? stack_trace_getter() : "";
if (stack_trace.empty()) {
output += "Failed to get release stack trace.";
} else {
output += "Release stack trace:\n";
output += stack_trace;
}
LOG(ERROR) << output;
}
[[noreturn]] void singletonThrowNullCreator(const std::type_info& type) {
auto const msg = sformat(
"nullptr_t should be passed if you want {} to be default constructed",
demangle(type));
throw std::logic_error(msg);
}
[[noreturn]] void singletonThrowGetInvokedAfterDestruction(
const TypeDescriptor& type) {
throw std::runtime_error(
"Raw pointer to a singleton requested after its destruction."
" Singleton type is: " +
type.name());
}
[[noreturn]] void SingletonVaultState::throwUnexpectedState(const char* msg) {
throw std::logic_error(msg);
}
} // namespace detail } // namespace detail
namespace { namespace {
...@@ -101,7 +217,7 @@ SingletonVault::~SingletonVault() { destroyInstances(); } ...@@ -101,7 +217,7 @@ SingletonVault::~SingletonVault() { destroyInstances(); }
void SingletonVault::registerSingleton(detail::SingletonHolderBase* entry) { void SingletonVault::registerSingleton(detail::SingletonHolderBase* entry) {
auto state = state_.rlock(); auto state = state_.rlock();
stateCheck(SingletonVaultState::Running, *state); state->check(detail::SingletonVaultState::Type::Running);
if (UNLIKELY(state->registrationComplete)) { if (UNLIKELY(state->registrationComplete)) {
LOG(ERROR) << "Registering singleton after registrationComplete()."; LOG(ERROR) << "Registering singleton after registrationComplete().";
...@@ -114,7 +230,7 @@ void SingletonVault::registerSingleton(detail::SingletonHolderBase* entry) { ...@@ -114,7 +230,7 @@ void SingletonVault::registerSingleton(detail::SingletonHolderBase* entry) {
void SingletonVault::addEagerInitSingleton(detail::SingletonHolderBase* entry) { void SingletonVault::addEagerInitSingleton(detail::SingletonHolderBase* entry) {
auto state = state_.rlock(); auto state = state_.rlock();
stateCheck(SingletonVaultState::Running, *state); state->check(detail::SingletonVaultState::Type::Running);
if (UNLIKELY(state->registrationComplete)) { if (UNLIKELY(state->registrationComplete)) {
LOG(ERROR) << "Registering for eager-load after registrationComplete()."; LOG(ERROR) << "Registering for eager-load after registrationComplete().";
...@@ -130,7 +246,7 @@ void SingletonVault::registrationComplete() { ...@@ -130,7 +246,7 @@ void SingletonVault::registrationComplete() {
std::atexit([](){ SingletonVault::singleton()->destroyInstances(); }); std::atexit([](){ SingletonVault::singleton()->destroyInstances(); });
auto state = state_.wlock(); auto state = state_.wlock();
stateCheck(SingletonVaultState::Running, *state); state->check(detail::SingletonVaultState::Type::Running);
if (state->registrationComplete) { if (state->registrationComplete) {
return; return;
...@@ -153,7 +269,7 @@ void SingletonVault::registrationComplete() { ...@@ -153,7 +269,7 @@ void SingletonVault::registrationComplete() {
void SingletonVault::doEagerInit() { void SingletonVault::doEagerInit() {
{ {
auto state = state_.rlock(); auto state = state_.rlock();
stateCheck(SingletonVaultState::Running, *state); state->check(detail::SingletonVaultState::Type::Running);
if (UNLIKELY(!state->registrationComplete)) { if (UNLIKELY(!state->registrationComplete)) {
throw std::logic_error("registrationComplete() not yet called"); throw std::logic_error("registrationComplete() not yet called");
} }
...@@ -168,7 +284,7 @@ void SingletonVault::doEagerInit() { ...@@ -168,7 +284,7 @@ void SingletonVault::doEagerInit() {
void SingletonVault::doEagerInitVia(Executor& exe, folly::Baton<>* done) { void SingletonVault::doEagerInitVia(Executor& exe, folly::Baton<>* done) {
{ {
auto state = state_.rlock(); auto state = state_.rlock();
stateCheck(SingletonVaultState::Running, *state); state->check(detail::SingletonVaultState::Type::Running);
if (UNLIKELY(!state->registrationComplete)) { if (UNLIKELY(!state->registrationComplete)) {
throw std::logic_error("registrationComplete() not yet called"); throw std::logic_error("registrationComplete() not yet called");
} }
...@@ -204,10 +320,10 @@ void SingletonVault::doEagerInitVia(Executor& exe, folly::Baton<>* done) { ...@@ -204,10 +320,10 @@ void SingletonVault::doEagerInitVia(Executor& exe, folly::Baton<>* done) {
void SingletonVault::destroyInstances() { void SingletonVault::destroyInstances() {
auto stateW = state_.wlock(); auto stateW = state_.wlock();
if (stateW->state == SingletonVaultState::Quiescing) { if (stateW->state == detail::SingletonVaultState::Type::Quiescing) {
return; return;
} }
stateW->state = SingletonVaultState::Quiescing; stateW->state = detail::SingletonVaultState::Type::Quiescing;
auto stateR = stateW.moveFromWriteToRead(); auto stateR = stateW.moveFromWriteToRead();
{ {
...@@ -249,9 +365,9 @@ void SingletonVault::destroyInstances() { ...@@ -249,9 +365,9 @@ void SingletonVault::destroyInstances() {
void SingletonVault::reenableInstances() { void SingletonVault::reenableInstances() {
auto state = state_.wlock(); auto state = state_.wlock();
stateCheck(SingletonVaultState::Quiescing, *state); state->check(detail::SingletonVaultState::Type::Quiescing);
state->state = SingletonVaultState::Running; state->state = detail::SingletonVaultState::Type::Running;
} }
void SingletonVault::scheduleDestroyInstances() { void SingletonVault::scheduleDestroyInstances() {
......
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
// should call reenableInstances. // should call reenableInstances.
#pragma once #pragma once
#include <folly/Demangle.h>
#include <folly/Exception.h> #include <folly/Exception.h>
#include <folly/Executor.h> #include <folly/Executor.h>
#include <folly/Memory.h> #include <folly/Memory.h>
...@@ -206,14 +206,7 @@ class TypeDescriptor { ...@@ -206,14 +206,7 @@ class TypeDescriptor {
return *this; return *this;
} }
std::string name() const { std::string name() const;
auto ret = demangle(ti_.name());
if (tag_ti_ != std::type_index(typeid(DefaultTag))) {
ret += "/";
ret += demangle(tag_ti_.name());
}
return ret.toStdString();
}
friend class TypeDescriptorHasher; friend class TypeDescriptorHasher;
...@@ -233,6 +226,59 @@ class TypeDescriptorHasher { ...@@ -233,6 +226,59 @@ class TypeDescriptorHasher {
} }
}; };
[[noreturn]] void singletonWarnLeakyDoubleRegistrationAndAbort(
const TypeDescriptor& type);
[[noreturn]] void singletonWarnLeakyInstantiatingNotRegisteredAndAbort(
const TypeDescriptor& type);
[[noreturn]] void singletonWarnRegisterMockEarlyAndAbort(
const TypeDescriptor& type);
void singletonWarnDestroyInstanceLeak(
const TypeDescriptor& type,
const void* ptr);
[[noreturn]] void singletonWarnCreateCircularDependencyAndAbort(
const TypeDescriptor& type);
[[noreturn]] void singletonWarnCreateUnregisteredAndAbort(
const TypeDescriptor& type);
[[noreturn]] void singletonWarnCreateBeforeRegistrationCompleteAndAbort(
const TypeDescriptor& type);
void singletonPrintDestructionStackTrace(const TypeDescriptor& type);
[[noreturn]] void singletonThrowNullCreator(const std::type_info& type);
[[noreturn]] void singletonThrowGetInvokedAfterDestruction(
const TypeDescriptor& type);
struct SingletonVaultState {
// The two stages of life for a vault, as mentioned in the class comment.
enum class Type {
Running,
Quiescing,
};
Type state{Type::Running};
bool registrationComplete{false};
// Each singleton in the vault can be in two states: dead
// (registered but never created), living (CreateFunc returned an instance).
void check(
Type expected,
const char* msg = "Unexpected singleton state change") const {
if (expected != state) {
throwUnexpectedState(msg);
}
}
[[noreturn]] static void throwUnexpectedState(const char* msg);
};
// This interface is used by SingletonVault to interact with SingletonHolders. // This interface is used by SingletonVault to interact with SingletonHolders.
// Having a non-template interface allows SingletonVault to keep a list of all // Having a non-template interface allows SingletonVault to keep a list of all
// SingletonHolders. // SingletonHolders.
...@@ -475,29 +521,6 @@ class SingletonVault { ...@@ -475,29 +521,6 @@ class SingletonVault {
template <typename T> template <typename T>
friend struct detail::SingletonHolder; friend struct detail::SingletonHolder;
// The two stages of life for a vault, as mentioned in the class comment.
enum class SingletonVaultState {
Running,
Quiescing,
};
struct State {
SingletonVaultState state{SingletonVaultState::Running};
bool registrationComplete{false};
};
// Each singleton in the vault can be in two states: dead
// (registered but never created), living (CreateFunc returned an instance).
static void stateCheck(
SingletonVaultState expected,
const State& state,
const char* msg = "Unexpected singleton state change") {
if (expected != state.state) {
throw std::logic_error(msg);
}
}
// This method only matters if registrationComplete() is never called. // This method only matters if registrationComplete() is never called.
// Otherwise destroyInstances is scheduled to be executed atexit. // Otherwise destroyInstances is scheduled to be executed atexit.
// //
...@@ -514,15 +537,15 @@ class SingletonVault { ...@@ -514,15 +537,15 @@ class SingletonVault {
typedef std::unordered_map<detail::TypeDescriptor, typedef std::unordered_map<detail::TypeDescriptor,
detail::SingletonHolderBase*, detail::SingletonHolderBase*,
detail::TypeDescriptorHasher> SingletonMap; detail::TypeDescriptorHasher> SingletonMap;
folly::Synchronized<SingletonMap> singletons_; Synchronized<SingletonMap> singletons_;
folly::Synchronized<std::unordered_set<detail::SingletonHolderBase*>> Synchronized<std::unordered_set<detail::SingletonHolderBase*>>
eagerInitSingletons_; eagerInitSingletons_;
folly::Synchronized<std::vector<detail::TypeDescriptor>> creationOrder_; Synchronized<std::vector<detail::TypeDescriptor>> creationOrder_;
// Using SharedMutexReadPriority is important here, because we want to make // Using SharedMutexReadPriority is important here, because we want to make
// sure we don't block nested singleton creation happening concurrently with // sure we don't block nested singleton creation happening concurrently with
// destroyInstances(). // destroyInstances().
folly::Synchronized<State, folly::SharedMutexReadPriority> state_; Synchronized<detail::SingletonVaultState, SharedMutexReadPriority> state_;
Type type_; Type type_;
}; };
...@@ -576,8 +599,7 @@ class Singleton { ...@@ -576,8 +599,7 @@ class Singleton {
explicit Singleton(typename Singleton::CreateFunc c, explicit Singleton(typename Singleton::CreateFunc c,
typename Singleton::TeardownFunc t = nullptr) { typename Singleton::TeardownFunc t = nullptr) {
if (c == nullptr) { if (c == nullptr) {
throw std::logic_error( detail::singletonThrowNullCreator(typeid(T));
"nullptr_t should be passed if you want T to be default constructed");
} }
auto vault = SingletonVault::singleton<VaultTag>(); auto vault = SingletonVault::singleton<VaultTag>();
...@@ -625,8 +647,7 @@ class Singleton { ...@@ -625,8 +647,7 @@ class Singleton {
static void make_mock(CreateFunc c, static void make_mock(CreateFunc c,
typename Singleton<T>::TeardownFunc t = nullptr) { typename Singleton<T>::TeardownFunc t = nullptr) {
if (c == nullptr) { if (c == nullptr) {
throw std::logic_error( detail::singletonThrowNullCreator(typeid(T));
"nullptr_t should be passed if you want T to be default constructed");
} }
auto& entry = getEntry(); auto& entry = getEntry();
...@@ -660,9 +681,7 @@ class LeakySingleton { ...@@ -660,9 +681,7 @@ class LeakySingleton {
explicit LeakySingleton(CreateFunc createFunc) { explicit LeakySingleton(CreateFunc createFunc) {
auto& entry = entryInstance(); auto& entry = entryInstance();
if (entry.state != State::NotRegistered) { if (entry.state != State::NotRegistered) {
LOG(FATAL) << "Double registration of singletons of the same " detail::singletonWarnLeakyDoubleRegistrationAndAbort(entry.type_);
<< "underlying type; check for multiple definitions "
<< "of type folly::LeakySingleton<" + entry.type_.name() + ">";
} }
entry.createFunc = createFunc; entry.createFunc = createFunc;
entry.state = State::Dead; entry.state = State::Dead;
...@@ -675,12 +694,11 @@ class LeakySingleton { ...@@ -675,12 +694,11 @@ class LeakySingleton {
} }
static void make_mock(CreateFunc createFunc) { static void make_mock(CreateFunc createFunc) {
auto& entry = entryInstance();
if (createFunc == nullptr) { if (createFunc == nullptr) {
throw std::logic_error( detail::singletonThrowNullCreator(typeid(T));
"nullptr_t should be passed if you want T to be default constructed");
} }
auto& entry = entryInstance();
entry.createFunc = createFunc; entry.createFunc = createFunc;
entry.state = State::Dead; entry.state = State::Dead;
} }
...@@ -723,11 +741,7 @@ class LeakySingleton { ...@@ -723,11 +741,7 @@ class LeakySingleton {
} }
if (entry.state == State::NotRegistered) { if (entry.state == State::NotRegistered) {
auto ptr = SingletonVault::stackTraceGetter().load(); detail::singletonWarnLeakyInstantiatingNotRegisteredAndAbort(entry.type_);
LOG(FATAL) << "Creating instance for unregistered singleton: "
<< entry.type_.name() << "\n"
<< "Stacktrace:"
<< "\n" << (ptr ? (*ptr)() : "(not available)");
} }
entry.ptr = entry.createFunc(); entry.ptr = entry.createFunc();
......
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