1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #pragma once
18 
19 #include <any>
20 #include <map>
21 #include <sstream>
22 #include <string>
23 
24 #include <android-base/thread_annotations.h>
25 #include <media/MediaMetricsItem.h>
26 
27 namespace android::mediametrics {
28 
29 /**
30  * The TransactionLog is used to record mediametrics::Items to present
31  * different views on the time information (selected by audio, and sorted by key).
32  *
33  * The TransactionLog will always present data in timestamp order. (Perhaps we
34  * just make this submit order).
35  *
36  * These Views have a cost in shared pointer storage, so they aren't quite free.
37  *
38  * The TransactionLog is NOT thread safe.
39  */
40 class TransactionLog final { // made final as we have copy constructor instead of dup() override.
41 public:
42     // In long term run, the garbage collector aims to keep the
43     // Transaction Log between the Low Water Mark and the High Water Mark.
44 
45     // low water mark
46     static inline constexpr size_t kLogItemsLowWater = 1700;
47     // high water mark
48     static inline constexpr size_t kLogItemsHighWater = 2000;
49 
50     // Estimated max data usage is 1KB * kLogItemsHighWater.
51 
52     TransactionLog() = default;
53 
TransactionLog(size_t lowWaterMark,size_t highWaterMark)54     TransactionLog(size_t lowWaterMark, size_t highWaterMark)
55         : mLowWaterMark(lowWaterMark)
56         , mHighWaterMark(highWaterMark) {
57         LOG_ALWAYS_FATAL_IF(highWaterMark <= lowWaterMark,
58               "%s: required that highWaterMark:%zu > lowWaterMark:%zu",
59                   __func__, highWaterMark, lowWaterMark);
60     }
61 
62     // The TransactionLog copy constructor/assignment is effectively an
63     // instantaneous, isochronous snapshot of the other TransactionLog.
64     //
65     // The contents of the Transaction Log are shared pointers to immutable instances -
66     // std::shared_ptr<const mediametrics::Item>, so we use a shallow copy,
67     // which is more efficient in space and execution time than a deep copy,
68     // and gives the same results.
69 
TransactionLog(const TransactionLog & other)70     TransactionLog(const TransactionLog &other) {
71         *this = other;
72     }
73 
74     TransactionLog& operator=(const TransactionLog &other) {
75         std::lock_guard lock(mLock);
76         mLog.clear();
77         mItemMap.clear();
78 
79         std::lock_guard lock2(other.mLock);
80         mLog = other.mLog;
81         mItemMap = other.mItemMap;
82         mGarbageCollectionCount = other.mGarbageCollectionCount.load();
83 
84         return *this;
85     }
86 
87     /**
88      * Put an item in the TransactionLog.
89      */
put(const std::shared_ptr<const mediametrics::Item> & item)90     status_t put(const std::shared_ptr<const mediametrics::Item>& item) {
91         const std::string& key = item->getKey();
92         const int64_t time = item->getTimestamp();
93 
94         std::vector<std::any> garbage;  // objects destroyed after lock.
95         std::lock_guard lock(mLock);
96 
97         (void)gc(garbage);
98         mLog.emplace_hint(mLog.end(), time, item);
99         mItemMap[key].emplace_hint(mItemMap[key].end(), time, item);
100         return NO_ERROR;  // no errors for now.
101     }
102 
103     /**
104      * Returns all records within [startTime, endTime]
105      */
106     std::vector<std::shared_ptr<const mediametrics::Item>> get(
107             int64_t startTime = 0, int64_t endTime = INT64_MAX) const {
108         std::lock_guard lock(mLock);
109         return getItemsInRange(mLog, startTime, endTime);
110     }
111 
112     /**
113      * Returns all records for a key within [startTime, endTime]
114      */
115     std::vector<std::shared_ptr<const mediametrics::Item>> get(
116             const std::string& key,
117             int64_t startTime = 0, int64_t endTime = INT64_MAX) const {
118         std::lock_guard lock(mLock);
119         auto mapIt = mItemMap.find(key);
120         if (mapIt == mItemMap.end()) return {};
121         return getItemsInRange(mapIt->second, startTime, endTime);
122     }
123 
124     /**
125      * Returns a pair consisting of the Transaction Log as a string
126      * and the number of lines in the string.
127      *
128      * The number of lines in the returned pair is used as an optimization
129      * for subsequent line limiting.
130      *
131      * \param lines the maximum number of lines in the string returned.
132      * \param sinceNs the nanoseconds since Unix epoch to start dump (0 shows all)
133      * \param prefix the desired key prefix to match (nullptr shows all)
134      */
135     std::pair<std::string, int32_t> dump(
136             int32_t lines, int64_t sinceNs, const char *prefix = nullptr) const {
137         std::stringstream ss;
138         int32_t ll = lines;
139         std::lock_guard lock(mLock);
140 
141         // All audio items in time order.
142         if (ll > 0) {
143             ss << "Consolidated:\n";
144             --ll;
145         }
146         auto [s, l] = dumpMapTimeItem(mLog, ll, sinceNs, prefix);
147         ss << s;
148         ll -= l;
149 
150         // Grouped by item key (category)
151         if (ll > 0) {
152             ss << "Categorized:\n";
153             --ll;
154         }
155 
156         for (auto it = prefix != nullptr ? mItemMap.lower_bound(prefix) : mItemMap.begin();
157                 it != mItemMap.end();
158                 ++it) {
159             if (ll <= 0) break;
160             if (prefix != nullptr && !startsWith(it->first, prefix)) break;
161             std::tie(s, l) = dumpMapTimeItem(it->second, ll - 1, sinceNs, prefix);
162             if (l == 0) continue; // don't show empty groups (due to sinceNs).
163             ss << " " << it->first << "\n" << s;
164             ll -= l + 1;
165         }
166         return { ss.str(), lines - ll };
167     }
168 
169     /**
170      *  Returns number of Items in the TransactionLog.
171      */
size()172     size_t size() const {
173         std::lock_guard lock(mLock);
174         return mLog.size();
175     }
176 
177     /**
178      * Clears all Items from the TransactionLog.
179      */
180     // TODO: Garbage Collector, sweep and expire old values
clear()181     void clear() {
182         std::lock_guard lock(mLock);
183         mLog.clear();
184         mItemMap.clear();
185         mGarbageCollectionCount = 0;
186     }
187 
getGarbageCollectionCount()188     size_t getGarbageCollectionCount() const {
189         return mGarbageCollectionCount;
190     }
191 
192 private:
193     using MapTimeItem =
194             std::multimap<int64_t /* time */, std::shared_ptr<const mediametrics::Item>>;
195 
196     static std::pair<std::string, int32_t> dumpMapTimeItem(
197             const MapTimeItem& mapTimeItem,
198             int32_t lines, int64_t sinceNs = 0, const char *prefix = nullptr) {
199         std::stringstream ss;
200         int32_t ll = lines;
201         // Note: for our data, mapTimeItem.lower_bound(0) == mapTimeItem.begin().
202         for (auto it = mapTimeItem.lower_bound(sinceNs);
203                 it != mapTimeItem.end(); ++it) {
204             if (ll <= 0) break;
205             if (prefix != nullptr && !startsWith(it->second->getKey(), prefix)) {
206                 continue;
207             }
208             ss << "  " << it->second->toString() << "\n";
209             --ll;
210         }
211         return { ss.str(), lines - ll };
212     }
213 
214     /**
215      * Garbage collects if the TimeMachine size exceeds the high water mark.
216      *
217      * \param garbage a type-erased vector of elements to be destroyed
218      *        outside of lock.  Move large items to be destroyed here.
219      *
220      * \return true if garbage collection was done.
221      */
gc(std::vector<std::any> & garbage)222     bool gc(std::vector<std::any>& garbage) REQUIRES(mLock) {
223         if (mLog.size() < mHighWaterMark) return false;
224 
225         auto eraseEnd = mLog.begin();
226         size_t toRemove = mLog.size() - mLowWaterMark;
227         // remove at least those elements.
228 
229         // use a stale vector with precise type to avoid type erasure overhead in garbage
230         std::vector<std::shared_ptr<const mediametrics::Item>> stale;
231 
232         for (size_t i = 0; i < toRemove; ++i) {
233             stale.emplace_back(std::move(eraseEnd->second));
234             ++eraseEnd; // amortized O(1)
235         }
236         // ensure that eraseEnd is an lower bound on timeToErase.
237         const int64_t timeToErase = eraseEnd->first;
238         while (eraseEnd != mLog.end()) {
239             auto it = eraseEnd;
240             --it;  // amortized O(1)
241             if (it->first != timeToErase) {
242                 break;  // eraseEnd represents a unique time jump.
243             }
244             stale.emplace_back(std::move(eraseEnd->second));
245             ++eraseEnd;
246         }
247 
248         mLog.erase(mLog.begin(), eraseEnd);  // O(ptr_diff)
249 
250         size_t itemMapCount = 0;
251         for (auto it = mItemMap.begin(); it != mItemMap.end();) {
252             auto &keyHist = it->second;
253             auto it2 = keyHist.lower_bound(timeToErase);
254             if (it2 == keyHist.end()) {
255                 garbage.emplace_back(std::move(keyHist)); // directly move keyhist to garbage
256                 it = mItemMap.erase(it);
257             } else {
258                 for (auto it3 = keyHist.begin(); it3 != it2; ++it3) {
259                     stale.emplace_back(std::move(it3->second));
260                 }
261                 keyHist.erase(keyHist.begin(), it2);
262                 itemMapCount += keyHist.size();
263                  ++it;
264             }
265         }
266 
267         garbage.emplace_back(std::move(stale));
268 
269         ALOGD("%s(%zu, %zu): log size:%zu item map size:%zu, item map items:%zu",
270                 __func__, mLowWaterMark, mHighWaterMark,
271                 mLog.size(), mItemMap.size(), itemMapCount);
272         ++mGarbageCollectionCount;
273         return true;
274     }
275 
276     static std::vector<std::shared_ptr<const mediametrics::Item>> getItemsInRange(
277             const MapTimeItem& map,
278             int64_t startTime = 0, int64_t endTime = INT64_MAX) {
279         auto it = map.lower_bound(startTime);
280         if (it == map.end()) return {};
281 
282         auto it2 = map.upper_bound(endTime);
283 
284         std::vector<std::shared_ptr<const mediametrics::Item>> ret;
285         while (it != it2) {
286             ret.push_back(it->second);
287             ++it;
288         }
289         return ret;
290     }
291 
292     const size_t mLowWaterMark = kLogItemsLowWater;
293     const size_t mHighWaterMark = kLogItemsHighWater;
294 
295     std::atomic<size_t> mGarbageCollectionCount{};
296 
297     mutable std::mutex mLock;
298 
299     MapTimeItem mLog GUARDED_BY(mLock);
300     std::map<std::string /* item_key */, MapTimeItem> mItemMap GUARDED_BY(mLock);
301 };
302 
303 } // namespace android::mediametrics
304