From 20bd643d25350e6cf0bb0232a5ebf78522b359e9 Mon Sep 17 00:00:00 2001 From: Dmitry Pleshkov Date: Mon, 1 Feb 2016 15:35:18 -0800 Subject: [PATCH] Implementing callback functionality for exception routines Summary: Depends on D2865911 Reviewed By: luciang Differential Revision: D2742158 fb-gh-sync-id: 3e7866a742575ee4f7501cff0abbd5c21e26a46e --- .../exception_tracer/ExceptionCounterLib.cpp | 142 ++++++++++++++ .../exception_tracer/ExceptionCounterLib.h | 43 ++++ .../ExceptionStackTraceLib.cpp | 111 +++++++++++ .../exception_tracer/ExceptionTracer.cpp | 10 +- .../exception_tracer/ExceptionTracerLib.cpp | 183 +++++++----------- .../exception_tracer/ExceptionTracerLib.h | 54 ++++++ .../test/ExceptionCounterTest.cpp | 125 ++++++++++++ 7 files changed, 548 insertions(+), 120 deletions(-) create mode 100644 folly/experimental/exception_tracer/ExceptionCounterLib.cpp create mode 100644 folly/experimental/exception_tracer/ExceptionCounterLib.h create mode 100644 folly/experimental/exception_tracer/ExceptionStackTraceLib.cpp create mode 100644 folly/experimental/exception_tracer/ExceptionTracerLib.h create mode 100644 folly/experimental/exception_tracer/test/ExceptionCounterTest.cpp diff --git a/folly/experimental/exception_tracer/ExceptionCounterLib.cpp b/folly/experimental/exception_tracer/ExceptionCounterLib.cpp new file mode 100644 index 00000000..d08971b0 --- /dev/null +++ b/folly/experimental/exception_tracer/ExceptionCounterLib.cpp @@ -0,0 +1,142 @@ +/* + * Copyright 2016 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include + +using namespace folly::exception_tracer; + +namespace { + +// We are using hash of the stack trace to uniquely identify the exception +using ExceptionId = uintptr_t; + +using ExceptionStatsHolderType = + std::unordered_map; + +struct ExceptionStatsStorage { + void appendTo(ExceptionStatsHolderType& data) { + ExceptionStatsHolderType tempHolder; + SYNCHRONIZED(statsHolder) { + using std::swap; + swap(statsHolder, tempHolder); + } + + for (const auto& myData : tempHolder) { + const auto& myStat = myData.second; + + auto it = data.find(myData.first); + if (it != data.end()) { + it->second.count += myStat.count; + } else { + data.insert(myData); + } + } + } + + folly::Synchronized statsHolder; +}; + +class Tag {}; + +folly::ThreadLocal gExceptionStats; + +} // namespace + +namespace folly { +namespace exception_tracer { + +std::vector getExceptionStatistics() { + ExceptionStatsHolderType accumulator; + for (auto& threadStats : gExceptionStats.accessAllThreads()) { + threadStats.appendTo(accumulator); + } + + std::vector result; + result.reserve(accumulator.size()); + for (const auto& item : accumulator) { + result.push_back(item.second); + } + + std::sort(result.begin(), + result.end(), + [](const ExceptionStats& lhs, const ExceptionStats& rhs) { + return (lhs.count > rhs.count); + }); + + return result; +} + +std::ostream& operator<<(std::ostream& out, const ExceptionStats& stats) { + out << "Exception report: " << std::endl; + out << "Exception count: " << stats.count << std::endl; + out << stats.info; + + return out; +} + +} // namespace exception_tracer +} // namespace folly + +namespace { + +/* + * This handler gathers statistics on all exceptions thrown by the program + * Information is being stored in thread local storage. + */ +void throwHandler(void*, std::type_info* exType, void (*)(void*)) noexcept { + ExceptionInfo info; + info.type = exType; + auto& frames = info.frames; + + frames.resize(kMaxFrames); + auto n = folly::symbolizer::getStackTrace(frames.data(), kMaxFrames); + + if (n == -1) { + LOG(ERROR) << "Invalid stack frame"; + return; + } + + frames.resize(n); + auto exceptionId = folly::hash::hash_range(frames.begin(), frames.end()); + + SYNCHRONIZED(holder, gExceptionStats->statsHolder) { + auto it = holder.find(exceptionId); + if (it != holder.end()) { + ++it->second.count; + } else { + holder.emplace(exceptionId, ExceptionStats{1, std::move(info)}); + } + } +} + +struct Initializer { + Initializer() { registerCxaThrowCallback(throwHandler); } +}; + +Initializer initializer; + +} // namespace diff --git a/folly/experimental/exception_tracer/ExceptionCounterLib.h b/folly/experimental/exception_tracer/ExceptionCounterLib.h new file mode 100644 index 00000000..3e96dc91 --- /dev/null +++ b/folly/experimental/exception_tracer/ExceptionCounterLib.h @@ -0,0 +1,43 @@ +/* + * Copyright 2016 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +namespace folly { +namespace exception_tracer { + +struct ExceptionStats { + uint64_t count; + ExceptionInfo info; +}; + +/** + * This function accumulates exception throwing statistics across all threads. + * Please note, that during call to this function, other threads might block + * on exception throws, so it should be called seldomly. + * All pef-thread statistics is being reset by the call. + */ +std::vector getExceptionStatistics(); + +std::ostream& operator<<(std::ostream& out, const ExceptionStats& data); + +} // namespace exception_tracer +} // namespace folly diff --git a/folly/experimental/exception_tracer/ExceptionStackTraceLib.cpp b/folly/experimental/exception_tracer/ExceptionStackTraceLib.cpp new file mode 100644 index 00000000..aee87b8c --- /dev/null +++ b/folly/experimental/exception_tracer/ExceptionStackTraceLib.cpp @@ -0,0 +1,111 @@ +/* + * Copyright 2016 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include +#include + +using namespace folly::exception_tracer; + +namespace { + +// If we somehow ended up in an invalid state, we don't want to print any stack +// trace at all because in could be bogus +FOLLY_TLS bool invalid; + +FOLLY_TLS StackTraceStack activeExceptions; +FOLLY_TLS StackTraceStack caughtExceptions; + +} // namespace + +// This function is exported and may be found via dlsym(RTLD_NEXT, ...) +extern "C" StackTraceStack* getExceptionStackTraceStack() { + return invalid ? nullptr : &caughtExceptions; +} + +namespace { + +void addActiveException() { + // Capture stack trace + if (!invalid) { + if (!activeExceptions.pushCurrent()) { + activeExceptions.clear(); + caughtExceptions.clear(); + invalid = true; + } + } +} + +void moveTopException(StackTraceStack& from, StackTraceStack& to) { + if (invalid) { + return; + } + if (!to.moveTopFrom(from)) { + from.clear(); + to.clear(); + invalid = true; + } +} + +struct Initializer { + Initializer() { + registerCxaThrowCallback( + [](void*, std::type_info*, void (*)(void*)) { addActiveException(); }); + + registerCxaBeginCatchCallback( + [](void*) { moveTopException(activeExceptions, caughtExceptions); }); + + registerCxaRethrowCallback( + []() { moveTopException(caughtExceptions, activeExceptions); }); + + registerCxaEndCatchCallback([]() { + if (invalid) { + return; + } + + __cxxabiv1::__cxa_exception* top = + __cxxabiv1::__cxa_get_globals_fast()->caughtExceptions; + // This is gcc specific and not specified in the ABI: + // abs(handlerCount) is the number of active handlers, it's negative + // for rethrown exceptions and positive (always 1) for regular + // exceptions. + // In the rethrow case, we've already popped the exception off the + // caught stack, so we don't do anything here. + if (top->handlerCount == 1) { + if (!caughtExceptions.pop()) { + activeExceptions.clear(); + invalid = true; + } + } + }); + + registerRethrowExceptionCallback( + [](std::exception_ptr) { addActiveException(); }); + + try { + ::folly::exception_tracer::installHandlers(); + } catch (...) { + } + } +}; + +Initializer initializer; + +} // namespace diff --git a/folly/experimental/exception_tracer/ExceptionTracer.cpp b/folly/experimental/exception_tracer/ExceptionTracer.cpp index c65398d3..593444d5 100644 --- a/folly/experimental/exception_tracer/ExceptionTracer.cpp +++ b/folly/experimental/exception_tracer/ExceptionTracer.cpp @@ -55,12 +55,12 @@ std::ostream& operator<<(std::ostream& out, const ExceptionInfo& info) { << ")\n"; try { size_t frameCount = info.frames.size(); - // Skip our own internal frames - static constexpr size_t skip = 3; - if (frameCount > skip) { - auto addresses = info.frames.data() + skip; - frameCount -= skip; + // Skip our own internal frames + static constexpr size_t kInternalFramesNumber = 3; + if (frameCount > kInternalFramesNumber) { + auto addresses = info.frames.data() + kInternalFramesNumber; + frameCount -= kInternalFramesNumber; std::vector frames; frames.resize(frameCount); diff --git a/folly/experimental/exception_tracer/ExceptionTracerLib.cpp b/folly/experimental/exception_tracer/ExceptionTracerLib.cpp index 1eb303aa..f854ddc0 100644 --- a/folly/experimental/exception_tracer/ExceptionTracerLib.cpp +++ b/folly/experimental/exception_tracer/ExceptionTracerLib.cpp @@ -14,17 +14,15 @@ * limitations under the License. */ +#include + #include -#include -#include -#include +#include #include -#include -#include -#include -#include +#include +#include namespace __cxxabiv1 { @@ -34,102 +32,70 @@ FOLLY_NORETURN void __cxa_throw(void* thrownException, void (*destructor)(void*)); void* __cxa_begin_catch(void* excObj) throw(); FOLLY_NORETURN void __cxa_rethrow(void); +void __cxa_rethrow(void); void __cxa_end_catch(void); } -} // namespace __cxxabiv1 +} // namespace __cxxabiv1 using namespace folly::exception_tracer; namespace { -FOLLY_TLS bool invalid; -FOLLY_TLS StackTraceStack activeExceptions; -FOLLY_TLS StackTraceStack caughtExceptions; -pthread_once_t initialized = PTHREAD_ONCE_INIT; +template +class CallbackHolder { + public: + void registerCallback(Function f) { + SYNCHRONIZED(callbacks_) { callbacks_.push_back(std::move(f)); } + } -extern "C" { -FOLLY_NORETURN typedef void (*CxaThrowType)(void*, - std::type_info*, - void (*)(void*)); -typedef void* (*CxaBeginCatchType)(void*); -FOLLY_NORETURN typedef void (*CxaRethrowType)(void); -typedef void (*CxaEndCatchType)(void); - -CxaThrowType orig_cxa_throw; -CxaBeginCatchType orig_cxa_begin_catch; -CxaRethrowType orig_cxa_rethrow; -CxaEndCatchType orig_cxa_end_catch; -} // extern "C" - -FOLLY_NORETURN typedef void (*RethrowExceptionType)(std::exception_ptr); -RethrowExceptionType orig_rethrow_exception; - -void initialize() { - orig_cxa_throw = (CxaThrowType)dlsym(RTLD_NEXT, "__cxa_throw"); - orig_cxa_begin_catch = - (CxaBeginCatchType)dlsym(RTLD_NEXT, "__cxa_begin_catch"); - orig_cxa_rethrow = - (CxaRethrowType)dlsym(RTLD_NEXT, "__cxa_rethrow"); - orig_cxa_end_catch = (CxaEndCatchType)dlsym(RTLD_NEXT, "__cxa_end_catch"); - // Mangled name for std::rethrow_exception - // TODO(tudorb): Dicey, as it relies on the fact that std::exception_ptr - // is typedef'ed to a type in namespace __exception_ptr - orig_rethrow_exception = - (RethrowExceptionType)dlsym( - RTLD_NEXT, - "_ZSt17rethrow_exceptionNSt15__exception_ptr13exception_ptrE"); - - if (!orig_cxa_throw || !orig_cxa_begin_catch || !orig_cxa_rethrow || - !orig_cxa_end_catch || !orig_rethrow_exception) { - abort(); // what else can we do? + // always inline to enforce kInternalFramesNumber + template + FOLLY_ALWAYS_INLINE void invoke(Args... args) { + SYNCHRONIZED_CONST(callbacks_) { + for (auto& cb : callbacks_) { + cb(args...); + } + } } -} -} // namespace + private: + folly::Synchronized> callbacks_; +}; -// This function is exported and may be found via dlsym(RTLD_NEXT, ...) -extern "C" StackTraceStack* getExceptionStackTraceStack() { - return invalid ? nullptr : &caughtExceptions; -} +} // namespace -namespace { +namespace folly { +namespace exception_tracer { -// Make sure we're counting stack frames correctly, don't inline. -FOLLY_NOINLINE void addActiveException(); - -void addActiveException() { - pthread_once(&initialized, initialize); - // Capture stack trace - if (!invalid) { - if (!activeExceptions.pushCurrent()) { - activeExceptions.clear(); - caughtExceptions.clear(); - invalid = true; - } +#define DECLARE_CALLBACK(NAME) \ + CallbackHolder& get##NAME##Callbacks() { \ + static CallbackHolder Callbacks; \ + return Callbacks; \ + } \ + void register##NAME##Callback(NAME##Type callback) { \ + get##NAME##Callbacks().registerCallback(callback); \ } -} -void moveTopException(StackTraceStack& from, StackTraceStack& to) { - if (invalid) { - return; - } - if (!to.moveTopFrom(from)) { - from.clear(); - to.clear(); - invalid = true; - } -} +DECLARE_CALLBACK(CxaThrow); +DECLARE_CALLBACK(CxaBeginCatch); +DECLARE_CALLBACK(CxaRethrow); +DECLARE_CALLBACK(CxaEndCatch); +DECLARE_CALLBACK(RethrowException); -} // namespace +} // exception_tracer +} // folly namespace __cxxabiv1 { void __cxa_throw(void* thrownException, std::type_info* type, void (*destructor)(void*)) { - addActiveException(); + static auto orig_cxa_throw = + reinterpret_cast(dlsym(RTLD_NEXT, "__cxa_throw")); + getCxaThrowCallbacks().invoke(thrownException, type, destructor); orig_cxa_throw(thrownException, type, destructor); + __builtin_unreachable(); // orig_cxa_throw never returns } void __cxa_rethrow() { @@ -138,57 +104,44 @@ void __cxa_rethrow() { // we'll implement something simpler (and slower): we pop the exception from // the caught stack, and push it back onto the active stack; this way, our // implementation of __cxa_begin_catch doesn't have to do anything special. - moveTopException(caughtExceptions, activeExceptions); + static auto orig_cxa_rethrow = reinterpret_cast( + dlsym(RTLD_NEXT, "__cxa_rethrow")); + getCxaRethrowCallbacks().invoke(); orig_cxa_rethrow(); + __builtin_unreachable(); // orig_cxa_rethrow never returns } -void* __cxa_begin_catch(void *excObj) throw() { +void* __cxa_begin_catch(void* excObj) throw() { // excObj is a pointer to the unwindHeader in __cxa_exception - moveTopException(activeExceptions, caughtExceptions); + static auto orig_cxa_begin_catch = + reinterpret_cast( + dlsym(RTLD_NEXT, "__cxa_begin_catch")); + getCxaBeginCatchCallbacks().invoke(excObj); return orig_cxa_begin_catch(excObj); } void __cxa_end_catch() { - if (!invalid) { - __cxa_exception* top = __cxa_get_globals_fast()->caughtExceptions; - // This is gcc specific and not specified in the ABI: - // abs(handlerCount) is the number of active handlers, it's negative - // for rethrown exceptions and positive (always 1) for regular exceptions. - // In the rethrow case, we've already popped the exception off the - // caught stack, so we don't do anything here. - if (top->handlerCount == 1) { - if (!caughtExceptions.pop()) { - activeExceptions.clear(); - invalid = true; - } - } - } + static auto orig_cxa_end_catch = reinterpret_cast( + dlsym(RTLD_NEXT, "__cxa_end_catch")); + getCxaEndCatchCallbacks().invoke(); orig_cxa_end_catch(); } -} // namespace __cxxabiv1 +} // namespace __cxxabiv1 namespace std { void rethrow_exception(std::exception_ptr ep) { - addActiveException(); + // Mangled name for std::rethrow_exception + // TODO(tudorb): Dicey, as it relies on the fact that std::exception_ptr + // is typedef'ed to a type in namespace __exception_ptr + static auto orig_rethrow_exception = + reinterpret_cast( + dlsym(RTLD_NEXT, + "_ZSt17rethrow_exceptionNSt15__exception_ptr13exception_ptrE")); + getRethrowExceptionCallbacks().invoke(ep); orig_rethrow_exception(ep); + __builtin_unreachable(); // orig_rethrow_exception never returns } -} // namespace std - - -namespace { - -struct Initializer { - Initializer() { - try { - ::folly::exception_tracer::installHandlers(); - } catch (...) { - } - } -}; - -Initializer initializer; - -} // namespace +} // namespace std diff --git a/folly/experimental/exception_tracer/ExceptionTracerLib.h b/folly/experimental/exception_tracer/ExceptionTracerLib.h new file mode 100644 index 00000000..72d16311 --- /dev/null +++ b/folly/experimental/exception_tracer/ExceptionTracerLib.h @@ -0,0 +1,54 @@ +/* + * Copyright 2016 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FOLLY_EXPERIMENTAL_EXCEPTION_TRACER_EXCEPTIONTRACERLIB_H_ +#define FOLLY_EXPERIMENTAL_EXCEPTION_TRACER_EXCEPTIONTRACERLIB_H_ + +#include +#include + +namespace folly { +namespace exception_tracer { + +namespace detail { +/* + * Unfortunately due to ambiguous nature of exception specifiers, + * standard does not allow them to appear in typedefs or alias-declarations. + * We, however, want callbacks to be exception safe. + * This dummies are an ugly workaround that problem. + */ +void dummyCxaThrow(void*, std::type_info*, void (*)(void*)) noexcept; +void dummyCxaBeginCatch(void*) noexcept; +void dummyCxaRethrow(void) noexcept; +void dummyCxaEndCatch(void) noexcept; +void dummyRethrowException(std::exception_ptr) noexcept; +} + +using CxaThrowType = decltype(&detail::dummyCxaThrow); +using CxaBeginCatchType = decltype(&detail::dummyCxaBeginCatch); +using CxaRethrowType = decltype(&detail::dummyCxaRethrow); +using CxaEndCatchType = decltype(&detail::dummyCxaEndCatch); +using RethrowExceptionType = decltype(&detail::dummyRethrowException); + +void registerCxaThrowCallback(CxaThrowType callback); +void registerCxaBeginCatchCallback(CxaBeginCatchType callback); +void registerCxaRethrowCallback(CxaRethrowType callback); +void registerCxaEndCatchCallback(CxaEndCatchType callback); +void registerRethrowExceptionCallback(RethrowExceptionType callback); +} +} + +#endif /* FOLLY_EXPERIMENTAL_EXCEPTION_TRACER_EXCEPTIONTRACERLIB_H_ */ diff --git a/folly/experimental/exception_tracer/test/ExceptionCounterTest.cpp b/folly/experimental/exception_tracer/test/ExceptionCounterTest.cpp new file mode 100644 index 00000000..37d63921 --- /dev/null +++ b/folly/experimental/exception_tracer/test/ExceptionCounterTest.cpp @@ -0,0 +1,125 @@ +/* + * Copyright 2016 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include + +struct MyException {}; + +void bar() { throw std::runtime_error("hello"); } + +void foo() { throw MyException(); } + +void baz() { foo(); } + +using namespace folly::exception_tracer; + +template +void throwAndCatch(F f) { + try { + f(); + } catch (...) { + // ignore + } +} + +TEST(ExceptionCounter, oneThread) { + throwAndCatch(foo); + for (int i = 0; i < 10; ++i) { + throwAndCatch(bar); + } + + auto stats = getExceptionStatistics(); + EXPECT_EQ(stats.size(), 2); + EXPECT_EQ(stats[0].count, 10); + EXPECT_EQ(stats[1].count, 1); + EXPECT_EQ(*(stats[0].info.type), typeid(std::runtime_error)); + EXPECT_EQ(*(stats[1].info.type), typeid(MyException)); +} + +TEST(ExceptionCounter, testClearExceptionStatistics) { + throwAndCatch(foo); + auto stats = getExceptionStatistics(); + EXPECT_EQ(stats.size(), 1); + stats = getExceptionStatistics(); + EXPECT_EQ(stats.size(), 0); +} + +TEST(ExceptionCounter, testDifferentStacks) { + throwAndCatch(foo); + throwAndCatch(baz); + auto stats = getExceptionStatistics(); + EXPECT_EQ(stats.size(), 2); +} + +TEST(ExceptionCounter, multyThreads) { + constexpr size_t kNumIterations = 10000; + constexpr size_t kNumThreads = 10; + std::vector threads; + threads.resize(kNumThreads); + + std::mutex preparedMutex; + std::mutex finishedMutex; + std::condition_variable preparedBarrier; + std::condition_variable finishedBarrier; + int preparedThreads = 0; + bool finished = false; + + for (auto& t : threads) { + t = std::thread([&]() { + for (size_t i = 0; i < kNumIterations; ++i) { + throwAndCatch(foo); + } + + { + std::unique_lock lock(preparedMutex); + ++preparedThreads; + preparedBarrier.notify_one(); + } + + std::unique_lock lock(finishedMutex); + finishedBarrier.wait(lock, [&]() { return finished; }); + }); + } + + { + std::unique_lock lock(preparedMutex); + preparedBarrier.wait(lock, + [&]() { return preparedThreads == kNumThreads; }); + } + + auto stats = getExceptionStatistics(); + EXPECT_EQ(stats.size(), 1); + EXPECT_EQ(stats[0].count, kNumIterations * kNumThreads); + + { + std::unique_lock lock(finishedMutex); + finished = true; + finishedBarrier.notify_all(); + } + + for (auto& t : threads) { + t.join(); + } +} -- 2.34.1