/*
* Copyright (C) 2017 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.
*/
#ifndef ANDROID_MEDIA_PERFORMANCEANALYSIS_H
#define ANDROID_MEDIA_PERFORMANCEANALYSIS_H
#include <deque>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include <media/nblog/Events.h>
#include <media/nblog/ReportPerformance.h>
#include <utils/Timers.h>
namespace android {
class String8;
namespace ReportPerformance {
// TODO make this a templated class and put it in a separate file.
// The templated parameters would be bin size and low limit.
/*
* Histogram provides a way to store numeric data in histogram format and read it as a serialized
* string. The terms "bin" and "bucket" are used interchangeably.
*
* This class is not thread-safe.
*/
class Histogram {
public:
struct Config {
const double binSize; // TODO template type
const size_t numBins;
const double low; // TODO template type
};
// Histograms are constructed with fixed configuration numbers. Dynamic configuration based
// the data is possible but complex because
// - data points are added one by one, not processed as a batch.
// - Histograms with different configuration parameters are tricky to aggregate, and they
// will need to be aggregated at the Media Metrics cloud side.
// - not providing limits theoretically allows for infinite number of buckets.
/**
* \brief Creates a Histogram object.
*
* \param binSize the width of each bin of the histogram, must be greater than 0.
* Units are whatever data the caller decides to store.
* \param numBins the number of bins desired in the histogram range, must be greater than 0.
* \param low the lower bound of the histogram bucket values.
* Units are whatever data the caller decides to store.
* Note that the upper bound can be calculated by the following:
* upper = lower + binSize * numBins.
*/
Histogram(double binSize, size_t numBins, double low = 0.)
: mBinSize(binSize), mNumBins(numBins), mLow(low), mBins(mNumBins + 2) {}
Histogram(const Config &c)
: Histogram(c.binSize, c.numBins, c.low) {}
/**
* \brief Add a data point to the histogram. The value of the data point
* is rounded to the nearest multiple of the bin size (before accounting
* for the lower bound offset, which may not be a multiple of the bin size).
*
* \param value the value of the data point to add.
*/
void add(double value);
/**
* \brief Removes all data points from the histogram.
*/
void clear();
/**
* \brief Returns the total number of data points added to the histogram.
*
* \return the total number of data points in the histogram.
*/
uint64_t totalCount() const;
/**
* \brief Serializes the histogram into a string. The format is chosen to be compatible with
* the histogram representation to send to the Media Metrics service.
*
* The string is as follows:
* binSize,numBins,low,{-1|lowCount,...,binIndex|count,...,numBins|highCount}
*
* - binIndex is an integer with 0 <= binIndex < numBins.
* - count is the number of occurrences of the (rounded) value
* low + binSize * bucketIndex.
* - lowCount is the number of (rounded) values less than low.
* - highCount is the number of (rounded) values greater than or equal to
* low + binSize * numBins.
* - a binIndex may be skipped if its count is 0.
*
* \return the histogram serialized as a string.
*/
std::string toString() const;
// Draw log scale sideways histogram as ASCII art and store as a std::string.
// Empty string is returned if totalCount() == 0.
std::string asciiArtString(size_t indent = 0) const;
private:
// Histogram version number.
static constexpr int kVersion = 1;
const double mBinSize; // Size of each bucket
const size_t mNumBins; // Number of buckets in range (excludes low and high)
const double mLow; // Lower bound of values
// Data structure to store the actual histogram. Counts of bin values less than mLow
// are stored in mBins[0]. Bin index i corresponds to mBins[i+1]. Counts of bin values
// >= high are stored in mBins[mNumBins + 1].
std::vector<uint64_t> mBins;
uint64_t mTotalCount = 0; // Total number of values recorded
};
// This is essentially the same as class PerformanceAnalysis, but PerformanceAnalysis
// also does some additional analyzing of data, while the purpose of this struct is
// to hold data.
struct PerformanceData {
// TODO the Histogram::Config numbers below are for FastMixer.
// Specify different numbers for other thread types.
// Values based on mUnderrunNs and mOverrunNs in FastMixer.cpp for frameCount = 192
// and mSampleRate = 48000, which correspond to 2 and 7 seconds.
static constexpr Histogram::Config kWorkConfig = { 0.25, 20, 2.};
// Values based on trial and error logging. Need a better way to determine
// bin size and lower/upper limits.
static constexpr Histogram::Config kLatencyConfig = { 2., 10, 10.};
// Values based on trial and error logging. Need a better way to determine
// bin size and lower/upper limits.
static constexpr Histogram::Config kWarmupConfig = { 5., 10, 10.};
NBLog::thread_info_t threadInfo{};
NBLog::thread_params_t threadParams{};
// Performance Data
Histogram workHist{kWorkConfig};
Histogram latencyHist{kLatencyConfig};
Histogram warmupHist{kWarmupConfig};
int64_t underruns = 0;
static constexpr size_t kMaxSnapshotsToStore = 256;
std::deque<std::pair<NBLog::Event, int64_t /*timestamp*/>> snapshots;
int64_t overruns = 0;
nsecs_t active = 0;
nsecs_t start{systemTime()};
// Reset the performance data. This does not represent a thread state change.
// Thread info is not reset here because the data is meant to be a continuation of the thread
// that struct PerformanceData is associated with.
void reset() {
workHist.clear();
latencyHist.clear();
warmupHist.clear();
underruns = 0;
overruns = 0;
active = 0;
start = systemTime();
}
// Return true if performance data has not been recorded yet, false otherwise.
bool empty() const {
return workHist.totalCount() == 0 && latencyHist.totalCount() == 0
&& warmupHist.totalCount() == 0 && underruns == 0 && overruns == 0
&& active == 0;
}
};
//------------------------------------------------------------------------------
class PerformanceAnalysis;
// a map of PerformanceAnalysis instances
// The outer key is for the thread, the inner key for the source file location.
using PerformanceAnalysisMap = std::map<int, std::map<log_hash_t, PerformanceAnalysis>>;
class PerformanceAnalysis {
// This class stores and analyzes audio processing wakeup timestamps from NBLog
// FIXME: currently, all performance data is stored in deques. Turn these into circular
// buffers.
// TODO: add a mutex.
public:
PerformanceAnalysis() {};
friend void dump(int fd, int indent,
PerformanceAnalysisMap &threadPerformanceAnalysis);
// Called in the case of an audio on/off event, e.g., EVENT_AUDIO_STATE.
// Used to discard idle time intervals
void handleStateChange();
// Writes wakeup timestamp entry to log and runs analysis
void logTsEntry(timestamp ts);
// FIXME: make peakdetector and storeOutlierData a single function
// Input: mOutlierData. Looks at time elapsed between outliers
// finds significant changes in the distribution
// writes timestamps of significant changes to mPeakTimestamps
bool detectAndStorePeak(msInterval delta, timestamp ts);
// stores timestamps of intervals above a threshold: these are assumed outliers.
// writes to mOutlierData <time elapsed since previous outlier, outlier timestamp>
bool detectAndStoreOutlier(const msInterval diffMs);
// Generates a string of analysis of the buffer periods and prints to console
// FIXME: move this data visualization to a separate class. Model/view/controller
void reportPerformance(String8 *body, int author, log_hash_t hash,
int maxHeight = 10);
private:
// TODO use a circular buffer for the deques and vectors below
// stores outlier analysis:
// <elapsed time between outliers in ms, outlier beginning timestamp>
std::deque<std::pair<msInterval, timestamp>> mOutlierData;
// stores each timestamp at which a peak was detected
// a peak is a moment at which the average outlier interval changed significantly
std::deque<timestamp> mPeakTimestamps;
// stores buffer period histograms with timestamp of first sample
std::deque<std::pair<timestamp, Hist>> mHists;
// Parameters used when detecting outliers
struct BufferPeriod {
double mMean = -1; // average time between audio processing wakeups
double mOutlierFactor = -1; // values > mMean * mOutlierFactor are outliers
double mOutlier = -1; // this is set to mMean * mOutlierFactor
timestamp mPrevTs = -1; // previous timestamp
} mBufferPeriod;
// capacity allocated to data structures
struct MaxLength {
size_t Hists; // number of histograms stored in memory
size_t Outliers; // number of values stored in outlier array
size_t Peaks; // number of values stored in peak array
int HistTimespanMs; // maximum histogram timespan
};
// These values allow for 10 hours of data allowing for a glitch and a peak
// as often as every 3 seconds
static constexpr MaxLength kMaxLength = {.Hists = 60, .Outliers = 12000,
.Peaks = 12000, .HistTimespanMs = 10 * kSecPerMin * kMsPerSec };
// these variables ensure continuity while analyzing the timestamp
// series one sample at a time.
// TODO: change this to a running variance/mean class
struct OutlierDistribution {
msInterval mMean = 0; // sample mean since previous peak
msInterval mSd = 0; // sample sd since previous peak
msInterval mElapsed = 0; // time since previous detected outlier
const int kMaxDeviation = 5; // standard deviations from the mean threshold
msInterval mTypicalDiff = 0; // global mean of outliers
double mN = 0; // length of sequence since the last peak
double mM2 = 0; // used to calculate sd
} mOutlierDistribution;
};
void dump(int fd, int indent, PerformanceAnalysisMap &threadPerformanceAnalysis);
void dumpLine(int fd, int indent, const String8 &body);
} // namespace ReportPerformance
} // namespace android
#endif // ANDROID_MEDIA_PERFORMANCEANALYSIS_H