xref: /aosp_15_r20/external/cronet/components/metrics/call_stacks/call_stack_profile_metadata.cc (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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