-- ca3a0009e675b699b5d6dd41f00ebac0e7d1935c by Derek Mauro <dmauro@google.com>: Internal change PiperOrigin-RevId: 396475923 -- 04d9fff79085bb18612af3da49007907394ae0b6 by Abseil Team <absl-team@google.com>: Move HastablezSampler from container/internal to profiling/internal. PiperOrigin-RevId: 396362093 GitOrigin-RevId: ca3a0009e675b699b5d6dd41f00ebac0e7d1935c Change-Id: I42d6d2944786afa24259fde002fed5e611f4e1f9pull/1016/head
parent
669184b4f3
commit
b2dc72c17a
11 changed files with 482 additions and 176 deletions
@ -0,0 +1,231 @@ |
|||||||
|
// Copyright 2018 The Abseil Authors.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
//
|
||||||
|
// https://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.
|
||||||
|
//
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// File: sample_recorder.h
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// This header file defines a lock-free linked list for recording samples
|
||||||
|
// collected from a random/stochastic process.
|
||||||
|
//
|
||||||
|
// This utility is internal-only. Use at your own risk.
|
||||||
|
|
||||||
|
#ifndef ABSL_PROFILING_INTERNAL_SAMPLE_RECORDER_H_ |
||||||
|
#define ABSL_PROFILING_INTERNAL_SAMPLE_RECORDER_H_ |
||||||
|
|
||||||
|
#include <atomic> |
||||||
|
#include <cstddef> |
||||||
|
#include <functional> |
||||||
|
|
||||||
|
#include "absl/base/config.h" |
||||||
|
#include "absl/base/thread_annotations.h" |
||||||
|
#include "absl/synchronization/mutex.h" |
||||||
|
#include "absl/time/time.h" |
||||||
|
|
||||||
|
namespace absl { |
||||||
|
ABSL_NAMESPACE_BEGIN |
||||||
|
namespace profiling_internal { |
||||||
|
|
||||||
|
// Sample<T> that has members required for linking samples in the linked list of
|
||||||
|
// samples maintained by the SampleRecorder. Type T defines the sampled data.
|
||||||
|
template <typename T> |
||||||
|
struct Sample { |
||||||
|
public: |
||||||
|
// Guards the ability to restore the sample to a pristine state. This
|
||||||
|
// prevents races with sampling and resurrecting an object.
|
||||||
|
absl::Mutex init_mu; |
||||||
|
T* next = nullptr; |
||||||
|
T* dead ABSL_GUARDED_BY(init_mu) = nullptr; |
||||||
|
}; |
||||||
|
|
||||||
|
// Holds samples and their associated stack traces with a soft limit of
|
||||||
|
// `SetHashtablezMaxSamples()`.
|
||||||
|
//
|
||||||
|
// Thread safe.
|
||||||
|
template <typename T> |
||||||
|
class SampleRecorder { |
||||||
|
public: |
||||||
|
SampleRecorder(); |
||||||
|
~SampleRecorder(); |
||||||
|
|
||||||
|
// Registers for sampling. Returns an opaque registration info.
|
||||||
|
T* Register(); |
||||||
|
|
||||||
|
// Unregisters the sample.
|
||||||
|
void Unregister(T* sample); |
||||||
|
|
||||||
|
// The dispose callback will be called on all samples the moment they are
|
||||||
|
// being unregistered. Only affects samples that are unregistered after the
|
||||||
|
// callback has been set.
|
||||||
|
// Returns the previous callback.
|
||||||
|
using DisposeCallback = void (*)(const T&); |
||||||
|
DisposeCallback SetDisposeCallback(DisposeCallback f); |
||||||
|
|
||||||
|
// Iterates over all the registered `StackInfo`s. Returning the number of
|
||||||
|
// samples that have been dropped.
|
||||||
|
int64_t Iterate(const std::function<void(const T& stack)>& f); |
||||||
|
|
||||||
|
void SetMaxSamples(int32_t max); |
||||||
|
|
||||||
|
private: |
||||||
|
void PushNew(T* sample); |
||||||
|
void PushDead(T* sample); |
||||||
|
T* PopDead(); |
||||||
|
|
||||||
|
std::atomic<size_t> dropped_samples_; |
||||||
|
std::atomic<size_t> size_estimate_; |
||||||
|
std::atomic<int32_t> max_samples_{1 << 20}; |
||||||
|
|
||||||
|
// Intrusive lock free linked lists for tracking samples.
|
||||||
|
//
|
||||||
|
// `all_` records all samples (they are never removed from this list) and is
|
||||||
|
// terminated with a `nullptr`.
|
||||||
|
//
|
||||||
|
// `graveyard_.dead` is a circular linked list. When it is empty,
|
||||||
|
// `graveyard_.dead == &graveyard`. The list is circular so that
|
||||||
|
// every item on it (even the last) has a non-null dead pointer. This allows
|
||||||
|
// `Iterate` to determine if a given sample is live or dead using only
|
||||||
|
// information on the sample itself.
|
||||||
|
//
|
||||||
|
// For example, nodes [A, B, C, D, E] with [A, C, E] alive and [B, D] dead
|
||||||
|
// looks like this (G is the Graveyard):
|
||||||
|
//
|
||||||
|
// +---+ +---+ +---+ +---+ +---+
|
||||||
|
// all -->| A |--->| B |--->| C |--->| D |--->| E |
|
||||||
|
// | | | | | | | | | |
|
||||||
|
// +---+ | | +->| |-+ | | +->| |-+ | |
|
||||||
|
// | G | +---+ | +---+ | +---+ | +---+ | +---+
|
||||||
|
// | | | | | |
|
||||||
|
// | | --------+ +--------+ |
|
||||||
|
// +---+ |
|
||||||
|
// ^ |
|
||||||
|
// +--------------------------------------+
|
||||||
|
//
|
||||||
|
std::atomic<T*> all_; |
||||||
|
T graveyard_; |
||||||
|
|
||||||
|
std::atomic<DisposeCallback> dispose_; |
||||||
|
}; |
||||||
|
|
||||||
|
template <typename T> |
||||||
|
typename SampleRecorder<T>::DisposeCallback |
||||||
|
SampleRecorder<T>::SetDisposeCallback(DisposeCallback f) { |
||||||
|
return dispose_.exchange(f, std::memory_order_relaxed); |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> |
||||||
|
SampleRecorder<T>::SampleRecorder() |
||||||
|
: dropped_samples_(0), size_estimate_(0), all_(nullptr), dispose_(nullptr) { |
||||||
|
absl::MutexLock l(&graveyard_.init_mu); |
||||||
|
graveyard_.dead = &graveyard_; |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> |
||||||
|
SampleRecorder<T>::~SampleRecorder() { |
||||||
|
T* s = all_.load(std::memory_order_acquire); |
||||||
|
while (s != nullptr) { |
||||||
|
T* next = s->next; |
||||||
|
delete s; |
||||||
|
s = next; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> |
||||||
|
void SampleRecorder<T>::PushNew(T* sample) { |
||||||
|
sample->next = all_.load(std::memory_order_relaxed); |
||||||
|
while (!all_.compare_exchange_weak(sample->next, sample, |
||||||
|
std::memory_order_release, |
||||||
|
std::memory_order_relaxed)) { |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> |
||||||
|
void SampleRecorder<T>::PushDead(T* sample) { |
||||||
|
if (auto* dispose = dispose_.load(std::memory_order_relaxed)) { |
||||||
|
dispose(*sample); |
||||||
|
} |
||||||
|
|
||||||
|
absl::MutexLock graveyard_lock(&graveyard_.init_mu); |
||||||
|
absl::MutexLock sample_lock(&sample->init_mu); |
||||||
|
sample->dead = graveyard_.dead; |
||||||
|
graveyard_.dead = sample; |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> |
||||||
|
T* SampleRecorder<T>::PopDead() { |
||||||
|
absl::MutexLock graveyard_lock(&graveyard_.init_mu); |
||||||
|
|
||||||
|
// The list is circular, so eventually it collapses down to
|
||||||
|
// graveyard_.dead == &graveyard_
|
||||||
|
// when it is empty.
|
||||||
|
T* sample = graveyard_.dead; |
||||||
|
if (sample == &graveyard_) return nullptr; |
||||||
|
|
||||||
|
absl::MutexLock sample_lock(&sample->init_mu); |
||||||
|
graveyard_.dead = sample->dead; |
||||||
|
sample->dead = nullptr; |
||||||
|
sample->PrepareForSampling(); |
||||||
|
return sample; |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> |
||||||
|
T* SampleRecorder<T>::Register() { |
||||||
|
int64_t size = size_estimate_.fetch_add(1, std::memory_order_relaxed); |
||||||
|
if (size > max_samples_.load(std::memory_order_relaxed)) { |
||||||
|
size_estimate_.fetch_sub(1, std::memory_order_relaxed); |
||||||
|
dropped_samples_.fetch_add(1, std::memory_order_relaxed); |
||||||
|
return nullptr; |
||||||
|
} |
||||||
|
|
||||||
|
T* sample = PopDead(); |
||||||
|
if (sample == nullptr) { |
||||||
|
// Resurrection failed. Hire a new warlock.
|
||||||
|
sample = new T(); |
||||||
|
PushNew(sample); |
||||||
|
} |
||||||
|
|
||||||
|
return sample; |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> |
||||||
|
void SampleRecorder<T>::Unregister(T* sample) { |
||||||
|
PushDead(sample); |
||||||
|
size_estimate_.fetch_sub(1, std::memory_order_relaxed); |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> |
||||||
|
int64_t SampleRecorder<T>::Iterate( |
||||||
|
const std::function<void(const T& stack)>& f) { |
||||||
|
T* s = all_.load(std::memory_order_acquire); |
||||||
|
while (s != nullptr) { |
||||||
|
absl::MutexLock l(&s->init_mu); |
||||||
|
if (s->dead == nullptr) { |
||||||
|
f(*s); |
||||||
|
} |
||||||
|
s = s->next; |
||||||
|
} |
||||||
|
|
||||||
|
return dropped_samples_.load(std::memory_order_relaxed); |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> |
||||||
|
void SampleRecorder<T>::SetMaxSamples(int32_t max) { |
||||||
|
max_samples_.store(max, std::memory_order_release); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace profiling_internal
|
||||||
|
ABSL_NAMESPACE_END |
||||||
|
} // namespace absl
|
||||||
|
|
||||||
|
#endif // ABSL_PROFILING_INTERNAL_SAMPLE_RECORDER_H_
|
@ -0,0 +1,171 @@ |
|||||||
|
// Copyright 2018 The Abseil Authors.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
//
|
||||||
|
// https://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 "absl/profiling/internal/sample_recorder.h" |
||||||
|
|
||||||
|
#include <atomic> |
||||||
|
#include <random> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
#include "gmock/gmock.h" |
||||||
|
#include "absl/base/thread_annotations.h" |
||||||
|
#include "absl/synchronization/internal/thread_pool.h" |
||||||
|
#include "absl/synchronization/mutex.h" |
||||||
|
#include "absl/synchronization/notification.h" |
||||||
|
#include "absl/time/time.h" |
||||||
|
|
||||||
|
namespace absl { |
||||||
|
ABSL_NAMESPACE_BEGIN |
||||||
|
namespace profiling_internal { |
||||||
|
|
||||||
|
namespace { |
||||||
|
using ::absl::synchronization_internal::ThreadPool; |
||||||
|
using ::testing::IsEmpty; |
||||||
|
using ::testing::UnorderedElementsAre; |
||||||
|
|
||||||
|
struct Info : public Sample<Info> { |
||||||
|
public: |
||||||
|
void PrepareForSampling() {} |
||||||
|
std::atomic<size_t> size; |
||||||
|
absl::Time create_time; |
||||||
|
}; |
||||||
|
|
||||||
|
std::vector<size_t> GetSizes(SampleRecorder<Info>* s) { |
||||||
|
std::vector<size_t> res; |
||||||
|
s->Iterate([&](const Info& info) { |
||||||
|
res.push_back(info.size.load(std::memory_order_acquire)); |
||||||
|
}); |
||||||
|
return res; |
||||||
|
} |
||||||
|
|
||||||
|
Info* Register(SampleRecorder<Info>* s, size_t size) { |
||||||
|
auto* info = s->Register(); |
||||||
|
assert(info != nullptr); |
||||||
|
info->size.store(size); |
||||||
|
return info; |
||||||
|
} |
||||||
|
|
||||||
|
TEST(SampleRecorderTest, Registration) { |
||||||
|
SampleRecorder<Info> sampler; |
||||||
|
auto* info1 = Register(&sampler, 1); |
||||||
|
EXPECT_THAT(GetSizes(&sampler), UnorderedElementsAre(1)); |
||||||
|
|
||||||
|
auto* info2 = Register(&sampler, 2); |
||||||
|
EXPECT_THAT(GetSizes(&sampler), UnorderedElementsAre(1, 2)); |
||||||
|
info1->size.store(3); |
||||||
|
EXPECT_THAT(GetSizes(&sampler), UnorderedElementsAre(3, 2)); |
||||||
|
|
||||||
|
sampler.Unregister(info1); |
||||||
|
sampler.Unregister(info2); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(SampleRecorderTest, Unregistration) { |
||||||
|
SampleRecorder<Info> sampler; |
||||||
|
std::vector<Info*> infos; |
||||||
|
for (size_t i = 0; i < 3; ++i) { |
||||||
|
infos.push_back(Register(&sampler, i)); |
||||||
|
} |
||||||
|
EXPECT_THAT(GetSizes(&sampler), UnorderedElementsAre(0, 1, 2)); |
||||||
|
|
||||||
|
sampler.Unregister(infos[1]); |
||||||
|
EXPECT_THAT(GetSizes(&sampler), UnorderedElementsAre(0, 2)); |
||||||
|
|
||||||
|
infos.push_back(Register(&sampler, 3)); |
||||||
|
infos.push_back(Register(&sampler, 4)); |
||||||
|
EXPECT_THAT(GetSizes(&sampler), UnorderedElementsAre(0, 2, 3, 4)); |
||||||
|
sampler.Unregister(infos[3]); |
||||||
|
EXPECT_THAT(GetSizes(&sampler), UnorderedElementsAre(0, 2, 4)); |
||||||
|
|
||||||
|
sampler.Unregister(infos[0]); |
||||||
|
sampler.Unregister(infos[2]); |
||||||
|
sampler.Unregister(infos[4]); |
||||||
|
EXPECT_THAT(GetSizes(&sampler), IsEmpty()); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(SampleRecorderTest, MultiThreaded) { |
||||||
|
SampleRecorder<Info> sampler; |
||||||
|
Notification stop; |
||||||
|
ThreadPool pool(10); |
||||||
|
|
||||||
|
for (int i = 0; i < 10; ++i) { |
||||||
|
pool.Schedule([&sampler, &stop]() { |
||||||
|
std::random_device rd; |
||||||
|
std::mt19937 gen(rd()); |
||||||
|
|
||||||
|
std::vector<Info*> infoz; |
||||||
|
while (!stop.HasBeenNotified()) { |
||||||
|
if (infoz.empty()) { |
||||||
|
infoz.push_back(sampler.Register()); |
||||||
|
} |
||||||
|
switch (std::uniform_int_distribution<>(0, 2)(gen)) { |
||||||
|
case 0: { |
||||||
|
infoz.push_back(sampler.Register()); |
||||||
|
break; |
||||||
|
} |
||||||
|
case 1: { |
||||||
|
size_t p = |
||||||
|
std::uniform_int_distribution<>(0, infoz.size() - 1)(gen); |
||||||
|
Info* info = infoz[p]; |
||||||
|
infoz[p] = infoz.back(); |
||||||
|
infoz.pop_back(); |
||||||
|
sampler.Unregister(info); |
||||||
|
break; |
||||||
|
} |
||||||
|
case 2: { |
||||||
|
absl::Duration oldest = absl::ZeroDuration(); |
||||||
|
sampler.Iterate([&](const Info& info) { |
||||||
|
oldest = std::max(oldest, absl::Now() - info.create_time); |
||||||
|
}); |
||||||
|
ASSERT_GE(oldest, absl::ZeroDuration()); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
// The threads will hammer away. Give it a little bit of time for tsan to
|
||||||
|
// spot errors.
|
||||||
|
absl::SleepFor(absl::Seconds(3)); |
||||||
|
stop.Notify(); |
||||||
|
} |
||||||
|
|
||||||
|
TEST(SampleRecorderTest, Callback) { |
||||||
|
SampleRecorder<Info> sampler; |
||||||
|
|
||||||
|
auto* info1 = Register(&sampler, 1); |
||||||
|
auto* info2 = Register(&sampler, 2); |
||||||
|
|
||||||
|
static const Info* expected; |
||||||
|
|
||||||
|
auto callback = [](const Info& info) { |
||||||
|
// We can't use `info` outside of this callback because the object will be
|
||||||
|
// disposed as soon as we return from here.
|
||||||
|
EXPECT_EQ(&info, expected); |
||||||
|
}; |
||||||
|
|
||||||
|
// Set the callback.
|
||||||
|
EXPECT_EQ(sampler.SetDisposeCallback(callback), nullptr); |
||||||
|
expected = info1; |
||||||
|
sampler.Unregister(info1); |
||||||
|
|
||||||
|
// Unset the callback.
|
||||||
|
EXPECT_EQ(callback, sampler.SetDisposeCallback(nullptr)); |
||||||
|
expected = nullptr; // no more calls.
|
||||||
|
sampler.Unregister(info2); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace profiling_internal
|
||||||
|
ABSL_NAMESPACE_END |
||||||
|
} // namespace absl
|
Loading…
Reference in new issue