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