[WifiScoreCard] Add histograms

Add histogram support.
For now, the histograms are not persisted.

Also remove constructors that accept object representing protobuf;
just use the merge methods instead.

Bug: 136675430
Test: WifiScoreCardTest
Change-Id: I4001a074b377dd6225abe8a88636e1d6e012b533
diff --git a/service/java/com/android/server/wifi/WifiScoreCard.java b/service/java/com/android/server/wifi/WifiScoreCard.java
index af995fd..77abdb1 100644
--- a/service/java/com/android/server/wifi/WifiScoreCard.java
+++ b/service/java/com/android/server/wifi/WifiScoreCard.java
@@ -33,11 +33,13 @@
 import com.android.internal.util.Preconditions;
 import com.android.server.wifi.WifiScoreCardProto.AccessPoint;
 import com.android.server.wifi.WifiScoreCardProto.Event;
+import com.android.server.wifi.WifiScoreCardProto.HistogramBucket;
 import com.android.server.wifi.WifiScoreCardProto.Network;
 import com.android.server.wifi.WifiScoreCardProto.NetworkList;
 import com.android.server.wifi.WifiScoreCardProto.SecurityType;
 import com.android.server.wifi.WifiScoreCardProto.Signal;
 import com.android.server.wifi.WifiScoreCardProto.UnivariateStatistic;
+import com.android.server.wifi.util.IntHistogram;
 import com.android.server.wifi.util.NativeUtil;
 
 import com.google.protobuf.ByteString;
@@ -68,12 +70,21 @@
     private static final String TAG = "WifiScoreCard";
     private static final boolean DBG = false;
 
+    @VisibleForTesting
+    boolean mPersistentHistograms = false; // not ready yet
+
     private static final int TARGET_IN_MEMORY_ENTRIES = 50;
 
     private final Clock mClock;
     private final String mL2KeySeed;
     private MemoryStore mMemoryStore;
 
+    @VisibleForTesting
+    static final int[] RSSI_BUCKETS =
+            {-99, -88, -87, -86, -85, -84, -83, -82, -81, -80, -79, -78, -77, -76, -75, -74, -73,
+                    -72, -71, -70, -66, -55};
+
+
     /** Our view of the memory store */
     public interface MemoryStore {
         /** Requests a read, with asynchronous reply */
@@ -393,17 +404,19 @@
             PerSignal perSignal = lookupSignal(event, frequency);
             if (rssi != INVALID_RSSI) {
                 perSignal.rssi.update(rssi);
+                changed = true;
             }
             if (linkspeed > 0) {
                 perSignal.linkspeed.update(linkspeed);
+                changed = true;
             }
             if (perSignal.elapsedMs != null && mTsConnectionAttemptStart > TS_NONE) {
                 long millis = mClock.getElapsedSinceBootMillis() - mTsConnectionAttemptStart;
                 if (millis >= 0) {
                     perSignal.elapsedMs.update(millis);
+                    changed = true;
                 }
             }
-            changed = true;
         }
         PerSignal lookupSignal(Event event, int frequency) {
             finishPendingRead();
@@ -470,7 +483,8 @@
                 Pair<Event, Integer> key = new Pair<>(signal.getEvent(), signal.getFrequency());
                 PerSignal perSignal = mSignalForEventAndFrequency.get(key);
                 if (perSignal == null) {
-                    mSignalForEventAndFrequency.put(key, new PerSignal(signal));
+                    mSignalForEventAndFrequency.put(key,
+                            new PerSignal(key.first, key.second).merge(signal));
                     // No need to set changed for this, since we are in sync with what's stored
                 } else {
                     perSignal.merge(signal);
@@ -690,7 +704,8 @@
         PerSignal(Event event, int frequency) {
             this.event = event;
             this.frequency = frequency;
-            this.rssi = new PerUnivariateStatistic();
+            // TODO(b/136675430) - histograms not needed for all events?
+            this.rssi = new PerUnivariateStatistic(RSSI_BUCKETS);
             this.linkspeed = new PerUnivariateStatistic();
             switch (event) {
                 case FIRST_POLL_AFTER_CONNECTION:
@@ -706,18 +721,7 @@
                     break;
             }
         }
-        PerSignal(Signal signal) {
-            this.event = signal.getEvent();
-            this.frequency = signal.getFrequency();
-            this.rssi = new PerUnivariateStatistic(signal.getRssi());
-            this.linkspeed = new PerUnivariateStatistic(signal.getLinkspeed());
-            if (signal.hasElapsedMs()) {
-                this.elapsedMs = new PerUnivariateStatistic(signal.getElapsedMs());
-            } else {
-                this.elapsedMs = null;
-            }
-        }
-        void merge(Signal signal) {
+        PerSignal merge(Signal signal) {
             Preconditions.checkArgument(event == signal.getEvent());
             Preconditions.checkArgument(frequency == signal.getFrequency());
             rssi.merge(signal.getRssi());
@@ -725,6 +729,7 @@
             if (signal.hasElapsedMs()) {
                 elapsedMs.merge(signal.getElapsedMs());
             }
+            return this;
         }
         Signal toSignal() {
             Signal.Builder builder = Signal.newBuilder();
@@ -735,6 +740,9 @@
             if (elapsedMs != null) {
                 builder.setElapsedMs(elapsedMs.toUnivariateStatistic());
             }
+            if (DBG && rssi.intHistogram != null && rssi.intHistogram.numNonEmptyBuckets() > 0) {
+                Log.d(TAG, "Histogram " + event + " RSSI" + rssi.intHistogram);
+            }
             return builder.build();
         }
     }
@@ -747,25 +755,10 @@
         public double maxValue = Double.NEGATIVE_INFINITY;
         public double historicalMean = 0.0;
         public double historicalVariance = Double.POSITIVE_INFINITY;
+        public IntHistogram intHistogram = null;
         PerUnivariateStatistic() {}
-        PerUnivariateStatistic(UnivariateStatistic stats) {
-            if (stats.hasCount()) {
-                this.count = stats.getCount();
-                this.sum = stats.getSum();
-                this.sumOfSquares = stats.getSumOfSquares();
-            }
-            if (stats.hasMinValue()) {
-                this.minValue = stats.getMinValue();
-            }
-            if (stats.hasMaxValue()) {
-                this.maxValue = stats.getMaxValue();
-            }
-            if (stats.hasHistoricalMean()) {
-                this.historicalMean = stats.getHistoricalMean();
-            }
-            if (stats.hasHistoricalVariance()) {
-                this.historicalVariance = stats.getHistoricalVariance();
-            }
+        PerUnivariateStatistic(int[] bucketBoundaries) {
+            intHistogram = new IntHistogram(bucketBoundaries);
         }
         void update(double value) {
             count++;
@@ -773,6 +766,9 @@
             sumOfSquares += value * value;
             minValue = Math.min(minValue, value);
             maxValue = Math.max(maxValue, value);
+            if (intHistogram != null) {
+                intHistogram.add(Math.round((float) value), 1);
+            }
         }
         void age() {
             //TODO  Fold the current stats into the historical stats
@@ -806,6 +802,18 @@
                     historicalVariance = stats.getHistoricalVariance();
                 }
             }
+            if (intHistogram != null) {
+                for (HistogramBucket bucket : stats.getBucketsList()) {
+                    long low = bucket.getLow();
+                    long count = bucket.getNumber();
+                    if (low != (int) low || count != (int) count || count < 0) {
+                        Log.e(TAG, "Found corrupted histogram! Clearing.");
+                        intHistogram.clear();
+                        break;
+                    }
+                    intHistogram.add((int) low, (int) count);
+                }
+            }
         }
         UnivariateStatistic toUnivariateStatistic() {
             UnivariateStatistic.Builder builder = UnivariateStatistic.newBuilder();
@@ -820,6 +828,14 @@
                 builder.setHistoricalMean(historicalMean)
                         .setHistoricalVariance(historicalVariance);
             }
+            if (mPersistentHistograms
+                    && intHistogram != null && intHistogram.numNonEmptyBuckets() > 0) {
+                for (IntHistogram.Bucket b : intHistogram) {
+                    if (b.count == 0) continue;
+                    builder.addBuckets(
+                            HistogramBucket.newBuilder().setLow(b.start).setNumber(b.count));
+                }
+            }
             return builder.build();
         }
     }
diff --git a/service/proto/src/scorecard.proto b/service/proto/src/scorecard.proto
index 27e1e13..28a4898 100644
--- a/service/proto/src/scorecard.proto
+++ b/service/proto/src/scorecard.proto
@@ -94,8 +94,20 @@
   // more recent measurements get a higher weight.
   optional double historical_mean = 6;     // Long-term average
   optional double historical_variance = 7; // Long-term variance
+
+  // Arranged by increasing value
+  repeated HistogramBucket buckets = 8;
 };
 
+message HistogramBucket {
+  // Lower bound (inclusive) for values falling in this bucket.
+  // Compact signed encoding used here, because rssi values are negative.
+  // The upper bound is not stored explicitly.
+  optional sint64 low = 1;
+  // Number of occurences for this value or bucket.
+  optional int64 number = 2;
+}
+
 // Events where statistics may be collected
 enum Event {
   SIGNAL_POLL = 1;
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiScoreCardTest.java b/tests/wifitests/src/com/android/server/wifi/WifiScoreCardTest.java
index e649b28..b78b248 100644
--- a/tests/wifitests/src/com/android/server/wifi/WifiScoreCardTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WifiScoreCardTest.java
@@ -34,6 +34,7 @@
 import com.android.server.wifi.WifiScoreCardProto.Network;
 import com.android.server.wifi.WifiScoreCardProto.NetworkList;
 import com.android.server.wifi.WifiScoreCardProto.Signal;
+import com.android.server.wifi.util.IntHistogram;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -98,6 +99,7 @@
         mWifiInfo.setNetworkId(TEST_NETWORK_CONFIG_ID);
         millisecondsPass(0);
         mWifiScoreCard = new WifiScoreCard(mClock, "some seed");
+        mWifiScoreCard.mPersistentHistograms = true; // TODO - remove when ready
     }
 
     /**
@@ -256,6 +258,15 @@
         millisecondsPass(1000);
         mWifiInfo.setRssi(-44);
         mWifiScoreCard.noteSignalPoll(mWifiInfo);
+        mWifiInfo.setFrequency(2432);
+        for (int round = 0; round < 4; round++) {
+            for (int i = 0; i < HISTOGRAM_COUNT.length; i++) {
+                if (HISTOGRAM_COUNT[i] > round) {
+                    mWifiInfo.setRssi(HISTOGRAM_RSSI[i]);
+                    mWifiScoreCard.noteSignalPoll(mWifiInfo);
+                }
+            }
+        }
         WifiScoreCard.PerBssid perBssid = mWifiScoreCard.fetchByBssid(TEST_BSSID_1);
         perBssid.lookupSignal(Event.SIGNAL_POLL, 2412).rssi.historicalMean = -42.0;
         perBssid.lookupSignal(Event.SIGNAL_POLL, 2412).rssi.historicalVariance = 4.0;
@@ -264,6 +275,21 @@
         byte[] serialized = perBssid.toAccessPoint().toByteArray();
         return serialized;
     }
+    private static final int[] HISTOGRAM_RSSI = {-80, -79, -78};
+    private static final int[] HISTOGRAM_COUNT = {3, 1, 4};
+
+    private void checkHistogramExample(String diag, IntHistogram rssiHistogram) {
+        int i = 0;
+        for (IntHistogram.Bucket bucket : rssiHistogram) {
+            if (bucket.count != 0) {
+                assertTrue(diag, i < HISTOGRAM_COUNT.length);
+                assertEquals(diag, HISTOGRAM_RSSI[i], bucket.start);
+                assertEquals(diag, HISTOGRAM_COUNT[i], bucket.count);
+                i++;
+            }
+        }
+        assertEquals(diag, HISTOGRAM_COUNT.length, i);
+    }
 
     /**
      * Checks that the fields of the serialization example are as expected
@@ -283,6 +309,8 @@
                 .rssi.historicalMean, TOL);
         assertEquals(diag, 4.0, perBssid.lookupSignal(Event.SIGNAL_POLL, 2412)
                 .rssi.historicalVariance, TOL);
+        checkHistogramExample(diag, perBssid.lookupSignal(Event.SIGNAL_POLL,
+                2432).rssi.intHistogram);
     }
 
     /**
@@ -294,7 +322,7 @@
 
         // Verify by parsing it and checking that we see the expected results
         AccessPoint ap = AccessPoint.parseFrom(serialized);
-        assertEquals(4, ap.getEventStatsCount());
+        assertEquals(5, ap.getEventStatsCount());
         for (Signal signal: ap.getEventStatsList()) {
             if (signal.getFrequency() == 2412) {
                 assertFalse(signal.getRssi().hasCount());
@@ -302,6 +330,12 @@
                 assertEquals(4.0, signal.getRssi().getHistoricalVariance(), TOL);
                 continue;
             }
+            if (signal.getFrequency() == 2432) {
+                assertEquals(Event.SIGNAL_POLL, signal.getEvent());
+                assertEquals(HISTOGRAM_RSSI[2], signal.getRssi().getBuckets(2).getLow());
+                assertEquals(HISTOGRAM_COUNT[2], signal.getRssi().getBuckets(2).getNumber());
+                continue;
+            }
             assertEquals(5805, signal.getFrequency());
             switch (signal.getEvent()) {
                 case IP_CONFIGURATION_SUCCESS: