MediaMetrics: Add AudioAnalytics
Test: mediametrics dumpsys, atest mediametrics_tests
Bug: 138583596
Change-Id: I56c82e6c685a9fae21581f14d3b370a8e352f3f3
diff --git a/services/mediaanalytics/Android.bp b/services/mediaanalytics/Android.bp
index dc72064..2eaabe1 100644
--- a/services/mediaanalytics/Android.bp
+++ b/services/mediaanalytics/Android.bp
@@ -30,6 +30,7 @@
name: "libmediaanalyticsservice",
srcs: [
+ "AudioAnalytics.cpp",
"iface_statsd.cpp",
"MediaAnalyticsService.cpp",
"statsd_audiopolicy.cpp",
diff --git a/services/mediaanalytics/AudioAnalytics.cpp b/services/mediaanalytics/AudioAnalytics.cpp
new file mode 100644
index 0000000..638c4ab
--- /dev/null
+++ b/services/mediaanalytics/AudioAnalytics.cpp
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "AudioAnalytics"
+#include <utils/Log.h>
+
+#include "AudioAnalytics.h"
+
+#include <audio_utils/clock.h> // clock conversions
+
+namespace android::mediametrics {
+
+AudioAnalytics::AudioAnalytics()
+{
+ ALOGD("%s", __func__);
+}
+
+AudioAnalytics::~AudioAnalytics()
+{
+ ALOGD("%s", __func__);
+}
+
+status_t AudioAnalytics::submit(
+ const std::shared_ptr<const MediaAnalyticsItem>& item, bool isTrusted)
+{
+ if (startsWith(item->getKey(), "audio.")) {
+ return mTimeMachine.put(item, isTrusted)
+ ?: mTransactionLog.put(item);
+ }
+ return BAD_VALUE;
+}
+
+std::pair<std::string, int32_t> AudioAnalytics::dump(int32_t lines) const
+{
+ std::stringstream ss;
+ int32_t ll = lines;
+
+ if (ll > 0) {
+ ss << "TransactionLog:\n";
+ --ll;
+ }
+ if (ll > 0) {
+ auto [s, l] = mTransactionLog.dump(ll);
+ ss << s;
+ ll -= l;
+ }
+ if (ll > 0) {
+ ss << "TimeMachine:\n";
+ --ll;
+ }
+ if (ll > 0) {
+ auto [s, l] = mTimeMachine.dump(ll);
+ ss << s;
+ ll -= l;
+ }
+ return { ss.str(), lines - ll };
+}
+
+} // namespace android
diff --git a/services/mediaanalytics/AudioAnalytics.h b/services/mediaanalytics/AudioAnalytics.h
new file mode 100644
index 0000000..366a809
--- /dev/null
+++ b/services/mediaanalytics/AudioAnalytics.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "TimeMachine.h"
+#include "TransactionLog.h"
+
+namespace android::mediametrics {
+
+class AudioAnalytics
+{
+public:
+ AudioAnalytics();
+ ~AudioAnalytics();
+
+ // TODO: update with conditions for keys.
+ /**
+ * Returns success if AudioAnalytics recognizes item.
+ *
+ * AudioAnalytics requires the item key to start with "audio.".
+ *
+ * A trusted source can create a new key, an untrusted source
+ * can only modify the key if the uid will match that authorized
+ * on the existing key.
+ *
+ * \param item the item to be submitted.
+ * \param isTrusted whether the transaction comes from a trusted source.
+ * In this case, a trusted source is verified by binder
+ * UID to be a system service by MediaMetrics service.
+ * Do not use true if you haven't really checked!
+ */
+ status_t submit(const std::shared_ptr<const MediaAnalyticsItem>& item, bool isTrusted);
+
+ /**
+ * Returns a pair consisting of the dump string, and the number of lines in the string.
+ *
+ * The number of lines in the returned pair is used as an optimization
+ * for subsequent line limiting.
+ *
+ * The TimeMachine and the TransactionLog are dumped separately under
+ * different locks, so may not be 100% consistent with the last data
+ * delivered.
+ *
+ * \param lines the maximum number of lines in the string returned.
+ */
+ std::pair<std::string, int32_t> dump(int32_t lines = INT32_MAX) const;
+
+private:
+ // The following are locked internally
+ TimeMachine mTimeMachine;
+ TransactionLog mTransactionLog;
+};
+
+} // namespace android::mediametrics
diff --git a/services/mediaanalytics/MediaAnalyticsService.cpp b/services/mediaanalytics/MediaAnalyticsService.cpp
index 091ddc5..a131e1a 100644
--- a/services/mediaanalytics/MediaAnalyticsService.cpp
+++ b/services/mediaanalytics/MediaAnalyticsService.cpp
@@ -159,6 +159,8 @@
// now attach either the item or its dup to a const shared pointer
std::shared_ptr<const MediaAnalyticsItem> sitem(release ? item : item->dup());
+ (void)mAudioAnalytics.submit(sitem, isTrusted);
+
extern bool dump2Statsd(const std::shared_ptr<const MediaAnalyticsItem>& item);
(void)dump2Statsd(sitem); // failure should be logged in function.
saveItem(sitem);
@@ -263,6 +265,9 @@
mItems.clear();
// shall we clear the summary data too?
}
+ // TODO: maybe consider a better way of dumping audio analytics info.
+ constexpr int32_t linesToDump = 1000;
+ result.append(mAudioAnalytics.dump(linesToDump).first.c_str());
}
write(fd, result.string(), result.size());
@@ -419,11 +424,14 @@
if (isTrusted) return true;
// untrusted uids can only send us a limited set of keys
const std::string &key = item->getKey();
+ if (startsWith(key, "audio.")) return true;
for (const char *allowedKey : {
+ // legacy audio
"audiopolicy",
"audiorecord",
"audiothread",
"audiotrack",
+ // other media
"codec",
"extractor",
"nuplayer",
diff --git a/services/mediaanalytics/MediaAnalyticsService.h b/services/mediaanalytics/MediaAnalyticsService.h
index ce7b9f4..5bdc48f 100644
--- a/services/mediaanalytics/MediaAnalyticsService.h
+++ b/services/mediaanalytics/MediaAnalyticsService.h
@@ -26,6 +26,8 @@
#include <media/IMediaAnalyticsService.h>
#include <utils/String8.h>
+#include "AudioAnalytics.h"
+
namespace android {
class MediaAnalyticsService : public BnMediaAnalyticsService
@@ -115,6 +117,8 @@
std::atomic<int64_t> mItemsSubmitted{}; // accessed outside of lock.
+ mediametrics::AudioAnalytics mAudioAnalytics;
+
std::mutex mLock;
// statistics about our analytics
int64_t mItemsFinalized = 0; // GUARDED_BY(mLock)
diff --git a/services/mediaanalytics/TimeMachine.h b/services/mediaanalytics/TimeMachine.h
new file mode 100644
index 0000000..578b838
--- /dev/null
+++ b/services/mediaanalytics/TimeMachine.h
@@ -0,0 +1,484 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <any>
+#include <map>
+#include <sstream>
+#include <string>
+#include <variant>
+#include <vector>
+
+#include <media/MediaAnalyticsItem.h>
+#include <utils/Timers.h>
+
+namespace android::mediametrics {
+
+// define a way of printing the monostate
+inline std::ostream & operator<< (std::ostream& s,
+ std::monostate const& v __unused) {
+ s << "none_item";
+ return s;
+}
+
+// define a way of printing a variant
+// see https://en.cppreference.com/w/cpp/utility/variant/visit
+template <typename T0, typename ... Ts>
+std::ostream & operator<< (std::ostream& s,
+ std::variant<T0, Ts...> const& v) {
+ std::visit([&s](auto && arg){ s << std::forward<decltype(arg)>(arg); }, v);
+ return s;
+}
+
+/**
+ * The TimeMachine is used to record timing changes of MediaAnalyticItem
+ * properties.
+ *
+ * Any URL that ends with '!' will have a time sequence that keeps duplicates.
+ *
+ * The TimeMachine is NOT thread safe.
+ */
+class TimeMachine {
+
+ using Elem = std::variant<std::monostate, int32_t, int64_t, double, std::string>;
+ using PropertyHistory = std::multimap<int64_t /* time */, Elem>;
+
+ // KeyHistory contains no lock.
+ // Access is through the TimeMachine, and a hash-striped lock is used
+ // before calling into KeyHistory.
+ class KeyHistory {
+ public:
+ template <typename T>
+ KeyHistory(T key, pid_t pid, uid_t uid, int64_t time)
+ : mKey(key)
+ , mPid(pid)
+ , mUid(uid)
+ , mCreationTime(time)
+ , mLastModificationTime(time)
+ {
+ putValue("_pid", (int32_t)pid, time);
+ putValue("_uid", (int32_t)uid, time);
+ }
+
+ status_t checkPermission(uid_t uidCheck) const {
+ return uidCheck != (uid_t)-1 && uidCheck != mUid ? PERMISSION_DENIED : NO_ERROR;
+ }
+
+ template <typename T>
+ status_t getValue(const std::string &property, T* value, int64_t time = 0) const {
+ if (time == 0) time = systemTime(SYSTEM_TIME_BOOTTIME);
+ const auto tsptr = mPropertyMap.find(property);
+ if (tsptr == mPropertyMap.end()) return BAD_VALUE;
+ const auto& timeSequence = tsptr->second;
+ auto eptr = timeSequence.upper_bound(time);
+ if (eptr == timeSequence.begin()) return BAD_VALUE;
+ --eptr;
+ if (eptr == timeSequence.end()) return BAD_VALUE;
+ const T* vptr = std::get_if<T>(&eptr->second);
+ if (vptr == nullptr) return BAD_VALUE;
+ *value = *vptr;
+ return NO_ERROR;
+ }
+
+ template <typename T>
+ status_t getValue(const std::string &property, T defaultValue, int64_t time = 0) const {
+ T value;
+ return getValue(property, &value, time) != NO_ERROR ? defaultValue : value;
+ }
+
+ void putProp(
+ const std::string &name, const MediaAnalyticsItem::Prop &prop, int64_t time = 0) {
+ prop.visit([&](auto value) { putValue(name, value, time); });
+ }
+
+ template <typename T>
+ void putValue(const std::string &property,
+ T&& e, int64_t time = 0) {
+ if (time == 0) time = systemTime(SYSTEM_TIME_BOOTTIME);
+ mLastModificationTime = time;
+ auto& timeSequence = mPropertyMap[property];
+ Elem el{std::forward<T>(e)};
+ if (timeSequence.empty() // no elements
+ || property.back() == '!' // keep duplicates TODO: remove?
+ || timeSequence.rbegin()->second != el) { // value changed
+ timeSequence.emplace(time, std::move(el));
+ }
+ }
+
+ // Explicitly ignore rate properties - we don't expose them for now.
+ void putValue(
+ const std::string &property __unused,
+ std::pair<int64_t, int64_t>& e __unused,
+ int64_t time __unused) {
+ }
+
+ std::pair<std::string, int32_t> dump(int32_t lines, int64_t time) const {
+ std::stringstream ss;
+ int32_t ll = lines;
+ for (auto& tsPair : mPropertyMap) {
+ if (ll <= 0) break;
+ ss << dump(mKey, tsPair, time);
+ --ll;
+ }
+ return { ss.str(), lines - ll };
+ }
+
+ int64_t getLastModificationTime() const { return mLastModificationTime; }
+
+ private:
+ static std::string dump(
+ const std::string &key,
+ const std::pair<std::string /* prop */, PropertyHistory>& tsPair,
+ int64_t time) {
+ const auto timeSequence = tsPair.second;
+ auto eptr = timeSequence.lower_bound(time);
+ if (eptr == timeSequence.end()) {
+ return tsPair.first + "={};\n";
+ }
+ std::stringstream ss;
+ ss << key << "." << tsPair.first << "={";
+ do {
+ ss << eptr->first << ":" << eptr->second << ",";
+ } while (++eptr != timeSequence.end());
+ ss << "};\n";
+ return ss.str();
+ }
+
+ const std::string mKey;
+ const pid_t mPid __unused;
+ const uid_t mUid;
+ const int64_t mCreationTime __unused;
+
+ int64_t mLastModificationTime;
+ std::map<std::string /* property */, PropertyHistory> mPropertyMap;
+ };
+
+ using History = std::map<std::string /* key */, std::shared_ptr<KeyHistory>>;
+
+ static inline constexpr size_t kKeyLowWaterMark = 500;
+ static inline constexpr size_t kKeyHighWaterMark = 1000;
+
+ // Estimated max data space usage is 3KB * kKeyHighWaterMark.
+
+public:
+
+ TimeMachine() = default;
+ TimeMachine(size_t keyLowWaterMark, size_t keyHighWaterMark)
+ : mKeyLowWaterMark(keyLowWaterMark)
+ , mKeyHighWaterMark(keyHighWaterMark) {
+ LOG_ALWAYS_FATAL_IF(keyHighWaterMark <= keyLowWaterMark,
+ "%s: required that keyHighWaterMark:%zu > keyLowWaterMark:%zu",
+ __func__, keyHighWaterMark, keyLowWaterMark);
+ }
+
+ /**
+ * Put all the properties from an item into the Time Machine log.
+ */
+ status_t put(const std::shared_ptr<const MediaAnalyticsItem>& item, bool isTrusted = false) {
+ const int64_t time = item->getTimestamp();
+ const std::string &key = item->getKey();
+
+ std::shared_ptr<KeyHistory> keyHistory;
+ {
+ std::vector<std::any> garbage;
+ std::lock_guard lock(mLock);
+
+ auto it = mHistory.find(key);
+ if (it == mHistory.end()) {
+ if (!isTrusted) return PERMISSION_DENIED;
+
+ (void)gc_l(garbage);
+
+ // no keylock needed here as we are sole owner
+ // until placed on mHistory.
+ keyHistory = std::make_shared<KeyHistory>(
+ key, item->getPid(), item->getUid(), time);
+ mHistory[key] = keyHistory;
+ } else {
+ keyHistory = it->second;
+ }
+ }
+
+ // deferred contains remote properties (for other keys) to do later.
+ std::vector<const MediaAnalyticsItem::Prop *> deferred;
+ {
+ // handle local properties
+ std::lock_guard lock(getLockForKey(key));
+ if (!isTrusted) {
+ status_t status = keyHistory->checkPermission(item->getUid());
+ if (status != NO_ERROR) return status;
+ }
+
+ for (const auto &prop : *item) {
+ const std::string &name = prop.getName();
+ if (name.size() == 0 || name[0] == '_') continue;
+
+ // Cross key settings are with [key]property
+ if (name[0] == '[') {
+ if (!isTrusted) continue;
+ deferred.push_back(&prop);
+ } else {
+ keyHistory->putProp(name, prop, time);
+ }
+ }
+ }
+
+ // handle remote properties, if any
+ for (const auto propptr : deferred) {
+ const auto &prop = *propptr;
+ const std::string &name = prop.getName();
+ size_t end = name.find_first_of(']'); // TODO: handle nested [] or escape?
+ if (end == 0) continue;
+ std::string remoteKey = name.substr(1, end - 1);
+ std::string remoteName = name.substr(end + 1);
+ if (remoteKey.size() == 0 || remoteName.size() == 0) continue;
+ std::shared_ptr<KeyHistory> remoteKeyHistory;
+ {
+ std::lock_guard lock(mLock);
+ auto it = mHistory.find(remoteKey);
+ if (it == mHistory.end()) continue;
+ remoteKeyHistory = it->second;
+ }
+ std::lock_guard(getLockForKey(remoteKey));
+ remoteKeyHistory->putProp(remoteName, prop, time);
+ }
+ return NO_ERROR;
+ }
+
+ template <typename T>
+ status_t get(const std::string &key, const std::string &property,
+ T* value, int32_t uidCheck = -1, int64_t time = 0) const {
+ std::shared_ptr<KeyHistory> keyHistory;
+ {
+ std::lock_guard lock(mLock);
+ const auto it = mHistory.find(key);
+ if (it == mHistory.end()) return BAD_VALUE;
+ keyHistory = it->second;
+ }
+ std::lock_guard lock(getLockForKey(key));
+ return keyHistory->checkPermission(uidCheck)
+ ?: keyHistory->getValue(property, value, time);
+ }
+
+ /**
+ * Individual property put.
+ *
+ * Put takes in a time (if none is provided then BOOTTIME is used).
+ */
+ template <typename T>
+ status_t put(const std::string &url, T &&e, int64_t time = 0) {
+ std::string key;
+ std::string prop;
+ std::shared_ptr<KeyHistory> keyHistory =
+ getKeyHistoryFromUrl(url, &key, &prop);
+ if (keyHistory == nullptr) return BAD_VALUE;
+ if (time == 0) time = systemTime(SYSTEM_TIME_BOOTTIME);
+ std::lock_guard lock(getLockForKey(key));
+ keyHistory->putValue(prop, std::forward<T>(e), time);
+ return NO_ERROR;
+ }
+
+ /**
+ * Individual property get
+ */
+ template <typename T>
+ status_t get(const std::string &url, T* value, int32_t uidCheck, int64_t time = 0) const {
+ std::string key;
+ std::string prop;
+ std::shared_ptr<KeyHistory> keyHistory =
+ getKeyHistoryFromUrl(url, &key, &prop);
+ if (keyHistory == nullptr) return BAD_VALUE;
+
+ std::lock_guard lock(getLockForKey(key));
+ return keyHistory->checkPermission(uidCheck)
+ ?: keyHistory->getValue(prop, value, time);
+ }
+
+ /**
+ * Individual property get with default
+ */
+ template <typename T>
+ T get(const std::string &url, const T &defaultValue, int32_t uidCheck,
+ int64_t time = 0) const {
+ T value;
+ return get(url, &value, uidCheck, time) == NO_ERROR
+ ? value : defaultValue;
+ }
+
+ /**
+ * Returns number of keys in the Time Machine.
+ */
+ size_t size() const {
+ std::lock_guard lock(mLock);
+ return mHistory.size();
+ }
+
+ /**
+ * Clears all properties from the Time Machine.
+ */
+ void clear() {
+ std::lock_guard lock(mLock);
+ mHistory.clear();
+ }
+
+ /**
+ * Returns a pair consisting of the TimeMachine state as a string
+ * and the number of lines in the string.
+ *
+ * The number of lines in the returned pair is used as an optimization
+ * for subsequent line limiting.
+ *
+ * \param lines the maximum number of lines in the string returned.
+ * \param key selects only that key.
+ * \param time to start the dump from.
+ */
+ std::pair<std::string, int32_t> dump(
+ int32_t lines = INT32_MAX, const std::string &key = {}, int64_t time = 0) const {
+ std::lock_guard lock(mLock);
+ if (!key.empty()) { // use std::regex
+ const auto it = mHistory.find(key);
+ if (it == mHistory.end()) return {};
+ std::lock_guard lock(getLockForKey(it->first));
+ return it->second->dump(lines, time);
+ }
+
+ std::stringstream ss;
+ int32_t ll = lines;
+ for (const auto &keyPair : mHistory) {
+ std::lock_guard lock(getLockForKey(keyPair.first));
+ if (lines <= 0) break;
+ auto [s, l] = keyPair.second->dump(ll, time);
+ ss << s;
+ ll -= l;
+ }
+ return { ss.str(), lines - ll };
+ }
+
+private:
+
+ // Obtains the lock for a KeyHistory.
+ std::mutex &getLockForKey(const std::string &key) const {
+ return mKeyLocks[std::hash<std::string>{}(key) % std::size(mKeyLocks)];
+ }
+
+ // Finds a KeyHistory from a URL. Returns nullptr if not found.
+ std::shared_ptr<KeyHistory> getKeyHistoryFromUrl(
+ std::string url, std::string* key, std::string *prop) const {
+ std::lock_guard lock(mLock);
+
+ auto it = mHistory.upper_bound(url);
+ if (it == mHistory.begin()) {
+ return nullptr;
+ }
+ --it; // go to the actual key, if it exists.
+
+ const std::string& itKey = it->first;
+ if (strncmp(itKey.c_str(), url.c_str(), itKey.size())) {
+ return nullptr;
+ }
+ if (key) *key = itKey;
+ if (prop) *prop = url.substr(itKey.size() + 1);
+ return it->second;
+ }
+
+ // GUARDED_BY mLock
+ /**
+ * Garbage collects if the TimeMachine size exceeds the high water mark.
+ *
+ * \param garbage a type-erased vector of elements to be destroyed
+ * outside of lock. Move large items to be destroyed here.
+ *
+ * \return true if garbage collection was done.
+ */
+ bool gc_l(std::vector<std::any>& garbage) {
+ // TODO: something better than this for garbage collection.
+ if (mHistory.size() < mKeyHighWaterMark) return false;
+
+ ALOGD("%s: garbage collection", __func__);
+
+ // erase everything explicitly expired.
+ std::multimap<int64_t, std::string> accessList;
+ // use a stale vector with precise type to avoid type erasure overhead in garbage
+ std::vector<std::shared_ptr<KeyHistory>> stale;
+
+ for (auto it = mHistory.begin(); it != mHistory.end();) {
+ const std::string& key = it->first;
+ std::shared_ptr<KeyHistory> &keyHist = it->second;
+
+ std::lock_guard lock(getLockForKey(it->first));
+ int64_t expireTime = keyHist->getValue("_expire", -1 /* default */);
+ if (expireTime != -1) {
+ stale.emplace_back(std::move(it->second));
+ it = mHistory.erase(it);
+ } else {
+ accessList.emplace(keyHist->getLastModificationTime(), key);
+ ++it;
+ }
+ }
+
+ if (mHistory.size() > mKeyLowWaterMark) {
+ const size_t toDelete = mHistory.size() - mKeyLowWaterMark;
+ auto it = accessList.begin();
+ for (size_t i = 0; i < toDelete; ++i) {
+ auto it2 = mHistory.find(it->second);
+ stale.emplace_back(std::move(it2->second));
+ mHistory.erase(it2);
+ ++it;
+ }
+ }
+ garbage.emplace_back(std::move(accessList));
+ garbage.emplace_back(std::move(stale));
+
+ ALOGD("%s(%zu, %zu): key size:%zu",
+ __func__, mKeyLowWaterMark, mKeyHighWaterMark,
+ mHistory.size());
+ return true;
+ }
+
+ const size_t mKeyLowWaterMark = kKeyLowWaterMark;
+ const size_t mKeyHighWaterMark = kKeyHighWaterMark;
+
+ /**
+ * Locking Strategy
+ *
+ * Each key in the History has a KeyHistory. To get a shared pointer to
+ * the KeyHistory requires a lookup of mHistory under mLock. Once the shared
+ * pointer to KeyHistory is obtained, the mLock for mHistory can be released.
+ *
+ * Once the shared pointer to the key's KeyHistory is obtained, the KeyHistory
+ * can be locked for read and modification through the method getLockForKey().
+ *
+ * Instead of having a mutex per KeyHistory, we use a hash striped lock
+ * which assigns a mutex based on the hash of the key string.
+ *
+ * Once the last shared pointer reference to KeyHistory is released, it is
+ * destroyed. This is done through the garbage collection method.
+ *
+ * This two level locking allows multiple threads to access the TimeMachine
+ * in parallel.
+ */
+
+ mutable std::mutex mLock; // Lock for mHistory
+ History mHistory; // GUARDED_BY mLock
+
+ // KEY_LOCKS is the number of mutexes for keys.
+ // It need not be a power of 2, but faster that way.
+ static inline constexpr size_t KEY_LOCKS = 256;
+ mutable std::mutex mKeyLocks[KEY_LOCKS]; // Hash-striped lock for KeyHistory based on key.
+};
+
+} // namespace android::mediametrics
diff --git a/services/mediaanalytics/TransactionLog.h b/services/mediaanalytics/TransactionLog.h
new file mode 100644
index 0000000..ca37862
--- /dev/null
+++ b/services/mediaanalytics/TransactionLog.h
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <any>
+#include <map>
+#include <sstream>
+#include <string>
+
+#include <media/MediaAnalyticsItem.h>
+
+namespace android::mediametrics {
+
+/**
+ * The TransactionLog is used to record MediaAnalyticsItems to present
+ * different views on the time information (selected by audio, and sorted by key).
+ *
+ * The TransactionLog will always present data in timestamp order. (Perhaps we
+ * just make this submit order).
+ *
+ * These Views have a cost in shared pointer storage, so they aren't quite free.
+ *
+ * The TransactionLog is NOT thread safe.
+ */
+class TransactionLog {
+public:
+ // In long term run, the garbage collector aims to keep the
+ // Transaction Log between the Low Water Mark and the High Water Mark.
+
+ // low water mark
+ static inline constexpr size_t kLogItemsLowWater = 5000;
+ // high water mark
+ static inline constexpr size_t kLogItemsHighWater = 10000;
+
+ // Estimated max data usage is 1KB * kLogItemsHighWater.
+
+ TransactionLog() = default;
+
+ TransactionLog(size_t lowWaterMark, size_t highWaterMark)
+ : mLowWaterMark(lowWaterMark)
+ , mHighWaterMark(highWaterMark) {
+ LOG_ALWAYS_FATAL_IF(highWaterMark <= lowWaterMark,
+ "%s: required that highWaterMark:%zu > lowWaterMark:%zu",
+ __func__, highWaterMark, lowWaterMark);
+ }
+
+ /**
+ * Put an item in the TransactionLog.
+ */
+ status_t put(const std::shared_ptr<const MediaAnalyticsItem>& item) {
+ const std::string& key = item->getKey();
+ const int64_t time = item->getTimestamp();
+
+ std::vector<std::any> garbage; // objects destroyed after lock.
+ std::lock_guard lock(mLock);
+
+ (void)gc_l(garbage);
+ mLog.emplace(time, item);
+ mItemMap[key].emplace(time, item);
+ return NO_ERROR; // no errors for now.
+ }
+
+ /**
+ * Returns all records within [startTime, endTime]
+ */
+ std::vector<std::shared_ptr<const MediaAnalyticsItem>> get(
+ int64_t startTime = 0, int64_t endTime = INT64_MAX) const {
+ std::lock_guard lock(mLock);
+ return getItemsInRange_l(mLog, startTime, endTime);
+ }
+
+ /**
+ * Returns all records for a key within [startTime, endTime]
+ */
+ std::vector<std::shared_ptr<const MediaAnalyticsItem>> get(
+ const std::string& key,
+ int64_t startTime = 0, int64_t endTime = INT64_MAX) const {
+ std::lock_guard lock(mLock);
+ auto mapIt = mItemMap.find(key);
+ if (mapIt == mItemMap.end()) return {};
+ return getItemsInRange_l(mapIt->second, startTime, endTime);
+ }
+
+ /**
+ * Returns a pair consisting of the Transaction Log as a string
+ * and the number of lines in the string.
+ *
+ * The number of lines in the returned pair is used as an optimization
+ * for subsequent line limiting.
+ *
+ * \param lines the maximum number of lines in the string returned.
+ */
+ std::pair<std::string, int32_t> dump(int32_t lines) const {
+ std::stringstream ss;
+ int32_t ll = lines;
+ std::lock_guard lock(mLock);
+
+ // All audio items in time order.
+ if (ll > 0) {
+ ss << "Consolidated:\n";
+ --ll;
+ }
+ for (const auto &log : mLog) {
+ if (ll <= 0) break;
+ ss << " " << log.second->toString() << "\n";
+ --ll;
+ }
+
+ // Grouped by item key (category)
+ if (ll > 0) {
+ ss << "Categorized:\n";
+ --ll;
+ }
+ for (const auto &itemMap : mItemMap) {
+ if (ll <= 0) break;
+ ss << " " << itemMap.first << "\n";
+ --ll;
+ for (const auto &item : itemMap.second) {
+ if (ll <= 0) break;
+ ss << " { " << item.first << ", " << item.second->toString() << " }\n";
+ --ll;
+ }
+ }
+ return { ss.str(), lines - ll };
+ }
+
+ /**
+ * Returns number of Items in the TransactionLog.
+ */
+ size_t size() const {
+ std::lock_guard lock(mLock);
+ return mLog.size();
+ }
+
+ /**
+ * Clears all Items from the TransactionLog.
+ */
+ // TODO: Garbage Collector, sweep and expire old values
+ void clear() {
+ std::lock_guard lock(mLock);
+ mLog.clear();
+ mItemMap.clear();
+ }
+
+private:
+ using MapTimeItem =
+ std::multimap<int64_t /* time */, std::shared_ptr<const MediaAnalyticsItem>>;
+
+ // GUARDED_BY mLock
+ /**
+ * Garbage collects if the TimeMachine size exceeds the high water mark.
+ *
+ * \param garbage a type-erased vector of elements to be destroyed
+ * outside of lock. Move large items to be destroyed here.
+ *
+ * \return true if garbage collection was done.
+ */
+ bool gc_l(std::vector<std::any>& garbage) {
+ if (mLog.size() < mHighWaterMark) return false;
+
+ ALOGD("%s: garbage collection", __func__);
+
+ auto eraseEnd = mLog.begin();
+ size_t toRemove = mLog.size() - mLowWaterMark;
+ // remove at least those elements.
+
+ // use a stale vector with precise type to avoid type erasure overhead in garbage
+ std::vector<std::shared_ptr<const MediaAnalyticsItem>> stale;
+
+ for (size_t i = 0; i < toRemove; ++i) {
+ stale.emplace_back(std::move(eraseEnd->second));
+ ++eraseEnd; // amortized O(1)
+ }
+ // ensure that eraseEnd is an lower bound on timeToErase.
+ const int64_t timeToErase = eraseEnd->first;
+ while (eraseEnd != mLog.end()) {
+ auto it = eraseEnd;
+ --it; // amortized O(1)
+ if (it->first != timeToErase) {
+ break; // eraseEnd represents a unique time jump.
+ }
+ stale.emplace_back(std::move(eraseEnd->second));
+ ++eraseEnd;
+ }
+
+ mLog.erase(mLog.begin(), eraseEnd); // O(ptr_diff)
+
+ size_t itemMapCount = 0;
+ for (auto it = mItemMap.begin(); it != mItemMap.end();) {
+ auto &keyHist = it->second;
+ auto it2 = keyHist.lower_bound(timeToErase);
+ if (it2 == keyHist.end()) {
+ garbage.emplace_back(std::move(keyHist)); // directly move keyhist to garbage
+ it = mItemMap.erase(it);
+ } else {
+ for (auto it3 = keyHist.begin(); it3 != it2; ++it3) {
+ stale.emplace_back(std::move(it3->second));
+ }
+ keyHist.erase(keyHist.begin(), it2);
+ itemMapCount += keyHist.size();
+ ++it;
+ }
+ }
+
+ garbage.emplace_back(std::move(stale));
+
+ ALOGD("%s(%zu, %zu): log size:%zu item map size:%zu, item map items:%zu",
+ __func__, mLowWaterMark, mHighWaterMark,
+ mLog.size(), mItemMap.size(), itemMapCount);
+ return true;
+ }
+
+ static std::vector<std::shared_ptr<const MediaAnalyticsItem>> getItemsInRange_l(
+ const MapTimeItem& map,
+ int64_t startTime = 0, int64_t endTime = INT64_MAX) {
+ auto it = map.lower_bound(startTime);
+ if (it == map.end()) return {};
+
+ auto it2 = map.upper_bound(endTime);
+
+ std::vector<std::shared_ptr<const MediaAnalyticsItem>> ret;
+ while (it != it2) {
+ ret.push_back(it->second);
+ ++it;
+ }
+ return ret;
+ }
+
+ const size_t mLowWaterMark = kLogItemsHighWater;
+ const size_t mHighWaterMark = kLogItemsHighWater;
+
+ mutable std::mutex mLock;
+
+ // GUARDED_BY mLock
+ MapTimeItem mLog;
+
+ // GUARDED_BY mLock
+ std::map<std::string /* item_key */, MapTimeItem> mItemMap;
+};
+
+} // namespace android::mediametrics
diff --git a/services/mediaanalytics/tests/mediametrics_tests.cpp b/services/mediaanalytics/tests/mediametrics_tests.cpp
index fc02767..89b5383 100644
--- a/services/mediaanalytics/tests/mediametrics_tests.cpp
+++ b/services/mediaanalytics/tests/mediametrics_tests.cpp
@@ -26,6 +26,15 @@
using namespace android;
+static size_t countNewlines(const char *s) {
+ size_t count = 0;
+ while ((s = strchr(s, '\n')) != nullptr) {
+ ++s;
+ ++count;
+ }
+ return count;
+}
+
TEST(mediametrics_tests, instantiate) {
sp mediaMetrics = new MediaAnalyticsService();
status_t status;
@@ -356,3 +365,247 @@
ASSERT_EQ((int32_t)i, i32);
}
}
+
+TEST(mediametrics_tests, time_machine_storage) {
+ auto item = std::make_shared<MediaAnalyticsItem>("Key");
+ (*item).set("i32", (int32_t)1)
+ .set("i64", (int64_t)2)
+ .set("double", (double)3.125)
+ .set("string", "abcdefghijklmnopqrstuvwxyz")
+ .set("rate", std::pair<int64_t, int64_t>(11, 12));
+
+ // Let's put the item in
+ android::mediametrics::TimeMachine timeMachine;
+ ASSERT_EQ(NO_ERROR, timeMachine.put(item, true));
+
+ // Can we read the values?
+ int32_t i32;
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key", "i32", &i32, -1));
+ ASSERT_EQ(1, i32);
+
+ int64_t i64;
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key", "i64", &i64, -1));
+ ASSERT_EQ(2, i64);
+
+ double d;
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key", "double", &d, -1));
+ ASSERT_EQ(3.125, d);
+
+ std::string s;
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key", "string", &s, -1));
+ ASSERT_EQ("abcdefghijklmnopqrstuvwxyz", s);
+
+ // Using fully qualified name?
+ i32 = 0;
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key.i32", &i32, -1));
+ ASSERT_EQ(1, i32);
+
+ i64 = 0;
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key.i64", &i64, -1));
+ ASSERT_EQ(2, i64);
+
+ d = 0.;
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key.double", &d, -1));
+ ASSERT_EQ(3.125, d);
+
+ s.clear();
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key.string", &s, -1));
+ ASSERT_EQ("abcdefghijklmnopqrstuvwxyz", s);
+}
+
+TEST(mediametrics_tests, time_machine_remote_key) {
+ auto item = std::make_shared<MediaAnalyticsItem>("Key1");
+ (*item).set("one", (int32_t)1)
+ .set("two", (int32_t)2);
+
+ android::mediametrics::TimeMachine timeMachine;
+ ASSERT_EQ(NO_ERROR, timeMachine.put(item, true));
+
+ auto item2 = std::make_shared<MediaAnalyticsItem>("Key2");
+ (*item2).set("three", (int32_t)3)
+ .set("[Key1]four", (int32_t)4) // affects Key1
+ .set("[Key1]five", (int32_t)5); // affects key1
+
+ ASSERT_EQ(NO_ERROR, timeMachine.put(item2, true));
+
+ auto item3 = std::make_shared<MediaAnalyticsItem>("Key2");
+ (*item3).set("six", (int32_t)6)
+ .set("[Key1]seven", (int32_t)7); // affects Key1
+
+ ASSERT_EQ(NO_ERROR, timeMachine.put(item3, false)); // remote keys not allowed.
+
+ // Can we read the values?
+ int32_t i32;
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key1.one", &i32, -1));
+ ASSERT_EQ(1, i32);
+
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key1.two", &i32, -1));
+ ASSERT_EQ(2, i32);
+
+ ASSERT_EQ(BAD_VALUE, timeMachine.get("Key1.three", &i32, -1));
+
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key2.three", &i32, -1));
+ ASSERT_EQ(3, i32);
+
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key1.four", &i32, -1));
+ ASSERT_EQ(4, i32);
+
+ ASSERT_EQ(BAD_VALUE, timeMachine.get("Key2.four", &i32, -1));
+
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key1.five", &i32, -1));
+ ASSERT_EQ(5, i32);
+
+ ASSERT_EQ(BAD_VALUE, timeMachine.get("Key2.five", &i32, -1));
+
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key2.six", &i32, -1));
+ ASSERT_EQ(6, i32);
+
+ ASSERT_EQ(BAD_VALUE, timeMachine.get("Key2.seven", &i32, -1));
+}
+
+TEST(mediametrics_tests, time_machine_gc) {
+ auto item = std::make_shared<MediaAnalyticsItem>("Key1");
+ (*item).set("one", (int32_t)1)
+ .set("two", (int32_t)2)
+ .setTimestamp(10);
+
+ android::mediametrics::TimeMachine timeMachine(1, 2); // keep at most 2 keys.
+
+ ASSERT_EQ((size_t)0, timeMachine.size());
+
+ ASSERT_EQ(NO_ERROR, timeMachine.put(item, true));
+
+ ASSERT_EQ((size_t)1, timeMachine.size());
+
+ auto item2 = std::make_shared<MediaAnalyticsItem>("Key2");
+ (*item2).set("three", (int32_t)3)
+ .set("[Key1]three", (int32_t)3)
+ .setTimestamp(11);
+
+ ASSERT_EQ(NO_ERROR, timeMachine.put(item2, true));
+ ASSERT_EQ((size_t)2, timeMachine.size());
+
+ //printf("Before\n%s\n\n", timeMachine.dump().c_str());
+
+ auto item3 = std::make_shared<MediaAnalyticsItem>("Key3");
+ (*item3).set("six", (int32_t)6)
+ .set("[Key1]four", (int32_t)4) // affects Key1
+ .set("[Key1]five", (int32_t)5) // affects key1
+ .setTimestamp(12);
+
+ ASSERT_EQ(NO_ERROR, timeMachine.put(item3, true));
+
+ ASSERT_EQ((size_t)2, timeMachine.size());
+
+ // Can we read the values?
+ int32_t i32;
+ ASSERT_EQ(BAD_VALUE, timeMachine.get("Key1.one", &i32, -1));
+ ASSERT_EQ(BAD_VALUE, timeMachine.get("Key1.two", &i32, -1));
+ ASSERT_EQ(BAD_VALUE, timeMachine.get("Key1.three", &i32, -1));
+ ASSERT_EQ(BAD_VALUE, timeMachine.get("Key1.four", &i32, -1));
+ ASSERT_EQ(BAD_VALUE, timeMachine.get("Key1.five", &i32, -1));
+
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key2.three", &i32, -1));
+ ASSERT_EQ(3, i32);
+
+ ASSERT_EQ(NO_ERROR, timeMachine.get("Key3.six", &i32, -1));
+ ASSERT_EQ(6, i32);
+
+ printf("After\n%s\n", timeMachine.dump().first.c_str());
+}
+
+TEST(mediametrics_tests, transaction_log_gc) {
+ auto item = std::make_shared<MediaAnalyticsItem>("Key1");
+ (*item).set("one", (int32_t)1)
+ .set("two", (int32_t)2)
+ .setTimestamp(10);
+
+ android::mediametrics::TransactionLog transactionLog(1, 2); // keep at most 2 items
+ ASSERT_EQ((size_t)0, transactionLog.size());
+
+ ASSERT_EQ(NO_ERROR, transactionLog.put(item));
+ ASSERT_EQ((size_t)1, transactionLog.size());
+
+ auto item2 = std::make_shared<MediaAnalyticsItem>("Key2");
+ (*item2).set("three", (int32_t)3)
+ .set("[Key1]three", (int32_t)3)
+ .setTimestamp(11);
+
+ ASSERT_EQ(NO_ERROR, transactionLog.put(item2));
+ ASSERT_EQ((size_t)2, transactionLog.size());
+
+ auto item3 = std::make_shared<MediaAnalyticsItem>("Key3");
+ (*item3).set("six", (int32_t)6)
+ .set("[Key1]four", (int32_t)4) // affects Key1
+ .set("[Key1]five", (int32_t)5) // affects key1
+ .setTimestamp(12);
+
+ ASSERT_EQ(NO_ERROR, transactionLog.put(item3));
+ ASSERT_EQ((size_t)2, transactionLog.size());
+}
+
+TEST(mediametrics_tests, audio_analytics_permission) {
+ auto item = std::make_shared<MediaAnalyticsItem>("audio.1");
+ (*item).set("one", (int32_t)1)
+ .set("two", (int32_t)2)
+ .setTimestamp(10);
+
+ auto item2 = std::make_shared<MediaAnalyticsItem>("audio.1");
+ (*item2).set("three", (int32_t)3)
+ .setTimestamp(11);
+
+ auto item3 = std::make_shared<MediaAnalyticsItem>("audio.2");
+ (*item3).set("four", (int32_t)4)
+ .setTimestamp(12);
+
+ android::mediametrics::AudioAnalytics audioAnalytics;
+
+ // untrusted entities cannot create a new key.
+ ASSERT_EQ(PERMISSION_DENIED, audioAnalytics.submit(item, false /* isTrusted */));
+ ASSERT_EQ(PERMISSION_DENIED, audioAnalytics.submit(item2, false /* isTrusted */));
+
+ // TODO: Verify contents of AudioAnalytics.
+ // Currently there is no getter API in AudioAnalytics besides dump.
+ ASSERT_EQ(4, audioAnalytics.dump(1000).second /* lines */);
+
+ ASSERT_EQ(NO_ERROR, audioAnalytics.submit(item, true /* isTrusted */));
+ // untrusted entities can add to an existing key
+ ASSERT_EQ(NO_ERROR, audioAnalytics.submit(item2, false /* isTrusted */));
+
+ // Check that we have some info in the dump.
+ ASSERT_LT(4, audioAnalytics.dump(1000).second /* lines */);
+}
+
+TEST(mediametrics_tests, audio_analytics_dump) {
+ auto item = std::make_shared<MediaAnalyticsItem>("audio.1");
+ (*item).set("one", (int32_t)1)
+ .set("two", (int32_t)2)
+ .setTimestamp(10);
+
+ auto item2 = std::make_shared<MediaAnalyticsItem>("audio.1");
+ (*item2).set("three", (int32_t)3)
+ .setTimestamp(11);
+
+ auto item3 = std::make_shared<MediaAnalyticsItem>("audio.2");
+ (*item3).set("four", (int32_t)4)
+ .setTimestamp(12);
+
+ android::mediametrics::AudioAnalytics audioAnalytics;
+
+ ASSERT_EQ(NO_ERROR, audioAnalytics.submit(item, true /* isTrusted */));
+ // untrusted entities can add to an existing key
+ ASSERT_EQ(NO_ERROR, audioAnalytics.submit(item2, false /* isTrusted */));
+ ASSERT_EQ(NO_ERROR, audioAnalytics.submit(item3, true /* isTrusted */));
+
+ // find out how many lines we have.
+ auto [string, lines] = audioAnalytics.dump(1000);
+ ASSERT_EQ(lines, (int32_t) countNewlines(string.c_str()));
+
+ printf("AudioAnalytics: %s", string.c_str());
+ // ensure that dump operates over those lines.
+ for (int32_t ll = 0; ll < lines; ++ll) {
+ auto [s, l] = audioAnalytics.dump(ll);
+ ASSERT_EQ(ll, l);
+ ASSERT_EQ(ll, (int32_t) countNewlines(s.c_str()));
+ }
+}