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