1 // Copyright 2019 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "components/metrics/call_stacks/call_stack_profile_metadata.h"
6
7 #include <iterator>
8 #include <tuple>
9
10 #include "base/ranges/algorithm.h"
11
12 namespace metrics {
13
14 namespace {
15
16 class MatchesNameHashIndexAndKey {
17 public:
MatchesNameHashIndexAndKey(int name_hash_index,std::optional<int64_t> key)18 MatchesNameHashIndexAndKey(int name_hash_index, std::optional<int64_t> key)
19 : name_hash_index_(name_hash_index), key_(key) {}
20
operator ()(const CallStackProfile::MetadataItem & item) const21 bool operator()(const CallStackProfile::MetadataItem& item) const {
22 std::optional<int64_t> item_key_as_optional =
23 item.has_key() ? item.key() : std::optional<int64_t>();
24 return item.name_hash_index() == name_hash_index_ &&
25 key_ == item_key_as_optional;
26 }
27
28 private:
29 int name_hash_index_;
30 std::optional<int64_t> key_;
31 };
32
33 // Finds the last value for a prior metadata application with |name_hash_index|
34 // and |key| from |begin| that was still active at |end|. Returns nullopt if no
35 // such application exists.
FindLastOpenEndedMetadataValue(int name_hash_index,std::optional<int64_t> key,google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>::iterator begin,google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>::iterator end)36 std::optional<int64_t> FindLastOpenEndedMetadataValue(
37 int name_hash_index,
38 std::optional<int64_t> key,
39 google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>::iterator
40 begin,
41 google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>::iterator
42 end) {
43 // Search the samples backward from the end of the range, looking for an
44 // application of the same metadata name hash/key that doesn't have a
45 // corresponding removal.
46 const auto rbegin = std::make_reverse_iterator(end);
47 const auto rend = std::make_reverse_iterator(begin);
48 for (auto it = rbegin; it != rend; ++it) {
49 auto item = base::ranges::find_if(
50 it->metadata(), MatchesNameHashIndexAndKey(name_hash_index, key));
51
52 if (item == it->metadata().end()) {
53 // The sample does not contain a matching item.
54 continue;
55 }
56
57 if (!item->has_value()) {
58 // A matching item was previously applied, but stopped being applied
59 // before the last sample in the range.
60 return std::nullopt;
61 }
62
63 // Else, a matching item was applied at this sample.
64 return item->value();
65 }
66
67 // No matching items were previously applied.
68 return std::nullopt;
69 }
70
71 // Clears any existing metadata changes between |begin| and |end|.
ClearExistingMetadata(const int name_hash_index,std::optional<int64_t> key,google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>::iterator begin,google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>::iterator end)72 void ClearExistingMetadata(
73 const int name_hash_index,
74 std::optional<int64_t> key,
75 google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>::iterator
76 begin,
77 google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>::iterator
78 end) {
79 for (auto it = begin; it != end; ++it) {
80 google::protobuf::RepeatedPtrField<CallStackProfile::MetadataItem>*
81 metadata = it->mutable_metadata();
82 metadata->erase(
83 std::remove_if(metadata->begin(), metadata->end(),
84 MatchesNameHashIndexAndKey(name_hash_index, key)),
85 metadata->end());
86 }
87 }
88
89 // Sets the state of |item| to the provided values.
SetMetadataItem(int name_hash_index,std::optional<int64_t> key,std::optional<int64_t> value,CallStackProfile::MetadataItem * item)90 void SetMetadataItem(int name_hash_index,
91 std::optional<int64_t> key,
92 std::optional<int64_t> value,
93 CallStackProfile::MetadataItem* item) {
94 item->set_name_hash_index(name_hash_index);
95 if (key.has_value())
96 item->set_key(*key);
97 if (value.has_value())
98 item->set_value(*value);
99 }
100
101 } // namespace
102
103 CallStackProfileMetadata::CallStackProfileMetadata() = default;
104
105 CallStackProfileMetadata::~CallStackProfileMetadata() = default;
106
107 // This function is invoked on the profiler thread while the target thread is
108 // suspended so must not take any locks, including indirectly through use of
109 // heap allocation, LOG, CHECK, or DCHECK.
RecordMetadata(const base::MetadataRecorder::MetadataProvider & metadata_provider)110 void CallStackProfileMetadata::RecordMetadata(
111 const base::MetadataRecorder::MetadataProvider& metadata_provider) {
112 metadata_item_count_ = metadata_provider.GetItems(&metadata_items_);
113 }
114
115 google::protobuf::RepeatedPtrField<CallStackProfile::MetadataItem>
CreateSampleMetadata(google::protobuf::RepeatedField<uint64_t> * metadata_name_hashes)116 CallStackProfileMetadata::CreateSampleMetadata(
117 google::protobuf::RepeatedField<uint64_t>* metadata_name_hashes) {
118 DCHECK_EQ(metadata_hashes_cache_.size(),
119 static_cast<size_t>(metadata_name_hashes->size()));
120
121 google::protobuf::RepeatedPtrField<CallStackProfile::MetadataItem>
122 metadata_items;
123 MetadataMap current_items =
124 CreateMetadataMap(metadata_items_, metadata_item_count_);
125
126 for (auto item :
127 GetNewOrModifiedMetadataItems(current_items, previous_items_)) {
128 size_t name_hash_index =
129 MaybeAppendNameHash(item.first.name_hash, metadata_name_hashes);
130
131 CallStackProfile::MetadataItem* profile_item = metadata_items.Add();
132 profile_item->set_name_hash_index(name_hash_index);
133 if (item.first.key.has_value())
134 profile_item->set_key(*item.first.key);
135 profile_item->set_value(item.second);
136 }
137
138 for (auto item : GetDeletedMetadataItems(current_items, previous_items_)) {
139 size_t name_hash_index =
140 MaybeAppendNameHash(item.first.name_hash, metadata_name_hashes);
141
142 CallStackProfile::MetadataItem* profile_item = metadata_items.Add();
143 profile_item->set_name_hash_index(name_hash_index);
144 if (item.first.key.has_value())
145 profile_item->set_key(*item.first.key);
146 // Leave the value empty to indicate that the item was deleted.
147 }
148
149 previous_items_ = std::move(current_items);
150 metadata_item_count_ = 0;
151
152 return metadata_items;
153 }
154
ApplyMetadata(const base::MetadataRecorder::Item & item,google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>::iterator begin,google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>::iterator end,google::protobuf::RepeatedPtrField<CallStackProfile::StackSample> * stack_samples,google::protobuf::RepeatedField<uint64_t> * metadata_name_hashes)155 void CallStackProfileMetadata::ApplyMetadata(
156 const base::MetadataRecorder::Item& item,
157 google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>::iterator
158 begin,
159 google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>::iterator
160 end,
161 google::protobuf::RepeatedPtrField<CallStackProfile::StackSample>*
162 stack_samples,
163 google::protobuf::RepeatedField<uint64_t>* metadata_name_hashes) {
164 if (begin == end)
165 return;
166
167 // This function works by finding the previously effective metadata values
168 // with the same name hash and key at the begin and end of the range. The
169 // range is then cleared of any metadata changes (with the same name hash and
170 // key) in preparation for recording the metadata application at the start of
171 // the range and the removal at the end of the range.
172 //
173 // We expect this function to be called at most a few times per collection, so
174 // it's written to be readable rather than optimally performant. If
175 // performance becomes a concern, the two FindLastOpenEndedMetadataValue()
176 // calls can be merged into one to avoid one loop over the samples, or a more
177 // efficient representation of when metadata is set/unset could be used to
178 // avoid the looping entirely.
179
180 const size_t name_hash_index =
181 MaybeAppendNameHash(item.name_hash, metadata_name_hashes);
182
183 // The previously set metadata value immediately prior to begin, or nullopt if
184 // none.
185 const std::optional<int64_t> previous_value_before_begin =
186 FindLastOpenEndedMetadataValue(name_hash_index, item.key,
187 stack_samples->begin(), begin);
188
189 // The end of the range will be in one of two states: terminating before the
190 // last recorded sample, or terminating at the last recorded sample. If it
191 // terminates before the last recorded sample then we are able to record the
192 // removal of the metadata on the sample following the end of the
193 // range. Otherwise we have to wait for the next recorded sample to record the
194 // removal of the metadata.
195 const bool range_terminates_at_last_sample = end == stack_samples->end();
196
197 // The previously set metadata value at *end (or the one to be set on the next
198 // sample if range_terminates_at_last_sample).
199 const std::optional<int64_t> previous_value_at_end =
200 FindLastOpenEndedMetadataValue(
201 name_hash_index, item.key, stack_samples->begin(),
202 // If a sample past the end exists check its value as well, since
203 // we'll be overwriting that sample's metadata below.
204 (range_terminates_at_last_sample ? end : end + 1));
205
206 ClearExistingMetadata(name_hash_index, item.key, begin,
207 (range_terminates_at_last_sample ? end : end + 1));
208
209 // Enable the metadata on the initial sample if different than the previous
210 // state.
211 if (!previous_value_before_begin.has_value() ||
212 *previous_value_before_begin != item.value) {
213 SetMetadataItem(name_hash_index, item.key, item.value,
214 begin->mutable_metadata()->Add());
215 }
216
217 if (range_terminates_at_last_sample) {
218 // We note that the metadata item that was set for the last sample in
219 // previous_items_ to enable computing the appropriate deltas from it on
220 // the next recorded sample.
221 previous_items_[MetadataKey(item.name_hash, item.key)] = item.value;
222 } else if (!previous_value_at_end.has_value() ||
223 *previous_value_at_end != item.value) {
224 // A sample exists past the end of the range so we can use that sample to
225 // record the end of the metadata application. We do so if there was no
226 // previous value at the end or it was different than what we set at begin.
227 SetMetadataItem(name_hash_index, item.key,
228 // If we had a previously set item at the end of the range,
229 // set its value. Otherwise leave the value empty to denote
230 // that it is being unset.
231 previous_value_at_end.has_value()
232 ? *previous_value_at_end
233 : std::optional<int64_t>(),
234 end->mutable_metadata()->Add());
235 }
236 }
237
SetMetadata(const base::MetadataRecorder::Item & src_item,CallStackProfile::MetadataItem * dest_item,google::protobuf::RepeatedField<uint64_t> * metadata_name_hashes)238 void CallStackProfileMetadata::SetMetadata(
239 const base::MetadataRecorder::Item& src_item,
240 CallStackProfile::MetadataItem* dest_item,
241 google::protobuf::RepeatedField<uint64_t>* metadata_name_hashes) {
242 const size_t name_hash_index =
243 MaybeAppendNameHash(src_item.name_hash, metadata_name_hashes);
244
245 SetMetadataItem(name_hash_index, src_item.key, src_item.value, dest_item);
246 }
247
operator ()(const MetadataKey & a,const MetadataKey & b) const248 bool CallStackProfileMetadata::MetadataKeyCompare::operator()(
249 const MetadataKey& a,
250 const MetadataKey& b) const {
251 return std::tie(a.name_hash, a.key) < std::tie(b.name_hash, b.key);
252 }
253
MetadataKey(uint64_t name_hash,std::optional<int64_t> key)254 CallStackProfileMetadata::MetadataKey::MetadataKey(uint64_t name_hash,
255 std::optional<int64_t> key)
256 : name_hash(name_hash), key(key) {}
257
258 CallStackProfileMetadata::MetadataKey::MetadataKey(const MetadataKey& other) =
259 default;
260 CallStackProfileMetadata::MetadataKey& CallStackProfileMetadata::MetadataKey::
261 operator=(const MetadataKey& other) = default;
262
263 CallStackProfileMetadata::MetadataMap
CreateMetadataMap(base::MetadataRecorder::ItemArray items,size_t item_count)264 CallStackProfileMetadata::CreateMetadataMap(
265 base::MetadataRecorder::ItemArray items,
266 size_t item_count) {
267 MetadataMap item_map;
268 for (size_t i = 0; i < item_count; ++i)
269 item_map[MetadataKey{items[i].name_hash, items[i].key}] = items[i].value;
270 return item_map;
271 }
272
273 CallStackProfileMetadata::MetadataMap
GetNewOrModifiedMetadataItems(const MetadataMap & current_items,const MetadataMap & previous_items)274 CallStackProfileMetadata::GetNewOrModifiedMetadataItems(
275 const MetadataMap& current_items,
276 const MetadataMap& previous_items) {
277 MetadataMap new_or_modified_items;
278 // Find the new or modified items by subtracting any previous items that are
279 // exactly the same as the current items (i.e. equal in key *and* value).
280 auto key_and_value_comparator = [](const std::pair<MetadataKey, int64_t>& a,
281 const std::pair<MetadataKey, int64_t>& b) {
282 return std::tie(a.first.name_hash, a.first.key, a.second) <
283 std::tie(b.first.name_hash, b.first.key, b.second);
284 };
285 std::set_difference(
286 current_items.begin(), current_items.end(), previous_items.begin(),
287 previous_items.end(),
288 std::inserter(new_or_modified_items, new_or_modified_items.begin()),
289 key_and_value_comparator);
290 return new_or_modified_items;
291 }
292
293 CallStackProfileMetadata::MetadataMap
GetDeletedMetadataItems(const MetadataMap & current_items,const MetadataMap & previous_items)294 CallStackProfileMetadata::GetDeletedMetadataItems(
295 const MetadataMap& current_items,
296 const MetadataMap& previous_items) {
297 MetadataMap deleted_items;
298 // Find the deleted metadata items by subtracting the current items from the
299 // previous items, comparing items solely map key (as opposed to map key and
300 // value as in GetNewOrModifiedMetadataItems()). Comparing by key is necessary
301 // to distinguish modified items from deleted items: subtraction of modified
302 // items, which have the same key but different values, should produce the
303 // empty set. Deleted items have a key only in |previous_items| so should be
304 // retained in the result.
305 auto key_comparator = [](const std::pair<MetadataKey, int64_t>& lhs,
306 const std::pair<MetadataKey, int64_t>& rhs) {
307 return MetadataKeyCompare()(lhs.first, rhs.first);
308 };
309 std::set_difference(previous_items.begin(), previous_items.end(),
310 current_items.begin(), current_items.end(),
311 std::inserter(deleted_items, deleted_items.begin()),
312 key_comparator);
313 return deleted_items;
314 }
315
MaybeAppendNameHash(uint64_t name_hash,google::protobuf::RepeatedField<uint64_t> * metadata_name_hashes)316 size_t CallStackProfileMetadata::MaybeAppendNameHash(
317 uint64_t name_hash,
318 google::protobuf::RepeatedField<uint64_t>* metadata_name_hashes) {
319 std::unordered_map<uint64_t, int>::iterator it;
320 bool inserted;
321 int next_item_index = metadata_name_hashes->size();
322
323 std::tie(it, inserted) =
324 metadata_hashes_cache_.emplace(name_hash, next_item_index);
325 if (inserted)
326 metadata_name_hashes->Add(name_hash);
327
328 return it->second;
329 }
330
331 } // namespace metrics
332