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
base / profiler / metadata_recorder.cc [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.
#include "base/profiler/metadata_recorder.h"
#include <optional>
#include "base/metrics/histogram_macros.h"
namespace base {
const size_t MetadataRecorder::MAX_METADATA_COUNT;
MetadataRecorder::Item::Item(uint64_t name_hash,
std::optional<int64_t> key,
std::optional<PlatformThreadId> thread_id,
int64_t value)
: name_hash(name_hash), key(key), thread_id(thread_id), value(value) {}
MetadataRecorder::Item::Item() : name_hash(0), value(0) {}
MetadataRecorder::Item::Item(const Item& other) = default;
MetadataRecorder::Item& MetadataRecorder::Item::Item::operator=(
const Item& other) = default;
MetadataRecorder::ItemInternal::ItemInternal() = default;
MetadataRecorder::ItemInternal::~ItemInternal() = default;
MetadataRecorder::MetadataRecorder() {
// Ensure that we have necessary atomic support.
DCHECK(items_[0].is_active.is_lock_free());
DCHECK(items_[0].value.is_lock_free());
}
MetadataRecorder::~MetadataRecorder() = default;
void MetadataRecorder::Set(uint64_t name_hash,
std::optional<int64_t> key,
std::optional<PlatformThreadId> thread_id,
int64_t value) {
AutoLock lock(write_lock_);
// Acquiring the |write_lock_| ensures that:
//
// - We don't try to write into the same new slot at the same time as
// another thread
// - We see all writes by other threads (acquiring a mutex implies acquire
// semantics)
size_t item_slots_used = item_slots_used_.load(std::memory_order_relaxed);
for (size_t i = 0; i < item_slots_used; ++i) {
auto& item = items_[i];
if (item.name_hash == name_hash && item.key == key &&
item.thread_id == thread_id) {
item.value.store(value, std::memory_order_relaxed);
const bool was_active =
item.is_active.exchange(true, std::memory_order_release);
if (!was_active)
inactive_item_count_--;
return;
}
}
item_slots_used = TryReclaimInactiveSlots(item_slots_used);
if (item_slots_used == items_.size()) {
// The metadata recorder is full, forcing us to drop this metadata. The
// above UMA histogram counting occupied metadata slots should help us set a
// max size that avoids this condition during normal Chrome use.
return;
}
// Wait until the item is fully created before setting |is_active| to true and
// incrementing |item_slots_used_|, which will signal to readers that the item
// is ready.
auto& item = items_[item_slots_used];
item.name_hash = name_hash;
item.key = key;
item.thread_id = thread_id;
item.value.store(value, std::memory_order_relaxed);
item.is_active.store(true, std::memory_order_release);
item_slots_used_.fetch_add(1, std::memory_order_release);
}
void MetadataRecorder::Remove(uint64_t name_hash,
std::optional<int64_t> key,
std::optional<PlatformThreadId> thread_id) {
AutoLock lock(write_lock_);
size_t item_slots_used = item_slots_used_.load(std::memory_order_relaxed);
for (size_t i = 0; i < item_slots_used; ++i) {
auto& item = items_[i];
if (item.name_hash == name_hash && item.key == key &&
item.thread_id == thread_id) {
// A removed item will occupy its slot until that slot is reclaimed.
const bool was_active =
item.is_active.exchange(false, std::memory_order_relaxed);
if (was_active)
inactive_item_count_++;
return;
}
}
}
MetadataRecorder::MetadataProvider::MetadataProvider(
MetadataRecorder* metadata_recorder,
PlatformThreadId thread_id)
: metadata_recorder_(metadata_recorder),
thread_id_(thread_id),
auto_lock_(metadata_recorder->read_lock_) {}
MetadataRecorder::MetadataProvider::~MetadataProvider() = default;
size_t MetadataRecorder::MetadataProvider::GetItems(
ItemArray* const items) const {
return metadata_recorder_->GetItems(items, thread_id_);
}
size_t MetadataRecorder::GetItems(ItemArray* const items,
PlatformThreadId thread_id) const {
// If a writer adds a new item after this load, it will be ignored. We do
// this instead of calling item_slots_used_.load() explicitly in the for loop
// bounds checking, which would be expensive.
//
// Also note that items are snapshotted sequentially and that items can be
// modified mid-snapshot by non-suspended threads. This means that there's a
// small chance that some items, especially those that occur later in the
// array, may have values slightly "in the future" from when the sample was
// actually collected. It also means that the array as returned may have never
// existed in its entirety, although each name/value pair represents a
// consistent item that existed very shortly after the thread was supended.
size_t item_slots_used = item_slots_used_.load(std::memory_order_acquire);
size_t write_index = 0;
for (size_t read_index = 0; read_index < item_slots_used; ++read_index) {
const auto& item = items_[read_index];
// Because we wait until |is_active| is set to consider an item active and
// that field is always set last, we ignore half-created items.
if (item.is_active.load(std::memory_order_acquire) &&
(!item.thread_id.has_value() || item.thread_id == thread_id)) {
(*items)[write_index++] =
Item{item.name_hash, item.key, item.thread_id,
item.value.load(std::memory_order_relaxed)};
}
}
return write_index;
}
size_t MetadataRecorder::TryReclaimInactiveSlots(size_t item_slots_used) {
const size_t remaining_slots = MAX_METADATA_COUNT - item_slots_used;
if (inactive_item_count_ == 0 || inactive_item_count_ < remaining_slots) {
// This reclaiming threshold has a few nice properties:
//
// - It avoids reclaiming when no items have been removed
// - It makes doing so more likely as free slots become more scarce
// - It makes doing so less likely when the benefits are lower
return item_slots_used;
}
if (read_lock_.Try()) {
// The lock isn't already held by a reader or another thread reclaiming
// slots.
item_slots_used = ReclaimInactiveSlots(item_slots_used);
read_lock_.Release();
}
return item_slots_used;
}
size_t MetadataRecorder::ReclaimInactiveSlots(size_t item_slots_used) {
// From here until the end of the reclamation, we can safely use
// memory_order_relaxed for all reads and writes. We don't need
// memory_order_acquire because acquiring the write mutex gives acquire
// semantics and no other threads can write after we hold that mutex. We don't
// need memory_order_release because no readers can read until we release the
// read mutex, which itself has release semantics.
size_t first_inactive_item_idx = 0;
size_t last_active_item_idx = item_slots_used - 1;
while (first_inactive_item_idx < last_active_item_idx) {
ItemInternal& inactive_item = items_[first_inactive_item_idx];
ItemInternal& active_item = items_[last_active_item_idx];
if (inactive_item.is_active.load(std::memory_order_relaxed)) {
// Keep seeking forward to an inactive item.
++first_inactive_item_idx;
continue;
}
if (!active_item.is_active.load(std::memory_order_relaxed)) {
// Keep seeking backward to an active item. Skipping over this item
// indicates that we're freeing the slot at this index.
--last_active_item_idx;
item_slots_used--;
continue;
}
inactive_item.name_hash = active_item.name_hash;
inactive_item.value.store(active_item.value.load(std::memory_order_relaxed),
std::memory_order_relaxed);
inactive_item.is_active.store(true, std::memory_order_relaxed);
++first_inactive_item_idx;
--last_active_item_idx;
item_slots_used--;
}
item_slots_used_.store(item_slots_used, std::memory_order_relaxed);
return item_slots_used;
}
} // namespace base