Improve ReportPerformance::Histogram implementation and add drawing

Test: dumpsys media.log -r
Bug: 68148948
Change-Id: Idde42ba6b152f15c8c1834c8eea4d7ca24b4c934
diff --git a/media/libnblog/PerformanceAnalysis.cpp b/media/libnblog/PerformanceAnalysis.cpp
index 46912d3..e91a511 100644
--- a/media/libnblog/PerformanceAnalysis.cpp
+++ b/media/libnblog/PerformanceAnalysis.cpp
@@ -22,6 +22,7 @@
 #include <algorithm>
 #include <climits>
 #include <deque>
+#include <iomanip>
 #include <math.h>
 #include <numeric>
 #include <sstream>
@@ -51,23 +52,21 @@
 
 void Histogram::add(double value)
 {
-    // TODO Handle domain and range error exceptions?
-    const int binIndex = lround((value - mLow) / mBinSize);
-    if (binIndex < 0) {
-        mLowCount++;
-    } else if (binIndex >= mNumBins) {
-        mHighCount++;
-    } else {
-        mBins[binIndex]++;
+    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);
-    mLowCount = 0;
-    mHighCount = 0;
     mTotalCount = 0;
 }
 
@@ -81,31 +80,79 @@
     static constexpr char kDivider = '|';
     ss << kVersion << "," << mBinSize << "," << mNumBins << "," << mLow << ",{";
     bool first = true;
-    if (mLowCount != 0) {
-        ss << "-1" << kDivider << mLowCount;
-        first = false;
-    }
-    for (size_t i = 0; i < mNumBins; i++) {
+    for (size_t i = 0; i < mBins.size(); i++) {
         if (mBins[i] != 0) {
             if (!first) {
                 ss << ",";
             }
-            ss << i << kDivider << mBins[i];
+            ss << static_cast<int>(i) - 1 << kDivider << mBins[i];
             first = false;
         }
     }
-    if (mHighCount != 0) {
-        if (!first) {
-            ss << ",";
-        }
-        ss << mNumBins << kDivider << mHighCount;
-        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
diff --git a/media/libnblog/include/media/nblog/PerformanceAnalysis.h b/media/libnblog/include/media/nblog/PerformanceAnalysis.h
index 983b28a..37a30aa 100644
--- a/media/libnblog/include/media/nblog/PerformanceAnalysis.h
+++ b/media/libnblog/include/media/nblog/PerformanceAnalysis.h
@@ -58,16 +58,16 @@
     /**
      * \brief Creates a Histogram object.
      *
-     * \param binSize the width of each bin of the histogram.
+     * \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.
+     * \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) {}
+        : mBinSize(binSize), mNumBins(numBins), mLow(low), mBins(mNumBins + 2) {}
 
     Histogram(const Config &c)
         : Histogram(c.binSize, c.numBins, c.low) {}
@@ -112,18 +112,24 @@
      */
     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 histogram range
-    const double mLow;          // Lower bound of values
-    std::vector<int> mBins;     // Data structure to store the actual histogram
+    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
 
-    int mLowCount = 0;          // Number of values less than mLow
-    int mHighCount = 0;         // Number of values >= mLow + mBinSize * mNumBins
-    uint64_t mTotalCount = 0;   // Total number of values recorded
+    // 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