content: /*
   * Copyright (C) 2018 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.
   */
  
  package com.android.server.wifi.rtt;
  
  import static com.android.server.wifi.util.MetricsUtils.addValueToLinearHistogram;
  import static com.android.server.wifi.util.MetricsUtils.addValueToLogHistogram;
  import static com.android.server.wifi.util.MetricsUtils.linearHistogramToGenericBuckets;
  import static com.android.server.wifi.util.MetricsUtils.logHistogramToGenericBuckets;
  
  import android.hardware.wifi.V1_0.RttResult;
  import android.hardware.wifi.V1_0.RttStatus;
  import android.net.MacAddress;
  import android.net.wifi.rtt.RangingRequest;
  import android.net.wifi.rtt.ResponderConfig;
  import android.os.WorkSource;
  import android.util.Log;
  import android.util.SparseArray;
  import android.util.SparseIntArray;
  
  import com.android.server.wifi.Clock;
  import com.android.server.wifi.nano.WifiMetricsProto;
  import com.android.server.wifi.util.MetricsUtils;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  /**
   * Wi-Fi RTT metric container/processor.
   */
  public class RttMetrics {
      private static final String TAG = "RttMetrics";
      private static final boolean VDBG = false;
      /* package */ boolean mDbg = false;
  
      private final Object mLock = new Object();
      private final Clock mClock;
  
      // accumulated metrics data
  
      // Histogram: 7 buckets (i=0, ..., 6) of 1 slots in range 10^i -> 10^(i+1)
      // Buckets:
      //    1 -> 10
      //    10 -> 100
      //    100 -> 1000
      //    10^3 -> 10^4
      //    10^4 -> 10^5
      //    10^5 -> 10^6
      //    10^5 -> 10^7 (10^7 ms = 160 minutes)
      private static final MetricsUtils.LogHistParms COUNT_LOG_HISTOGRAM =
              new MetricsUtils.LogHistParms(0, 1, 10, 1, 7);
  
      // Histogram for ranging limits in discovery. Indicates the following 7 buckets (in meters):
      //   < 0
      //   [0, 5)
      //   [5, 15)
      //   [15, 30)
      //   [30, 60)
      //   [60, 100)
      //   >= 100
      private static final int[] DISTANCE_MM_HISTOGRAM =
              {0, 5 * 1000, 15 * 1000, 30 * 1000, 60 * 1000, 100 * 1000};
  
      private static final int PEER_AP = 0;
      private static final int PEER_AWARE = 1;
  
      private int mNumStartRangingCalls = 0;
      private SparseIntArray mOverallStatusHistogram = new SparseIntArray();
      private PerPeerTypeInfo[] mPerPeerTypeInfo;
  
      public RttMetrics(Clock clock) {
          mClock = clock;
  
          mPerPeerTypeInfo = new PerPeerTypeInfo[2];
          mPerPeerTypeInfo[PEER_AP] = new PerPeerTypeInfo();
          mPerPeerTypeInfo[PEER_AWARE] = new PerPeerTypeInfo();
      }
  
      private class PerUidInfo {
          public int numRequests;
          public long lastRequestMs;
  
          @Override
          public String toString() {
              return "numRequests=" + numRequests + ", lastRequestMs=" + lastRequestMs;
          }
      }
  
      private class PerPeerTypeInfo {
          public int numCalls;
          public int numIndividualCalls;
          public SparseArray<PerUidInfo> perUidInfo = new SparseArray<>();
          public SparseIntArray numRequestsHistogram = new SparseIntArray();
          public SparseIntArray requestGapHistogram = new SparseIntArray();
          public SparseIntArray statusHistogram = new SparseIntArray();
          public SparseIntArray measuredDistanceHistogram = new SparseIntArray();
  
          @Override
          public String toString() {
              return "numCalls=" + numCalls + ", numIndividualCalls=" + numIndividualCalls
                      + ", perUidInfo=" + perUidInfo + ", numRequestsHistogram="
                      + numRequestsHistogram + ", requestGapHistogram=" + requestGapHistogram
                      + ", statusHistogram=" + statusHistogram
                      + ", measuredDistanceHistogram=" + measuredDistanceHistogram;
          }
      }
  
      // metric recording API
  
      /**
       * Record metrics for the range request.
       */
      public void recordRequest(WorkSource ws, RangingRequest requests) {
          mNumStartRangingCalls++;
  
          int numApRequests = 0;
          int numAwareRequests = 0;
          for (ResponderConfig request : requests.mRttPeers) {
              if (request == null) {
                  continue;
              }
              if (request.responderType == ResponderConfig.RESPONDER_AWARE) {
                  numAwareRequests++;
              } else if (request.responderType == ResponderConfig.RESPONDER_AP) {
                  numApRequests++;
              } else {
                  if (mDbg) Log.d(TAG, "Unexpected Responder type: " + request.responderType);
              }
          }
  
          updatePeerInfoWithRequestInfo(mPerPeerTypeInfo[PEER_AP], ws, numApRequests);
          updatePeerInfoWithRequestInfo(mPerPeerTypeInfo[PEER_AWARE], ws, numAwareRequests);
      }
  
      /**
       * Record metrics for the range results.
       */
      public void recordResult(RangingRequest requests, List<RttResult> results) {
          Map<MacAddress, ResponderConfig> requestEntries = new HashMap<>();
          for (ResponderConfig responder : requests.mRttPeers) {
              requestEntries.put(responder.macAddress, responder);
          }
  
          if (results != null) {
              for (RttResult result : results) {
                  if (result == null) {
                      continue;
                  }
                  ResponderConfig responder = requestEntries.remove(
                          MacAddress.fromBytes(result.addr));
                  if (responder == null) {
                      Log.e(TAG,
                              "recordResult: found a result which doesn't match any requests: "
                                      + result);
                      continue;
                  }
  
                  if (responder.responderType == ResponderConfig.RESPONDER_AP) {
                      updatePeerInfoWithResultInfo(mPerPeerTypeInfo[PEER_AP], result);
                  } else if (responder.responderType == ResponderConfig.RESPONDER_AWARE) {
                      updatePeerInfoWithResultInfo(mPerPeerTypeInfo[PEER_AWARE], result);
                  } else {
                      Log.e(TAG, "recordResult: unexpected peer type in responder: " + responder);
                  }
              }
          }
  
          for (ResponderConfig responder : requestEntries.values()) {
              PerPeerTypeInfo peerInfo;
              if (responder.responderType == ResponderConfig.RESPONDER_AP) {
                  peerInfo = mPerPeerTypeInfo[PEER_AP];
              } else if (responder.responderType == ResponderConfig.RESPONDER_AWARE) {
                  peerInfo = mPerPeerTypeInfo[PEER_AWARE];
              } else {
                  Log.e(TAG, "recordResult: unexpected peer type in responder: " + responder);
                  continue;
              }
              peerInfo.statusHistogram.put(WifiMetricsProto.WifiRttLog.MISSING_RESULT,
                      peerInfo.statusHistogram.get(WifiMetricsProto.WifiRttLog.MISSING_RESULT) + 1);
          }
      }
  
      /**
       * Record metrics for the overall ranging request status.
       */
      public void recordOverallStatus(int status) {
          mOverallStatusHistogram.put(status, mOverallStatusHistogram.get(status) + 1);
      }
  
      private void updatePeerInfoWithRequestInfo(PerPeerTypeInfo peerInfo, WorkSource ws,
              int numIndividualCalls) {
          if (numIndividualCalls == 0) {
              return;
          }
  
          long nowMs = mClock.getElapsedSinceBootMillis();
          peerInfo.numCalls++;
          peerInfo.numIndividualCalls += numIndividualCalls;
          peerInfo.numRequestsHistogram.put(numIndividualCalls,
                  peerInfo.numRequestsHistogram.get(numIndividualCalls) + 1);
          boolean recordedIntervals = false;
  
          for (int i = 0; i < ws.size(); ++i) {
              int uid = ws.get(i);
  
              PerUidInfo perUidInfo = peerInfo.perUidInfo.get(uid);
              if (perUidInfo == null) {
                  perUidInfo = new PerUidInfo();
              }
  
              perUidInfo.numRequests++;
  
              if (!recordedIntervals && perUidInfo.lastRequestMs != 0) {
                  recordedIntervals = true; // don't want to record twice
                  addValueToLogHistogram(nowMs - perUidInfo.lastRequestMs,
                          peerInfo.requestGapHistogram, COUNT_LOG_HISTOGRAM);
              }
              perUidInfo.lastRequestMs = nowMs;
  
              peerInfo.perUidInfo.put(uid, perUidInfo);
          }
      }
  
      private void updatePeerInfoWithResultInfo(PerPeerTypeInfo peerInfo, RttResult result) {
          int protoStatus = convertRttStatusTypeToProtoEnum(result.status);
          peerInfo.statusHistogram.put(protoStatus, peerInfo.statusHistogram.get(protoStatus) + 1);
          addValueToLinearHistogram(result.distanceInMm, peerInfo.measuredDistanceHistogram,
                  DISTANCE_MM_HISTOGRAM);
      }
  
      /**
       * Consolidate all metrics into the proto.
       */
      public WifiMetricsProto.WifiRttLog consolidateProto() {
          WifiMetricsProto.WifiRttLog log = new WifiMetricsProto.WifiRttLog();
          log.rttToAp = new WifiMetricsProto.WifiRttLog.RttToPeerLog();
          log.rttToAware = new WifiMetricsProto.WifiRttLog.RttToPeerLog();
          synchronized (mLock) {
              log.numRequests = mNumStartRangingCalls;
              log.histogramOverallStatus = consolidateOverallStatus(mOverallStatusHistogram);
  
              consolidatePeerType(log.rttToAp, mPerPeerTypeInfo[PEER_AP]);
              consolidatePeerType(log.rttToAware, mPerPeerTypeInfo[PEER_AWARE]);
          }
          return log;
      }
  
      private WifiMetricsProto.WifiRttLog.RttOverallStatusHistogramBucket[] consolidateOverallStatus(
              SparseIntArray histogram) {
          WifiMetricsProto.WifiRttLog.RttOverallStatusHistogramBucket[] h =
                  new WifiMetricsProto.WifiRttLog.RttOverallStatusHistogramBucket[histogram.size()];
          for (int i = 0; i < histogram.size(); i++) {
              h[i] = new WifiMetricsProto.WifiRttLog.RttOverallStatusHistogramBucket();
              h[i].statusType = histogram.keyAt(i);
              h[i].count = histogram.valueAt(i);
          }
          return h;
      }
  
      private void consolidatePeerType(WifiMetricsProto.WifiRttLog.RttToPeerLog peerLog,
              PerPeerTypeInfo peerInfo) {
          peerLog.numRequests = peerInfo.numCalls;
          peerLog.numIndividualRequests = peerInfo.numIndividualCalls;
          peerLog.numApps = peerInfo.perUidInfo.size();
          peerLog.histogramNumPeersPerRequest = consolidateNumPeersPerRequest(
                  peerInfo.numRequestsHistogram);
          peerLog.histogramNumRequestsPerApp = consolidateNumRequestsPerApp(peerInfo.perUidInfo);
          peerLog.histogramRequestIntervalMs = genericBucketsToRttBuckets(
                  logHistogramToGenericBuckets(peerInfo.requestGapHistogram, COUNT_LOG_HISTOGRAM));
          peerLog.histogramIndividualStatus = consolidateIndividualStatus(peerInfo.statusHistogram);
          peerLog.histogramDistance = genericBucketsToRttBuckets(
                  linearHistogramToGenericBuckets(peerInfo.measuredDistanceHistogram,
                          DISTANCE_MM_HISTOGRAM));
      }
  
      private WifiMetricsProto.WifiRttLog.RttIndividualStatusHistogramBucket[]
              consolidateIndividualStatus(SparseIntArray histogram) {
          WifiMetricsProto.WifiRttLog.RttIndividualStatusHistogramBucket[] h =
                  new WifiMetricsProto.WifiRttLog.RttIndividualStatusHistogramBucket[histogram.size(
                  )];
          for (int i = 0; i < histogram.size(); i++) {
              h[i] = new WifiMetricsProto.WifiRttLog.RttIndividualStatusHistogramBucket();
              h[i].statusType = histogram.keyAt(i);
              h[i].count = histogram.valueAt(i);
          }
          return h;
      }
  
      private WifiMetricsProto.WifiRttLog.HistogramBucket[] consolidateNumPeersPerRequest(
              SparseIntArray data) {
          WifiMetricsProto.WifiRttLog.HistogramBucket[] protoArray =
                  new WifiMetricsProto.WifiRttLog.HistogramBucket[data.size()];
  
          for (int i = 0; i < data.size(); i++) {
              protoArray[i] = new WifiMetricsProto.WifiRttLog.HistogramBucket();
              protoArray[i].start = data.keyAt(i);
              protoArray[i].end = data.keyAt(i);
              protoArray[i].count = data.valueAt(i);
          }
  
          return protoArray;
      }
  
      private WifiMetricsProto.WifiRttLog.HistogramBucket[] consolidateNumRequestsPerApp(
              SparseArray<PerUidInfo> perUidInfos) {
          SparseIntArray histogramNumRequestsPerUid = new SparseIntArray();
          for (int i = 0; i < perUidInfos.size(); i++) {
              addValueToLogHistogram(perUidInfos.valueAt(i).numRequests, histogramNumRequestsPerUid,
                      COUNT_LOG_HISTOGRAM);
          }
  
          return genericBucketsToRttBuckets(logHistogramToGenericBuckets(
                  histogramNumRequestsPerUid, COUNT_LOG_HISTOGRAM));
      }
  
      private WifiMetricsProto.WifiRttLog.HistogramBucket[] genericBucketsToRttBuckets(
              MetricsUtils.GenericBucket[] genericHistogram) {
          WifiMetricsProto.WifiRttLog.HistogramBucket[] histogram =
                  new WifiMetricsProto.WifiRttLog.HistogramBucket[genericHistogram.length];
          for (int i = 0; i < genericHistogram.length; i++) {
              histogram[i] = new WifiMetricsProto.WifiRttLog.HistogramBucket();
              histogram[i].start = genericHistogram[i].start;
              histogram[i].end = genericHistogram[i].end;
              histogram[i].count = genericHistogram[i].count;
          }
          return histogram;
      }
  
      /**
       * Dump all RttMetrics to console (pw) - this method is never called to dump the serialized
       * metrics (handled by parent WifiMetrics).
       *
       * @param fd   unused
       * @param pw   PrintWriter for writing dump to
       * @param args unused
       */
      public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
          synchronized (mLock) {
              pw.println("RTT Metrics:");
              pw.println("mNumStartRangingCalls:" + mNumStartRangingCalls);
              pw.println("mOverallStatusHistogram:" + mOverallStatusHistogram);
              pw.println("AP:" + mPerPeerTypeInfo[PEER_AP]);
              pw.println("AWARE:" + mPerPeerTypeInfo[PEER_AWARE]);
          }
      }
  
      /**
       * clear Wi-Fi RTT metrics
       */
      public void clear() {
          synchronized (mLock) {
              mNumStartRangingCalls = 0;
              mOverallStatusHistogram.clear();
              mPerPeerTypeInfo[PEER_AP] = new PerPeerTypeInfo();
              mPerPeerTypeInfo[PEER_AWARE] = new PerPeerTypeInfo();
          }
      }
  
      /**
       * Convert a HAL RttStatus enum to a Metrics proto enum RttIndividualStatusTypeEnum.
       */
      public static int convertRttStatusTypeToProtoEnum(int rttStatusType) {
          switch (rttStatusType) {
              case RttStatus.SUCCESS:
                  return WifiMetricsProto.WifiRttLog.SUCCESS;
              case RttStatus.FAILURE:
                  return WifiMetricsProto.WifiRttLog.FAILURE;
              case RttStatus.FAIL_NO_RSP:
                  return WifiMetricsProto.WifiRttLog.FAIL_NO_RSP;
              case RttStatus.FAIL_REJECTED:
                  return WifiMetricsProto.WifiRttLog.FAIL_REJECTED;
              case RttStatus.FAIL_NOT_SCHEDULED_YET:
                  return WifiMetricsProto.WifiRttLog.FAIL_NOT_SCHEDULED_YET;
              case RttStatus.FAIL_TM_TIMEOUT:
                  return WifiMetricsProto.WifiRttLog.FAIL_TM_TIMEOUT;
              case RttStatus.FAIL_AP_ON_DIFF_CHANNEL:
                  return WifiMetricsProto.WifiRttLog.FAIL_AP_ON_DIFF_CHANNEL;
              case RttStatus.FAIL_NO_CAPABILITY:
                  return WifiMetricsProto.WifiRttLog.FAIL_NO_CAPABILITY;
              case RttStatus.ABORTED:
                  return WifiMetricsProto.WifiRttLog.ABORTED;
              case RttStatus.FAIL_INVALID_TS:
                  return WifiMetricsProto.WifiRttLog.FAIL_INVALID_TS;
              case RttStatus.FAIL_PROTOCOL:
                  return WifiMetricsProto.WifiRttLog.FAIL_PROTOCOL;
              case RttStatus.FAIL_SCHEDULE:
                  return WifiMetricsProto.WifiRttLog.FAIL_SCHEDULE;
              case RttStatus.FAIL_BUSY_TRY_LATER:
                  return WifiMetricsProto.WifiRttLog.FAIL_BUSY_TRY_LATER;
              case RttStatus.INVALID_REQ:
                  return WifiMetricsProto.WifiRttLog.INVALID_REQ;
              case RttStatus.NO_WIFI:
                  return WifiMetricsProto.WifiRttLog.NO_WIFI;
              case RttStatus.FAIL_FTM_PARAM_OVERRIDE:
                  return WifiMetricsProto.WifiRttLog.FAIL_FTM_PARAM_OVERRIDE;
              default:
                  Log.e(TAG, "Unrecognized RttStatus: " + rttStatusType);
                  return WifiMetricsProto.WifiRttLog.UNKNOWN;
          }
      }
  }
  