AppScanStats.java revision 3376fb589d0279b58f42986a3e13d4498dbf2c4f
1/* 2 * Copyright (C) 2016 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 */ 16package com.android.bluetooth.gatt; 17 18import android.bluetooth.le.ScanSettings; 19import android.os.Binder; 20import android.os.RemoteException; 21import android.os.ServiceManager; 22import android.os.SystemClock; 23import android.os.WorkSource; 24 25import com.android.bluetooth.btservice.BluetoothProto; 26import com.android.internal.app.IBatteryStats; 27 28import java.text.DateFormat; 29import java.text.SimpleDateFormat; 30import java.util.ArrayList; 31import java.util.Date; 32import java.util.HashMap; 33import java.util.Iterator; 34import java.util.List; 35 36/** 37 * ScanStats class helps keep track of information about scans 38 * on a per application basis. 39 * @hide 40 */ 41/*package*/ class AppScanStats { 42 static final DateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss"); 43 44 /* ContextMap here is needed to grab Apps and Connections */ ContextMap mContextMap; 45 46 /* GattService is needed to add scan event protos to be dumped later */ GattService 47 mGattService; 48 49 /* Battery stats is used to keep track of scans and result stats */ IBatteryStats mBatteryStats; 50 51 class LastScan { 52 public long duration; 53 public long suspendDuration; 54 public long suspendStartTime; 55 public boolean isSuspended; 56 public long timestamp; 57 public boolean opportunistic; 58 public boolean timeout; 59 public boolean background; 60 public boolean filtered; 61 public int results; 62 public int scannerId; 63 64 LastScan(long timestamp, long duration, boolean opportunistic, boolean background, 65 boolean filtered, int scannerId) { 66 this.duration = duration; 67 this.timestamp = timestamp; 68 this.opportunistic = opportunistic; 69 this.background = background; 70 this.filtered = filtered; 71 this.results = 0; 72 this.scannerId = scannerId; 73 this.suspendDuration = 0; 74 this.suspendStartTime = 0; 75 this.isSuspended = false; 76 } 77 } 78 79 static final int NUM_SCAN_DURATIONS_KEPT = 5; 80 81 // This constant defines the time window an app can scan multiple times. 82 // Any single app can scan up to |NUM_SCAN_DURATIONS_KEPT| times during 83 // this window. Once they reach this limit, they must wait until their 84 // earliest recorded scan exits this window. 85 static final long EXCESSIVE_SCANNING_PERIOD_MS = 30 * 1000; 86 87 // Maximum msec before scan gets downgraded to opportunistic 88 static final int SCAN_TIMEOUT_MS = 30 * 60 * 1000; 89 90 public String appName; 91 public WorkSource workSource; // Used for BatteryStats 92 private int mScansStarted = 0; 93 private int mScansStopped = 0; 94 public boolean isRegistered = false; 95 private long mMinScanTime = Long.MAX_VALUE; 96 private long mMaxScanTime = 0; 97 private long mScanStartTime = 0; 98 private long mTotalScanTime = 0; 99 private long mTotalSuspendTime = 0; 100 private List<LastScan> mLastScans = new ArrayList<LastScan>(NUM_SCAN_DURATIONS_KEPT); 101 private HashMap<Integer, LastScan> mOngoingScans = new HashMap<Integer, LastScan>(); 102 public long startTime = 0; 103 public long stopTime = 0; 104 public int results = 0; 105 106 AppScanStats(String name, WorkSource source, ContextMap map, GattService service) { 107 appName = name; 108 mContextMap = map; 109 mGattService = service; 110 mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService("batterystats")); 111 112 if (source == null) { 113 // Bill the caller if the work source isn't passed through 114 source = new WorkSource(Binder.getCallingUid(), appName); 115 } 116 workSource = source; 117 } 118 119 synchronized void addResult(int scannerId) { 120 LastScan scan = getScanFromScannerId(scannerId); 121 if (scan != null) { 122 int batteryStatsResults = ++scan.results; 123 124 // Only update battery stats after receiving 100 new results in order 125 // to lower the cost of the binder transaction 126 if (batteryStatsResults % 100 == 0) { 127 try { 128 mBatteryStats.noteBleScanResults(workSource, 100); 129 } catch (RemoteException e) { 130 /* ignore */ 131 } 132 } 133 } 134 135 results++; 136 } 137 138 boolean isScanning() { 139 return !mOngoingScans.isEmpty(); 140 } 141 142 LastScan getScanFromScannerId(int scannerId) { 143 return mOngoingScans.get(scannerId); 144 } 145 146 synchronized void recordScanStart(ScanSettings settings, boolean filtered, int scannerId) { 147 LastScan existingScan = getScanFromScannerId(scannerId); 148 if (existingScan != null) { 149 return; 150 } 151 this.mScansStarted++; 152 startTime = SystemClock.elapsedRealtime(); 153 154 LastScan scan = new LastScan(startTime, 0, false, false, filtered, scannerId); 155 if (settings != null) { 156 scan.opportunistic = settings.getScanMode() == ScanSettings.SCAN_MODE_OPPORTUNISTIC; 157 scan.background = 158 (settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0; 159 } 160 161 BluetoothProto.ScanEvent scanEvent = new BluetoothProto.ScanEvent(); 162 scanEvent.setScanEventType(BluetoothProto.ScanEvent.SCAN_EVENT_START); 163 scanEvent.setScanTechnologyType(BluetoothProto.ScanEvent.SCAN_TECH_TYPE_LE); 164 scanEvent.setEventTimeMillis(System.currentTimeMillis()); 165 scanEvent.setInitiator(truncateAppName(appName)); 166 mGattService.addScanEvent(scanEvent); 167 168 if (!isScanning()) { 169 mScanStartTime = startTime; 170 } 171 try { 172 boolean isUnoptimized = !(scan.filtered || scan.background || scan.opportunistic); 173 mBatteryStats.noteBleScanStarted(workSource, isUnoptimized); 174 } catch (RemoteException e) { 175 /* ignore */ 176 } 177 178 mOngoingScans.put(scannerId, scan); 179 } 180 181 synchronized void recordScanStop(int scannerId) { 182 LastScan scan = getScanFromScannerId(scannerId); 183 if (scan == null) { 184 return; 185 } 186 this.mScansStopped++; 187 stopTime = SystemClock.elapsedRealtime(); 188 long scanDuration = stopTime - scan.timestamp; 189 scan.duration = scanDuration; 190 if (scan.isSuspended) { 191 long suspendDuration = stopTime - scan.suspendStartTime; 192 scan.suspendDuration += suspendDuration; 193 mTotalSuspendTime += suspendDuration; 194 } 195 mOngoingScans.remove(scannerId); 196 if (mLastScans.size() >= NUM_SCAN_DURATIONS_KEPT) { 197 mLastScans.remove(0); 198 } 199 mLastScans.add(scan); 200 201 BluetoothProto.ScanEvent scanEvent = new BluetoothProto.ScanEvent(); 202 scanEvent.setScanEventType(BluetoothProto.ScanEvent.SCAN_EVENT_STOP); 203 scanEvent.setScanTechnologyType(BluetoothProto.ScanEvent.SCAN_TECH_TYPE_LE); 204 scanEvent.setEventTimeMillis(System.currentTimeMillis()); 205 scanEvent.setInitiator(truncateAppName(appName)); 206 mGattService.addScanEvent(scanEvent); 207 208 if (!isScanning()) { 209 long totalDuration = stopTime - mScanStartTime; 210 mTotalScanTime += totalDuration; 211 mMinScanTime = Math.min(totalDuration, mMinScanTime); 212 mMaxScanTime = Math.max(totalDuration, mMaxScanTime); 213 } 214 215 try { 216 // Inform battery stats of any results it might be missing on 217 // scan stop 218 boolean isUnoptimized = !(scan.filtered || scan.background || scan.opportunistic); 219 mBatteryStats.noteBleScanResults(workSource, scan.results % 100); 220 mBatteryStats.noteBleScanStopped(workSource, isUnoptimized); 221 } catch (RemoteException e) { 222 /* ignore */ 223 } 224 } 225 226 synchronized void recordScanSuspend(int scannerId) { 227 LastScan scan = getScanFromScannerId(scannerId); 228 if (scan == null || scan.isSuspended) { 229 return; 230 } 231 scan.suspendStartTime = SystemClock.elapsedRealtime(); 232 scan.isSuspended = true; 233 } 234 235 synchronized void recordScanResume(int scannerId) { 236 LastScan scan = getScanFromScannerId(scannerId); 237 long suspendDuration = 0; 238 if (scan == null || !scan.isSuspended) { 239 return; 240 } 241 scan.isSuspended = false; 242 stopTime = SystemClock.elapsedRealtime(); 243 suspendDuration = stopTime - scan.suspendStartTime; 244 scan.suspendDuration += suspendDuration; 245 mTotalSuspendTime += suspendDuration; 246 } 247 248 synchronized void setScanTimeout(int scannerId) { 249 if (!isScanning()) { 250 return; 251 } 252 253 LastScan scan = getScanFromScannerId(scannerId); 254 if (scan != null) { 255 scan.timeout = true; 256 } 257 } 258 259 synchronized boolean isScanningTooFrequently() { 260 if (mLastScans.size() < NUM_SCAN_DURATIONS_KEPT) { 261 return false; 262 } 263 264 return (SystemClock.elapsedRealtime() - mLastScans.get(0).timestamp) 265 < EXCESSIVE_SCANNING_PERIOD_MS; 266 } 267 268 synchronized boolean isScanningTooLong() { 269 if (!isScanning()) { 270 return false; 271 } 272 return (SystemClock.elapsedRealtime() - mScanStartTime) > SCAN_TIMEOUT_MS; 273 } 274 275 // This function truncates the app name for privacy reasons. Apps with 276 // four part package names or more get truncated to three parts, and apps 277 // with three part package names names get truncated to two. Apps with two 278 // or less package names names are untouched. 279 // Examples: one.two.three.four => one.two.three 280 // one.two.three => one.two 281 private String truncateAppName(String name) { 282 String initiator = name; 283 String[] nameSplit = initiator.split("\\."); 284 if (nameSplit.length > 3) { 285 initiator = nameSplit[0] + "." + nameSplit[1] + "." + nameSplit[2]; 286 } else if (nameSplit.length == 3) { 287 initiator = nameSplit[0] + "." + nameSplit[1]; 288 } 289 290 return initiator; 291 } 292 293 synchronized void dumpToString(StringBuilder sb) { 294 long currTime = SystemClock.elapsedRealtime(); 295 long maxScan = mMaxScanTime; 296 long minScan = mMinScanTime; 297 long scanDuration = 0; 298 299 if (isScanning()) { 300 scanDuration = currTime - mScanStartTime; 301 } 302 minScan = Math.min(scanDuration, minScan); 303 maxScan = Math.max(scanDuration, maxScan); 304 305 if (minScan == Long.MAX_VALUE) { 306 minScan = 0; 307 } 308 309 /*TODO: Average scan time can be skewed for 310 * multiple scan clients. It will show less than 311 * actual value. 312 * */ 313 long avgScan = 0; 314 long totalScanTime = mTotalScanTime + scanDuration; 315 if (mScansStarted > 0) { 316 avgScan = totalScanTime / mScansStarted; 317 } 318 319 sb.append(" " + appName); 320 if (isRegistered) { 321 sb.append(" (Registered)"); 322 } 323 324 if (!mLastScans.isEmpty()) { 325 LastScan lastScan = mLastScans.get(mLastScans.size() - 1); 326 if (lastScan.opportunistic) { 327 sb.append(" (Opportunistic)"); 328 } 329 if (lastScan.background) { 330 sb.append(" (Background)"); 331 } 332 if (lastScan.timeout) { 333 sb.append(" (Forced-Opportunistic)"); 334 } 335 if (lastScan.filtered) { 336 sb.append(" (Filtered)"); 337 } 338 } 339 sb.append("\n"); 340 341 sb.append(" LE scans (started/stopped) : " + mScansStarted + " / " + mScansStopped 342 + "\n"); 343 sb.append(" Scan time in ms (min/max/avg/total): " + minScan + " / " + maxScan + " / " 344 + avgScan + " / " + totalScanTime + "\n"); 345 if (mTotalSuspendTime != 0) { 346 sb.append(" Total time suspended : " + mTotalSuspendTime + "ms\n"); 347 } 348 sb.append(" Total number of results : " + results + "\n"); 349 350 long currentTime = System.currentTimeMillis(); 351 long elapsedRt = SystemClock.elapsedRealtime(); 352 if (!mLastScans.isEmpty()) { 353 sb.append(" Last " + mLastScans.size() + " scans :\n"); 354 355 for (int i = 0; i < mLastScans.size(); i++) { 356 LastScan scan = mLastScans.get(i); 357 Date timestamp = new Date(currentTime - elapsedRt + scan.timestamp); 358 sb.append(" " + DATE_FORMAT.format(timestamp) + " - "); 359 sb.append(scan.duration + "ms "); 360 if (scan.opportunistic) { 361 sb.append("Opp "); 362 } 363 if (scan.background) { 364 sb.append("Back "); 365 } 366 if (scan.timeout) { 367 sb.append("Forced "); 368 } 369 if (scan.filtered) { 370 sb.append("Filter "); 371 } 372 sb.append(scan.results + " results"); 373 sb.append(" (" + scan.scannerId + ")"); 374 sb.append("\n"); 375 if (scan.suspendDuration != 0) { 376 sb.append(" └" + " Suspended Time: " + scan.suspendDuration + "ms\n"); 377 } 378 } 379 } 380 381 if (!mOngoingScans.isEmpty()) { 382 sb.append(" Ongoing scans :\n"); 383 for (Integer key : mOngoingScans.keySet()) { 384 LastScan scan = mOngoingScans.get(key); 385 Date timestamp = new Date(currentTime - elapsedRt + scan.timestamp); 386 sb.append(" " + DATE_FORMAT.format(timestamp) + " - "); 387 sb.append((elapsedRt - scan.timestamp) + "ms "); 388 if (scan.opportunistic) { 389 sb.append("Opp "); 390 } 391 if (scan.background) { 392 sb.append("Back "); 393 } 394 if (scan.timeout) { 395 sb.append("Forced "); 396 } 397 if (scan.filtered) { 398 sb.append("Filter "); 399 } 400 if (scan.isSuspended) { 401 sb.append("Suspended "); 402 } 403 sb.append(scan.results + " results"); 404 sb.append(" (" + scan.scannerId + ")"); 405 sb.append("\n"); 406 if (scan.suspendStartTime != 0) { 407 long duration = scan.suspendDuration + (scan.isSuspended ? (elapsedRt 408 - scan.suspendStartTime) : 0); 409 sb.append(" └" + " Suspended Time: " + duration + "ms\n"); 410 } 411 } 412 } 413 414 ContextMap.App appEntry = mContextMap.getByName(appName); 415 if (appEntry != null && isRegistered) { 416 sb.append(" Application ID : " + appEntry.id + "\n"); 417 sb.append(" UUID : " + appEntry.uuid + "\n"); 418 419 List<ContextMap.Connection> connections = mContextMap.getConnectionByApp(appEntry.id); 420 421 sb.append(" Connections: " + connections.size() + "\n"); 422 423 Iterator<ContextMap.Connection> ii = connections.iterator(); 424 while (ii.hasNext()) { 425 ContextMap.Connection connection = ii.next(); 426 long connectionTime = elapsedRt - connection.startTime; 427 Date timestamp = new Date(currentTime - elapsedRt + connection.startTime); 428 sb.append(" " + DATE_FORMAT.format(timestamp) + " - "); 429 sb.append((connectionTime) + "ms "); 430 sb.append(": " + connection.address + " (" + connection.connId + ")\n"); 431 } 432 } 433 sb.append("\n"); 434 } 435} 436