| // 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 SERVICES_TRACING_PUBLIC_CPP_PERFETTO_INTERNING_INDEX_H_ |
| #define SERVICES_TRACING_PUBLIC_CPP_PERFETTO_INTERNING_INDEX_H_ |
| |
| #include <algorithm> |
| #include <array> |
| #include <cstdint> |
| #include <tuple> |
| |
| #include "base/component_export.h" |
| |
| namespace tracing { |
| |
| // Value 0 is an invalid ID. |
| using InterningID = uint32_t; |
| |
| struct COMPONENT_EXPORT(TRACING_CPP) InterningIndexEntry { |
| InterningID id; |
| |
| // Whether the entry was emitted since the last reset of emitted state. If |
| // |false|, the sink should (re)emit the entry in the current TracePacket. |
| // |
| // We don't remove entries on reset of emitted state, so that we can continue |
| // to use their original IDs and avoid unnecessarily incrementing the ID |
| // counter. |
| bool was_emitted; |
| }; |
| |
| // Index is a template that finds the index (stored in |value|) of a type |
| // |ValueType| inside a |Tuple|. This is done at compile time since this is |
| // needed inside a std::get<>(). |
| template <typename ValueType, typename Tuple> |
| struct Index; |
| |
| template <typename T, typename... Types> |
| struct Index<T, std::tuple<T, Types...>> { |
| static const size_t value = 0; |
| }; |
| |
| template <typename T, typename U, typename... Types> |
| struct Index<T, std::tuple<U, Types...>> { |
| static const size_t value = 1 + Index<T, std::tuple<Types...>>::value; |
| }; |
| |
| // A TypeList holds a list of types, while SizeList holds a list of size_t |
| // integers. |
| // |
| // We can't just use a parameter pack to store these two informations because we |
| // can only have one parameter pack in a template (it absorbs all the types) and |
| // must be last. However we want users to be able to specify per type size |
| // limits to decrease memory usage. One of these lists aren't strictly needed |
| // (you could just have users enter the numbers (or types) as a parameter pack |
| // directly), but we use both so that when declaring a InterningIndex to |
| // declaration is readable and avoids surprising behaviour. |
| template <typename...> |
| struct TypeList {}; |
| template <size_t...> |
| struct SizeList {}; |
| |
| // Interning index that associates interned values with interning IDs. It can |
| // track entries of different types within the same ID space, e.g. so that both |
| // copied strings and pointers to static strings can co-exist in the same index. |
| // |
| // The index will cache for a given ValueType and Size pair up to |Size| values |
| // after which it will start replacing them in a FIFO basis. All |Size|s must be |
| // a power of 2 for performance reasons (this is a compile time error if not). |
| // |
| // This definition just declares the type and template. Do not use this this |
| // directly, instead use the partial specialization below. |
| template <typename ValueTypes, typename Sizes> |
| class COMPONENT_EXPORT(TRACING_CPP) InterningIndex; |
| // Partially specalized so we can have two parameter packs which get nested |
| // inside their respective lists. |
| template <typename... ValueTypes, size_t... Sizes> |
| class InterningIndex<TypeList<ValueTypes...>, SizeList<Sizes...>> { |
| public: |
| // IndexCache is just a pair of std::arrays which are kept in sync with each |
| // other. This has an advantage over a std::array<pairs> since we can load a |
| // bunch of keys to search in one cache line without loading values. |
| template <size_t N, typename ValueType> |
| class IndexCache { |
| public: |
| IndexCache() { |
| // Assert that N is a power of two. This allows the "% N" in Insert() to |
| // compile to a bunch of bit shifts which improves performance over an |
| // arbitrary modulo division. |
| static_assert( |
| N && ((N & (N - 1)) == 0), |
| "InterningIndex requires that the cache size be a power of 2."); |
| } |
| |
| void Clear() { |
| for (auto& val : keys_) { |
| val = ValueType{}; |
| } |
| valid_index_ = 0; |
| current_index_ = 0; |
| } |
| |
| typename std::array<InterningIndexEntry, N>::iterator Find( |
| const ValueType& value) { |
| // First note that only |valid_index_| worth of entries have been assigned |
| // InterningIDs. |
| // |
| // So we search from the beginning up to |valid_index_|. |
| // |
| // If valid_index_ <= keys_.size and the element doesn't exist find will |
| // return it == keys_.begin() + valid_index_. This works because |
| // IndexCache::end() returns values_.begin() + valid_index_. So this will |
| // appear to be the IndexCache::end(), which again makes Find() act like |
| // std::find should. If valid_index_ == keys_.size() then this will be |
| // values_.end() as well. |
| // |
| // By doing this here and not checking the conditional we can only check |
| // it once and saves us a pretty impactful branch (this find is the |
| // majority of the time in LookUpOrInsert when profiled. |
| auto it = std::find(keys_.begin(), keys_.begin() + valid_index_, value); |
| return values_.begin() + (it - keys_.begin()); |
| } |
| |
| typename std::array<InterningIndexEntry, N>::iterator Insert( |
| const ValueType& key, |
| InterningIndexEntry&& value) { |
| size_t new_position = current_index_++ % N; |
| keys_[new_position] = key; |
| values_[new_position] = std::move(value); |
| valid_index_ = std::min(valid_index_ + 1, values_.size()); |
| return values_.begin() + new_position; |
| } |
| |
| typename std::array<InterningIndexEntry, N>::iterator begin() { |
| return values_.begin(); |
| } |
| typename std::array<InterningIndexEntry, N>::iterator end() { |
| return values_.begin() + valid_index_; |
| } |
| |
| private: |
| size_t valid_index_ = 0; |
| size_t current_index_ = 0; |
| std::array<ValueType, N> keys_{{}}; |
| std::array<InterningIndexEntry, N> values_{{}}; |
| }; |
| |
| // Construct a new index with caches for each of the ValueTypes. The cache |
| // size for the n-th ValueType will be limited to n-th element in the Sizes |
| // list. |
| // |
| // For example, to construct an index containing at most 1024 char* pointers |
| // and 128 std::string objects: |
| // InterningIndex<TypeList<char*, std::string>, SizeList<1024, 128>> |
| // index; |
| InterningIndex() {} |
| |
| // GetCache<ValueType>() Returns the cache for |ValueType| as a reference. |
| // |
| // A c++14 helper function. |
| // This is purely syntactic sugar function that is made possible by c++14. |
| // Every call to GetCache could be replaced by inlining the std::get below, |
| // but this hinders readability due to the template meta programing. |
| // |
| // auto return type means the compiler will determine the exact type returned, |
| // using the trailing decltype(auto) return type (which is a c++14 feature) |
| // allows us to never specify the type while the decltype(auto) rules |
| // preserves references. |
| // |
| // The return type here is an std::array<ValueType, Size>. However, figuring |
| // out the correct Size for the given ValueType is hard: because each |
| // ValueType has a unique Size given at compile time, but when we look up a |
| // Cache we only know which ValueType we want to store. So instead, we let the |
| // compiler figure out the return type (including the value of Size) based on |
| // the call to std;:get<>. |
| template <typename ValueType> |
| auto GetCache() -> decltype(auto) { |
| return std::get<Index<ValueType, std::tuple<ValueTypes...>>::value>( |
| entry_caches_); |
| } |
| |
| // Returns the entry for the given interned |value|, adding it to the index if |
| // it didn't exist previously or was evicted from the index. Entries may be |
| // evicted if they are accessed infrequently and the index for the respective |
| // ValueType is at full capacity. |
| // |
| // If the returned entry's |was_emitted| flag is false, the caller should |
| // (re)emit the entry in the current TracePacket's InternedData message. |
| template <typename ValueType> |
| InterningIndexEntry LookupOrAdd(const ValueType& value) { |
| auto& cache = GetCache<ValueType>(); |
| auto it = cache.Find(value); |
| if (it == cache.end()) { |
| it = cache.Insert(value, InterningIndexEntry{next_id_++, false}); |
| } |
| DCHECK(it->id != 0); |
| bool was_emitted = it->was_emitted; |
| // The caller will (re)emit the entry, so mark it as emitted. |
| it->was_emitted = true; |
| return InterningIndexEntry{it->id, was_emitted}; |
| } |
| |
| // Marks all entries as "not emitted", so that they will be reemitted when |
| // next accessed. |
| void ResetEmittedState() { ResetStateHelper<ValueTypes...>(/*clear=*/false); } |
| |
| // Removes all entries from the index and restarts from the first valid ID. |
| void Clear() { |
| ResetStateHelper<ValueTypes...>(/*clear=*/true); |
| next_id_ = 1u; |
| } |
| |
| private: |
| // Recursive helper template methods to reset the emitted state for all |
| // entries or remove all entries from each of the caches. |
| template <typename ValueType> |
| void ResetStateHelper(bool clear) { |
| return ResetStateForValueType<ValueType>(clear); |
| } |
| |
| template <typename ValueType1, |
| typename ValueType2, |
| typename... RemainingValueTypes> |
| void ResetStateHelper(bool clear) { |
| ResetStateForValueType<ValueType1>(clear); |
| ResetStateHelper<ValueType2, RemainingValueTypes...>(clear); |
| } |
| |
| template <typename ValueType> |
| void ResetStateForValueType(bool clear) { |
| auto& cache = GetCache<ValueType>(); |
| if (clear) { |
| cache.Clear(); |
| } else { |
| for (auto& entry : cache) { |
| entry.was_emitted = false; |
| } |
| } |
| } |
| |
| std::tuple<IndexCache<Sizes, ValueTypes>...> entry_caches_; |
| InterningID next_id_ = 1u; // ID 0 indicates an unset field value. |
| }; |
| |
| } // namespace tracing |
| |
| #endif // SERVICES_TRACING_PUBLIC_CPP_PERFETTO_INTERNING_INDEX_H_ |