1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
base / profiler / metadata_recorder.h [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 <optional>
#include <utility>
#include "base/base_export.h"
#include "base/memory/stack_allocated.h"
#include "base/synchronization/lock.h"
#include "base/thread_annotations.h"
#include "base/threading/platform_thread.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,
std::optional<int64_t> key,
std::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.
std::optional<int64_t> key;
// The thread_id is captured from the current thread for a thread-scoped
// item.
std::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,
std::optional<int64_t> key,
std::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,
std::optional<int64_t> key,
std::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 {
STACK_ALLOCATED();
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 MetadataRecorder* const 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;
std::optional<int64_t> key;
std::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_