blob: b0f8c3628e8b1d6b5f5230cc2ccdd530f3f6ee7b [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef BASE_PROFILER_METADATA_RECORDER_H_
#define BASE_PROFILER_METADATA_RECORDER_H_
#include <array>
#include <atomic>
#include <utility>
#include "base/base_export.h"
#include "base/memory/raw_ptr.h"
#include "base/synchronization/lock.h"
#include "base/thread_annotations.h"
#include "base/threading/platform_thread.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace base {
// MetadataRecorder provides a data structure to store metadata key/value pairs
// to be associated with samples taken by the sampling profiler. Whatever
// metadata is present in this map when a sample is recorded is then associated
// with the sample.
//
// Methods on this class are safe to call unsynchronized from arbitrary threads.
//
// This class was designed to read metadata from a single sampling thread and
// write metadata from many Chrome threads within the same process. These other
// threads might be suspended by the sampling thread at any time in order to
// collect a sample.
//
// This class has a few notable constraints:
//
// A) If a lock that's required to read the metadata might be held while writing
// the metadata, that lock must be acquirable *before* the thread is
// suspended. Otherwise, the sampling thread might suspend the target thread
// while it is holding the required lock, causing deadlock.
//
// Ramifications:
//
// - When retrieving items, lock acquisition (through
// CreateMetadataProvider()) and actual item retrieval (through
// MetadataProvider::GetItems()) are separate.
//
// B) We can't allocate data on the heap while reading the metadata items. This
// is because, on many operating systems, there's a process-wide heap lock
// that is held while allocating on the heap. If a thread is suspended while
// holding this lock and the sampling thread then tries to allocate on the
// heap to read the metadata, it will deadlock trying to acquire the heap
// lock.
//
// Ramifications:
//
// - We hold and retrieve the metadata using a fixed-size array, which
// allows readers to preallocate the data structure that we pass back
// the metadata in.
//
// C) We shouldn't guard writes with a lock that also guards reads, since the
// read lock is held from the time that the sampling thread requests that a
// thread be suspended up to the time that the thread is resumed. If all
// metadata writes block their thread during that time, we're very likely to
// block all Chrome threads.
//
// Ramifications:
//
// - We use two locks to guard the metadata: a read lock and a write
// lock. Only the write lock is required to write into the metadata, and
// only the read lock is required to read the metadata.
//
// - Because we can't guard reads and writes with the same lock, we have to
// face the possibility of writes occurring during a read. This is
// especially problematic because there's no way to read both the key and
// value for an item atomically without using mutexes, which violates
// constraint A). If the sampling thread were to see the following
// interleaving of reads and writes:
//
// * Reader thread reads key for slot 0
// * Writer thread removes item at slot 0
// * Writer thread creates new item with different key in slot 0
// * Reader thread reads value for slot 0
//
// then the reader would see an invalid value for the given key. Because
// of this possibility, we keep slots reserved for a specific key even
// after that item has been removed. We reclaim these slots on a
// best-effort basis during writes when the metadata recorder has become
// sufficiently full and we can acquire the read lock.
//
// - We use state stored in atomic data types to ensure that readers and
// writers are synchronized about where data should be written to and
// read from. We must use atomic data types to guarantee that there's no
// instruction during a write after which the recorder is in an
// inconsistent state that might yield garbage data for a reader.
//
// Here are a few of the many states the recorder can be in:
//
// - No thread is using the recorder.
//
// - A single writer is writing into the recorder without a simultaneous read.
// The write will succeed.
//
// - A reader is reading from the recorder without a simultaneous write. The
// read will succeed.
//
// - Multiple writers attempt to write into the recorder simultaneously. All
// writers but one will block because only one can hold the write lock.
//
// - A writer is writing into the recorder, which hasn't reached the threshold
// at which it will try to reclaim inactive slots. The writer won't try to
// acquire the read lock to reclaim inactive slots. The reader will therefore
// be able to immediately acquire the read lock, suspend the target thread,
// and read the metadata.
//
// - A writer is writing into the recorder, the recorder has reached the
// threshold at which it needs to reclaim inactive slots, and the writer
// thread is now in the middle of reclaiming those slots when a reader
// arrives. The reader will try to acquire the read lock before suspending the
// thread but will block until the writer thread finishes reclamation and
// releases the read lock. The reader will then be able to acquire the read
// lock and suspend the target thread.
//
// - A reader is reading the recorder when a writer attempts to write. The write
// will be successful. However, if the writer deems it necessary to reclaim
// inactive slots, it will skip doing so because it won't be able to acquire
// the read lock.
class BASE_EXPORT MetadataRecorder {
public:
MetadataRecorder();
virtual ~MetadataRecorder();
MetadataRecorder(const MetadataRecorder&) = delete;
MetadataRecorder& operator=(const MetadataRecorder&) = delete;
struct BASE_EXPORT Item {
Item(uint64_t name_hash,
absl::optional<int64_t> key,
absl::optional<PlatformThreadId> thread_id,
int64_t value);
Item();
Item(const Item& other);
Item& operator=(const Item& other);
// The hash of the metadata name, as produced by HashMetricName().
uint64_t name_hash;
// The key if specified when setting the item.
absl::optional<int64_t> key;
// The thread_id is captured from the current thread for a thread-scoped
// item.
absl::optional<PlatformThreadId> thread_id;
// The value of the metadata item.
int64_t value;
};
static constexpr size_t MAX_METADATA_COUNT = 50;
typedef std::array<Item, MAX_METADATA_COUNT> ItemArray;
// Sets a value for a (|name_hash|, |key|, |thread_id|) tuple, overwriting any
// value previously set for the tuple. Nullopt keys are treated as just
// another key state for the purpose of associating values.
void Set(uint64_t name_hash,
absl::optional<int64_t> key,
absl::optional<PlatformThreadId> thread_id,
int64_t value);
// Removes the item with the specified name hash and optional key. Has no
// effect if such an item does not exist.
void Remove(uint64_t name_hash,
absl::optional<int64_t> key,
absl::optional<PlatformThreadId> thread_id);
// An object that provides access to a MetadataRecorder's items and holds the
// necessary exclusive read lock until the object is destroyed. Reclaiming of
// inactive slots in the recorder can't occur while this object lives, so it
// should be created as soon before it's needed as possible and released as
// soon as possible.
//
// This object should be created *before* suspending the target thread and
// destroyed after resuming the target thread. Otherwise, that thread might be
// suspended while reclaiming inactive slots and holding the read lock, which
// would cause the sampling thread to deadlock.
//
// Example usage:
//
// MetadataRecorder r;
// base::MetadataRecorder::ItemArray arr;
// size_t item_count;
// ...
// {
// MetadtaRecorder::MetadataProvider provider;
// item_count = provider.GetItems(arr);
// }
class SCOPED_LOCKABLE BASE_EXPORT MetadataProvider {
public:
// Acquires an exclusive read lock on the metadata recorder which is held
// until the object is destroyed.
explicit MetadataProvider(MetadataRecorder* metadata_recorder,
PlatformThreadId thread_id)
EXCLUSIVE_LOCK_FUNCTION(metadata_recorder_->read_lock_);
~MetadataProvider() UNLOCK_FUNCTION();
MetadataProvider(const MetadataProvider&) = delete;
MetadataProvider& operator=(const MetadataProvider&) = delete;
// Retrieves the first |available_slots| items in the metadata recorder for
// |thread_id| and copies them into |items|, returning the number of
// metadata items that were copied. To ensure that all items can be copied,
// |available slots| should be greater than or equal to
// |MAX_METADATA_COUNT|. Requires NO_THREAD_SAFETY_ANALYSIS because clang's
// analyzer doesn't understand the cross-class locking used in this class'
// implementation.
size_t GetItems(ItemArray* const items) const NO_THREAD_SAFETY_ANALYSIS;
private:
const raw_ptr<const MetadataRecorder> metadata_recorder_;
PlatformThreadId thread_id_;
base::AutoLock auto_lock_;
};
private:
// TODO(charliea): Support large quantities of metadata efficiently.
struct ItemInternal {
ItemInternal();
~ItemInternal();
// Indicates whether the metadata item is still active (i.e. not removed).
//
// Requires atomic reads and writes to avoid word tearing when reading and
// writing unsynchronized. Requires acquire/release semantics to ensure that
// the other state in this struct is visible to the reading thread before it
// is marked as active.
std::atomic<bool> is_active{false};
// Neither name_hash, key or thread_id require atomicity or memory order
// constraints because no reader will attempt to read them mid-write.
// Specifically, readers wait until |is_active| is true to read them.
// Because |is_active| is always stored with a memory_order_release fence,
// we're guaranteed that |name_hash|, |key| and |thread_id| will be finished
// writing before |is_active| is set to true.
uint64_t name_hash;
absl::optional<int64_t> key;
absl::optional<PlatformThreadId> thread_id;
// Requires atomic reads and writes to avoid word tearing when updating an
// existing item unsynchronized. Does not require acquire/release semantics
// because we rely on the |is_active| acquire/release semantics to ensure
// that an item is fully created before we attempt to read it.
std::atomic<int64_t> value;
};
// Attempts to free slots in the metadata map that are currently allocated to
// inactive items. May fail silently if the read lock is already held, in
// which case no slots will be freed. Returns the number of item slots used
// after the reclamation.
size_t TryReclaimInactiveSlots(size_t item_slots_used)
EXCLUSIVE_LOCKS_REQUIRED(write_lock_) LOCKS_EXCLUDED(read_lock_);
// Updates item_slots_used_ to reflect the new item count and returns the
// number of item slots used after the reclamation.
size_t ReclaimInactiveSlots(size_t item_slots_used)
EXCLUSIVE_LOCKS_REQUIRED(write_lock_)
EXCLUSIVE_LOCKS_REQUIRED(read_lock_);
// Retrieves items in the metadata recorder that are active for |thread_id|
// and copies them into |items|, returning the number of metadata items that
// were copied.
size_t GetItems(ItemArray* const items, PlatformThreadId thread_id) const
EXCLUSIVE_LOCKS_REQUIRED(read_lock_);
// Metadata items that the recorder has seen. Rather than implementing the
// metadata recorder as a dense array, we implement it as a sparse array where
// removed metadata items keep their slot with their |is_active| bit set to
// false. This avoids race conditions caused by reusing slots that might
// otherwise cause mismatches between metadata name hashes and values.
//
// For the rationale behind this design (along with others considered), see
// https://docs.google.com/document/d/18shLhVwuFbLl_jKZxCmOfRB98FmNHdKl0yZZZ3aEO4U/edit#.
std::array<ItemInternal, MAX_METADATA_COUNT> items_;
// The number of item slots used in the metadata map.
//
// Requires atomic reads and writes to avoid word tearing when reading and
// writing unsynchronized. Requires acquire/release semantics to ensure that a
// newly-allocated slot is fully initialized before the reader becomes aware
// of its existence.
std::atomic<size_t> item_slots_used_{0};
// The number of item slots occupied by inactive items.
size_t inactive_item_count_ GUARDED_BY(write_lock_) = 0;
// A lock that guards against multiple threads trying to manipulate items_,
// item_slots_used_, or inactive_item_count_ at the same time.
base::Lock write_lock_;
// A lock that guards against a reader trying to read items_ while inactive
// slots are being reclaimed.
base::Lock read_lock_;
};
} // namespace base
#endif // BASE_PROFILER_METADATA_RECORDER_H_