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