1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
|
/*
* 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.
*/
#define LOG_TAG "PerformanceAnalysis"
// #define LOG_NDEBUG 0
// #define WRITE_TO_FILE
#include <algorithm>
#include <climits>
#include <deque>
#include <iomanip>
#include <math.h>
#include <numeric>
#include <sstream>
#include <string>
#include <vector>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/prctl.h>
#include <time.h>
#include <new>
#include <audio_utils/LogPlot.h>
#include <audio_utils/roundup.h>
#include <media/nblog/NBLog.h>
#include <media/nblog/PerformanceAnalysis.h>
#include <media/nblog/ReportPerformance.h>
#include <utils/Log.h>
#include <utils/String8.h>
#include <utils/Timers.h>
#include <queue>
#include <utility>
namespace android {
namespace ReportPerformance {
void Histogram::add(double value)
{
if (mBinSize <= 0 || mBins.size() < 2) {
return;
}
// TODO Handle domain and range error exceptions?
const int unboundedIndex = lround((value - mLow) / mBinSize) + 1;
// std::clamp is introduced in C++17
//const int index = std::clamp(unboundedIndex, 0, (int)(mBins.size() - 1));
const int index = std::max(0, std::min((int)(mBins.size() - 1), unboundedIndex));
mBins[index]++;
mTotalCount++;
}
void Histogram::clear()
{
std::fill(mBins.begin(), mBins.end(), 0);
mTotalCount = 0;
}
uint64_t Histogram::totalCount() const
{
return mTotalCount;
}
std::string Histogram::toString() const {
std::stringstream ss;
static constexpr char kDivider = '|';
ss << kVersion << "," << mBinSize << "," << mNumBins << "," << mLow << ",{";
bool first = true;
for (size_t i = 0; i < mBins.size(); i++) {
if (mBins[i] != 0) {
if (!first) {
ss << ",";
}
ss << static_cast<int>(i) - 1 << kDivider << mBins[i];
first = false;
}
}
ss << "}";
return ss.str();
}
std::string Histogram::asciiArtString(size_t indent) const {
if (totalCount() == 0 || mBinSize <= 0 || mBins.size() < 2) {
return "";
}
static constexpr char kMarker = '-';
// One increment is considered one step of a bin's height.
static constexpr size_t kMarkersPerIncrement = 2;
static constexpr size_t kMaxIncrements = 64 + 1;
static constexpr size_t kMaxNumberWidth = 7;
static const std::string kMarkers(kMarkersPerIncrement * kMaxIncrements, kMarker);
static const std::string kSpaces(kMarkersPerIncrement * kMaxIncrements, ' ');
// get the last n characters of s, or the whole string if it is shorter
auto getTail = [](const size_t n, const std::string &s) {
return s.c_str() + s.size() - std::min(n, s.size());
};
// Since totalCount() > 0, mBins is not empty and maxCount > 0.
const unsigned maxCount = *std::max_element(mBins.begin(), mBins.end());
const size_t maxIncrements = log2(maxCount) + 1;
std::stringstream ss;
// Non-zero bins must exist at this point because totalCount() > 0.
size_t firstNonZeroBin = 0;
// If firstNonZeroBin reaches mBins.size() - 1, then it must be a nonzero bin.
for (; firstNonZeroBin < mBins.size() - 1 && mBins[firstNonZeroBin] == 0; firstNonZeroBin++) {}
const size_t firstBinToPrint = firstNonZeroBin == 0 ? 0 : firstNonZeroBin - 1;
size_t lastNonZeroBin = mBins.size() - 1;
// If lastNonZeroBin reaches 0, then it must be a nonzero bin.
for (; lastNonZeroBin > 0 && mBins[lastNonZeroBin] == 0; lastNonZeroBin--) {}
const size_t lastBinToPrint = lastNonZeroBin == mBins.size() - 1 ? lastNonZeroBin
: lastNonZeroBin + 1;
for (size_t bin = firstBinToPrint; bin <= lastBinToPrint; bin++) {
ss << std::setw(indent + kMaxNumberWidth);
if (bin == 0) {
ss << "<";
} else if (bin == mBins.size() - 1) {
ss << ">";
} else {
ss << mLow + (bin - 1) * mBinSize;
}
ss << " |";
size_t increments = 0;
const uint64_t binCount = mBins[bin];
if (binCount > 0) {
increments = log2(binCount) + 1;
ss << getTail(increments * kMarkersPerIncrement, kMarkers);
}
ss << getTail((maxIncrements - increments + 1) * kMarkersPerIncrement, kSpaces)
<< binCount << "\n";
}
ss << "\n";
return ss.str();
}
//------------------------------------------------------------------------------
// Given an audio processing wakeup timestamp, buckets the time interval
// since the previous timestamp into a histogram, searches for
// outliers, analyzes the outlier series for unexpectedly
// small or large values and stores these as peaks
void PerformanceAnalysis::logTsEntry(timestamp ts) {
// after a state change, start a new series and do not
// record time intervals in-between
if (mBufferPeriod.mPrevTs == 0) {
mBufferPeriod.mPrevTs = ts;
return;
}
// calculate time interval between current and previous timestamp
const msInterval diffMs = static_cast<msInterval>(
deltaMs(mBufferPeriod.mPrevTs, ts));
const int diffJiffy = deltaJiffy(mBufferPeriod.mPrevTs, ts);
// old versus new weight ratio when updating the buffer period mean
static constexpr double exponentialWeight = 0.999;
// update buffer period mean with exponential weighting
mBufferPeriod.mMean = (mBufferPeriod.mMean < 0) ? diffMs :
exponentialWeight * mBufferPeriod.mMean + (1.0 - exponentialWeight) * diffMs;
// set mOutlierFactor to a smaller value for the fastmixer thread
const int kFastMixerMax = 10;
// NormalMixer times vary much more than FastMixer times.
// TODO: mOutlierFactor values are set empirically based on what appears to be
// an outlier. Learn these values from the data.
mBufferPeriod.mOutlierFactor = mBufferPeriod.mMean < kFastMixerMax ? 1.8 : 2.0;
// set outlier threshold
mBufferPeriod.mOutlier = mBufferPeriod.mMean * mBufferPeriod.mOutlierFactor;
// Check whether the time interval between the current timestamp
// and the previous one is long enough to count as an outlier
const bool isOutlier = detectAndStoreOutlier(diffMs);
// If an outlier was found, check whether it was a peak
if (isOutlier) {
/*bool isPeak =*/ detectAndStorePeak(
mOutlierData[0].first, mOutlierData[0].second);
// TODO: decide whether to insert a new empty histogram if a peak
// TODO: remove isPeak if unused to avoid "unused variable" error
// occurred at the current timestamp
}
// Insert a histogram to mHists if it is empty, or
// close the current histogram and insert a new empty one if
// if the current histogram has spanned its maximum time interval.
if (mHists.empty() ||
deltaMs(mHists[0].first, ts) >= kMaxLength.HistTimespanMs) {
mHists.emplace_front(ts, std::map<int, int>());
// When memory is full, delete oldest histogram
// TODO: use a circular buffer
if (mHists.size() >= kMaxLength.Hists) {
mHists.resize(kMaxLength.Hists);
}
}
// add current time intervals to histogram
++mHists[0].second[diffJiffy];
// update previous timestamp
mBufferPeriod.mPrevTs = ts;
}
// forces short-term histogram storage to avoid adding idle audio time interval
// to buffer period data
void PerformanceAnalysis::handleStateChange() {
mBufferPeriod.mPrevTs = 0;
return;
}
// Checks whether the time interval between two outliers is far enough from
// a typical delta to be considered a peak.
// looks for changes in distribution (peaks), which can be either positive or negative.
// The function sets the mean to the starting value and sigma to 0, and updates
// them as long as no peak is detected. When a value is more than 'threshold'
// standard deviations from the mean, a peak is detected and the mean and sigma
// are set to the peak value and 0.
bool PerformanceAnalysis::detectAndStorePeak(msInterval diff, timestamp ts) {
bool isPeak = false;
if (mOutlierData.empty()) {
return false;
}
// Update mean of the distribution
// TypicalDiff is used to check whether a value is unusually large
// when we cannot use standard deviations from the mean because the sd is set to 0.
mOutlierDistribution.mTypicalDiff = (mOutlierDistribution.mTypicalDiff *
(mOutlierData.size() - 1) + diff) / mOutlierData.size();
// Initialize short-term mean at start of program
if (mOutlierDistribution.mMean == 0) {
mOutlierDistribution.mMean = diff;
}
// Update length of current sequence of outliers
mOutlierDistribution.mN++;
// Check whether a large deviation from the mean occurred.
// If the standard deviation has been reset to zero, the comparison is
// instead to the mean of the full mOutlierInterval sequence.
if ((fabs(diff - mOutlierDistribution.mMean) <
mOutlierDistribution.kMaxDeviation * mOutlierDistribution.mSd) ||
(mOutlierDistribution.mSd == 0 &&
fabs(diff - mOutlierDistribution.mMean) <
mOutlierDistribution.mTypicalDiff)) {
// update the mean and sd using online algorithm
// https://en.wikipedia.org/wiki/
// Algorithms_for_calculating_variance#Online_algorithm
mOutlierDistribution.mN++;
const double kDelta = diff - mOutlierDistribution.mMean;
mOutlierDistribution.mMean += kDelta / mOutlierDistribution.mN;
const double kDelta2 = diff - mOutlierDistribution.mMean;
mOutlierDistribution.mM2 += kDelta * kDelta2;
mOutlierDistribution.mSd = (mOutlierDistribution.mN < 2) ? 0 :
sqrt(mOutlierDistribution.mM2 / (mOutlierDistribution.mN - 1));
} else {
// new value is far from the mean:
// store peak timestamp and reset mean, sd, and short-term sequence
isPeak = true;
mPeakTimestamps.emplace_front(ts);
// if mPeaks has reached capacity, delete oldest data
// Note: this means that mOutlierDistribution values do not exactly
// match the data we have in mPeakTimestamps, but this is not an issue
// in practice for estimating future peaks.
// TODO: turn this into a circular buffer
if (mPeakTimestamps.size() >= kMaxLength.Peaks) {
mPeakTimestamps.resize(kMaxLength.Peaks);
}
mOutlierDistribution.mMean = 0;
mOutlierDistribution.mSd = 0;
mOutlierDistribution.mN = 0;
mOutlierDistribution.mM2 = 0;
}
return isPeak;
}
// Determines whether the difference between a timestamp and the previous
// one is beyond a threshold. If yes, stores the timestamp as an outlier
// and writes to mOutlierdata in the following format:
// Time elapsed since previous outlier: Timestamp of start of outlier
// e.g. timestamps (ms) 1, 4, 5, 16, 18, 28 will produce pairs (4, 5), (13, 18).
// TODO: learn what timestamp sequences correlate with glitches instead of
// manually designing a heuristic.
bool PerformanceAnalysis::detectAndStoreOutlier(const msInterval diffMs) {
bool isOutlier = false;
if (diffMs >= mBufferPeriod.mOutlier) {
isOutlier = true;
mOutlierData.emplace_front(
mOutlierDistribution.mElapsed, mBufferPeriod.mPrevTs);
// Remove oldest value if the vector is full
// TODO: turn this into a circular buffer
// TODO: make sure kShortHistSize is large enough that that data will never be lost
// before being written to file or to a FIFO
if (mOutlierData.size() >= kMaxLength.Outliers) {
mOutlierData.resize(kMaxLength.Outliers);
}
mOutlierDistribution.mElapsed = 0;
}
mOutlierDistribution.mElapsed += diffMs;
return isOutlier;
}
// rounds value to precision based on log-distance from mean
__attribute__((no_sanitize("signed-integer-overflow")))
inline double logRound(double x, double mean) {
// Larger values decrease range of high resolution and prevent overflow
// of a histogram on the console.
// The following formula adjusts kBase based on the buffer period length.
// Different threads have buffer periods ranging from 2 to 40. The
// formula below maps buffer period 2 to kBase = ~1, 4 to ~2, 20 to ~3, 40 to ~4.
// TODO: tighten this for higher means, the data still overflows
const double kBase = log(mean) / log(2.2);
const double power = floor(
log(abs(x - mean) / mean) / log(kBase)) + 2;
// do not round values close to the mean
if (power < 1) {
return x;
}
const int factor = static_cast<int>(pow(10, power));
return (static_cast<int>(x) * factor) / factor;
}
// TODO Make it return a std::string instead of modifying body
// TODO: move this to ReportPerformance, probably make it a friend function
// of PerformanceAnalysis
void PerformanceAnalysis::reportPerformance(String8 *body, int author, log_hash_t hash,
int maxHeight) {
if (mHists.empty() || body == nullptr) {
return;
}
// ms of active audio in displayed histogram
double elapsedMs = 0;
// starting timestamp of histogram
timestamp startingTs = mHists[0].first;
// histogram which stores .1 precision ms counts instead of Jiffy multiple counts
std::map<double, int> buckets;
for (const auto &shortHist: mHists) {
for (const auto &countPair : shortHist.second) {
const double ms = static_cast<double>(countPair.first) / kJiffyPerMs;
buckets[logRound(ms, mBufferPeriod.mMean)] += countPair.second;
elapsedMs += ms * countPair.second;
}
}
static const int SIZE = 128;
char title[SIZE];
snprintf(title, sizeof(title), "\n%s %3.2f %s\n%s%d, %lld, %lld\n",
"Occurrences in", (elapsedMs / kMsPerSec), "seconds of audio:",
"Thread, hash, starting timestamp: ", author,
static_cast<long long>(hash), static_cast<long long>(startingTs));
static const char * const kLabel = "ms";
body->appendFormat("%s",
audio_utils_plot_histogram(buckets, title, kLabel, maxHeight).c_str());
// Now report glitches
body->appendFormat("\ntime elapsed between glitches and glitch timestamps:\n");
for (const auto &outlier: mOutlierData) {
body->appendFormat("%lld: %lld\n", static_cast<long long>(outlier.first),
static_cast<long long>(outlier.second));
}
}
//------------------------------------------------------------------------------
// writes summary of performance into specified file descriptor
void dump(int fd, int indent, PerformanceAnalysisMap &threadPerformanceAnalysis) {
String8 body;
#ifdef WRITE_TO_FILE
const char* const kDirectory = "/data/misc/audioserver/";
#endif
for (auto & thread : threadPerformanceAnalysis) {
for (auto & hash: thread.second) {
PerformanceAnalysis& curr = hash.second;
// write performance data to console
curr.reportPerformance(&body, thread.first, hash.first);
if (!body.isEmpty()) {
dumpLine(fd, indent, body);
body.clear();
}
#ifdef WRITE_TO_FILE
// write to file. Enable by uncommenting macro at top of file.
writeToFile(curr.mHists, curr.mOutlierData, curr.mPeakTimestamps,
kDirectory, false, thread.first, hash.first);
#endif
}
}
}
// Writes a string into specified file descriptor
void dumpLine(int fd, int indent, const String8 &body) {
dprintf(fd, "%.*s%s \n", indent, "", body.string());
}
} // namespace ReportPerformance
} // namespace android
|