Setup and load


In [1]:
import json

In [2]:
%matplotlib inline

In [3]:
import pandas as pd

Method

Android

Software: Changed code to support perturbing the following values:

  • geofenced versus continuous
  • time filter (interval between readings)
  • accuracy (balanced power versus high accuracy)

Hardware

  • Collected data for 6 hour stretches
  • Same phone was used for all experiments (Galaxy Nexus S, released in 2011, running Android 4.0 Ice Cream Sandwich)

Workload

  • Largely stationary at home
  • Made one trip to kids's school (4 km, ~ 20 mins) in every 6 hour window
  • Scaled x4, simulates day in the life of "office worker"
    • commutes to work in the morning
    • commutes back in the evening
    • go to dinner/gym etc at night
    • return home
  • Used scaling in order to explore space quickly using a single phone

High order bit for results

Android


In [4]:
raw_data = json.load(open("battery_drain_data.txt"))

In [5]:
raw_df = pd.DataFrame(raw_data)

Background battery drain over 6 hours


In [6]:
raw_df[["discharge_screen_off", "name"]].plot(figsize=(18,5), kind="bar", x="name")


Out[6]:
<matplotlib.axes.AxesSubplot at 0x10981d710>

Background battery drain extrapolated to 24 hours (6 hours * 4)


In [7]:
all_day_df = raw_df
all_day_df.discharge_screen_off = raw_df.discharge_screen_off * 4

In [8]:
ax = all_day_df[["discharge_screen_off", "name"]].plot(figsize=(18,5), kind="bar", x="name")


Observations

  • Significant background power drain even in the absence of data collection (~ 60% drain over the course of the day)
  • At 30 - 60 sec cycles, data collection with geofencing is essentially free (~ 40% drain over the course of a day)
    • At 60 sec intervals, geofence doesn't buy you that much (~ 2%, well within error ranges)
    • At 30 sec intervals, geofence is somewhat useful (~ 20%, but is that within error range? see below)
    • At 2 sec intervals, geofence is incredibly useful (difference between running out of battery and not)

Existing duty cycling?

  • Drain with data collection during the day (10:30pm to 4:30am and 7am to 1pm) is 10-15% (extrapolated to 40-60% through the day). During the first 10:30pm to 4:30am, the phone was by my side as I worked. I checked it occasionally, but was at home except for one mandatory trip to kids's school. The 7am to 1pm data collection was on my trip to Berkeley on Friday. At that time, I was obviously in motion but made a conscious effort not to use the phone for normal activities. This was a real strain, but I persevered until I downloaded the data during the SDB meeting.
  • No data collection between midnight and 6am was around 2%. At this time, the phone was lying on a bedside table and I did not touch it.
  • Interestingly, on the night that I slept in the lab, the drain between midnight and 6am was still around 10%. I hypothesise that this is because I used the phone as an alarm clock and picked it up occasionally to check the time.

This is consistent with the Martins et al paper in USENIX 2015 (https://www.usenix.org/system/files/conference/atc15/atc15-paper-martins.pdf) where they compared the power drain of a base Android install with one that had Google Play Services installed. Presumably this is what Doze is intended to assist with. My phone is a Galaxy Nexus that is a few generations old, so old that Google no longer releases OTA for it. So I am not running Android Marshmallow and cannot verify what Doze does. It would be pretty cool to see how all this interacts with Doze.

Unexplained Anomaly

There is one clear anomaly - the low power drain of the ongoing high accuracy collection.

My expectations around high accuracy are as follows:

  • high accuracy sensing will have higher power drain than balanced accuracy. This appears to be true with a geofence - the high accuracy drain is significantly higher. In particular, battery drain at high accuracy at an interval of 30 secs is higher than balanced accuracy at 2 secs (80% v/s 60%), and is almost double of low accuracy at 30 secs (80% v/s 40%)
  • ongoing sensing will have a higher or equal power drain than geofencing. We don't know what the power drain for the blackbox geofencing implementation is, but it seems reasonable to assume that it would be no worse than just running the sensing continuously, since otherwise it makes no sense for the OS to provide the API. And indeed, this is true for all balanced power sensing modes, although the difference can be small at times. The difference is particularly pronounced for the 2 sec case (130% versus 60%) and probably within noise for the 60 sec case (42% versus 40%).

However, this expectation is not met for the high accuracy case. The ongoing high accuracy case has two anomalies:

  • With geofencing, high accuracy drain is higher than balanced accuracy with 2 sec drain. But without geofencing, high accuracy drain is lower than balanced accuracy with 2 sec drain.
  • The power drain of ongoing high accuracy collection is significantly lower than the power drain for geofenced high accuracy collection (~ 55% v/s 80%).

This is counter-intuitive. If high accuracy sensing is so expensive, why isn't more of it more expensive? Since this was a counter-intuitive result, I did some sanity checking to ensure that this wasn't a bug.

Is this a bug?


In [9]:
gps_lock_df = pd.read_csv("gps-lock_data.txt", header=None, names=["ignore1", "ignore2", "ignore3","lock","time","count"])

In [10]:
gps_lock_df[gps_lock_df.ignore2=='t'][["time", "count"]].plot(subplots=True, figsize=(18,10), kind="bar")


Out[10]:
array([<matplotlib.axes.AxesSubplot object at 0x1098f2f90>,
       <matplotlib.axes.AxesSubplot object at 0x10a32a350>], dtype=object)

It is pretty clear from the graph above that:

  • balanced power accuracy turns the GPS on very infrequently (O(10) times)
  • high power accuracy uses the GPS much more frequently - for the geofenced implementation, the lock is acquired ~ 40 times for a ~ 20 min ride
  • high power, ongoing collection acquires the gps lock at 7 times the rate of the geofenced implementation (350 v/s ~ 50) and it runs for 1 hr 23 min instead of 10 min. But it still consumes less power. The only explanations for this that I can think of are:
    • GPS chips have now become efficient enough that while they are on in a suburban location, they take about as much power as a wifi scan
    • they have a large startup cost, so powering them on occasionally consumes more power than just leaving them on.

The explanations are not very convincing (if #2 above were true, then the fused API is no longer as compelling), so it would be interesting to explore this further in the future.


In [11]:
6 * 60 * 2


Out[11]:
720

Note also that we would expect the gps-lock to be acquired 720 times (6 hours, at 30 sec intervals), but it is actually acquired only 350 times. It looks like even in high accuracy mode with the fused API, the OS only turns the GPS on half the time. It either takes two readings per lock, or duty cycles the GPS based on techniques similar to Paek et al from Mobisys 2010. http://dl.acm.org/citation.cfm?id=1814463

Noise and error bars

Except for the no data collection, I have run the data collection for a particular regime exactly once. This means that some of the other anomalies may just be the result of noise. In particular, there are some small unexpected anomalies in addition to the previous one.

  • drain for geofenced data collection at 30 sec intervals is less than the drain at 60 sec intervals (38% versus 40%)
  • drain for geofenced data collection at both 30 and 60 sec intervals is less than the drain for no data collection (38% and 40% versus ~45%)
  • drain for ongoing data collection at 30 and 60 secs is around the same as that for no data collection (48%)
  • drain for ongoing data collection at 60 secs is less than that for no data collection (42% versus 48%)

My assumption is that all of these are within the noise for the battery drain, but then are the savings by using the geofence real? I think that we should get multiple runs and get error bars to confirm.

What is the accuracy?


In [22]:
import emission.analysis.plotting.leaflet_osm.ipython_helper as ipy
import emission.analysis.plotting.leaflet_osm.our_plotter as lo
import emission.analysis.plotting.geojson.geojson_feature_converter as gfc
import emission.storage.timeseries.abstract_timeseries as esta

In [24]:
uuids = esta.TimeSeries.get_uuid_list(); uuids


Out[24]:
[UUID('0763de67-f61e-3f5d-90e7-518e69793954')]

In [25]:
myuuid = uuids[0]

In [26]:
ts = esta.TimeSeries.get_time_series(myuuid)

In [67]:
import geojson as gj

def getGeojsonPoints(ts, key, start_ts, end_ts):
    import emission.net.usercache.abstract_usercache as enua
    import emission.core.wrapper.location as ecwl 
    
    tq = enua.UserCache.TimeQuery("write_ts", start_ts, end_ts)
    entry_it = ts.find_entries([key], tq)
    section_location_array = [ecwl.Location(ts._to_df_entry(entry)) for entry in entry_it]
    
    points_feature_array = [gfc.location_to_geojson(l) for l in section_location_array]
    print ("Found %d points" % len(points_feature_array))
    
    points_line_string = gj.LineString()
    points_line_string.coordinates = []
    for loc in section_location_array:
        points_line_string.coordinates.append(l.loc.coordinates)
    
    points_line_feature = gj.Feature()
    points_line_feature.geometry = points_line_string
    
    if len(points_feature_array) != 0:
        feature_array = []
        feature_array.append(points_feature_array[0])
        feature_array.append(points_feature_array[1])
        feature_array.append(gj.FeatureCollection(points_feature_array))
        feature_array.append(points_line_feature)
    
        feature_coll = gj.FeatureCollection(feature_array)
    else:
        feature_coll = gj.FeatureCollection([])
    return feature_coll

In [70]:
# geofence balanced power 1 min
geofence_balanced_power_1_min = getGeojsonPoints(ts, "background/filtered_location", 1445450400, 1445472000)
geofence_balanced_power_2_sec = getGeojsonPoints(ts, "background/filtered_location", 1445427600, 1445449200)
geofence_balanced_power_30_sec = getGeojsonPoints(ts, "background/filtered_location", 1445353200, 1445374800)
# geofence_high_accuracy_30_sec = getGeojsonPoints(ts, "background/filtered_location", 1445544000, 1445567400)


Found 32 points
Found 242 points
Found 93 points

In [77]:
map_list = lo.get_maps_for_geojson_list([geofence_balanced_power_1_min, geofence_balanced_power_30_sec, geofence_balanced_power_2_sec])

In [79]:
ipy.inline_maps([map_list])


Out[79]:

In [53]:
import datetime as pydt
pydt.datetime.fromtimestamp(1448031600)


Out[53]:
datetime.datetime(2015, 11, 20, 7, 0)

In [42]:
edb.get_timeseries_db().find_one()


Out[42]:
{u'_id': ObjectId('55d8c4a47d65cb39ee983f2a'),
 u'data': {u'confidence': 100,
  u'fmt_time': u'2015-08-21T20:31:34.431000-07:00',
  u'local_dt': datetime.datetime(2015, 8, 21, 20, 31, 34, 431000),
  u'ts': 1440214294.431,
  u'type': 3},
 u'metadata': {u'filter': u'time',
  u'key': u'background/motion_activity',
  u'platform': u'android',
  u'read_ts': 0,
  u'time_zone': u'America/Los_Angeles',
  u'type': u'message',
  u'write_fmt_time': u'2015-08-21T20:31:34.431000-07:00',
  u'write_local_dt': datetime.datetime(2015, 8, 21, 20, 31, 34, 431000),
  u'write_ts': 1440214294.431},
 u'user_id': UUID('0763de67-f61e-3f5d-90e7-518e69793954')}

batteryinfo dump and accounting

In order to generate these stats, I used the adb shell dumpsys batteryinfo command. This command is replaced in later versions of android with the adb shell dumpsys batterystats command and the current documentation (https://source.android.com/devices/tech/power/batterystats.html) refers to the output of the current command.

In the prior analysis, I have primarily used the battery drain, which can be measured directly using a standard API. However, the dumpsys shell command promises to provide additional information which could potentially be used to build a richer model. How much additional information can I collect, and how useful is it?

My first thought was to use $ adb shell dumpsys batterystats --charged from the documentation, but it turns out that it is not supported for batteryinfo.

The output also does not support the pwi (Power Use Item) or pws (Power Use Summary) fields.

I then looked at the raw data and pulled out some information that appeared to be relevant.

  • duration since start
  • for the tracker process (cfc_tracker or mobility)
    • bytes sent
    • bytes received
    • wake lock duration and count
    • wake lock for sync duration and count
    • travel diary state machine invocation duration and count
    • location change service invocation duration and count
    • activity recognition service invocation duration and count

Observations

  • Sensor power drain is not charged to the process. Note neither the accelerometer, nor WiFi are listed below.
  • It looks like what is primarily charged to the process is the time that it runs in the foreground, consuming CPU resources
  • It is not clear what all the various Wakelocks represent (why is the GCoreFlp wakelock 273 times? What does it represent?
  • The sync takes way too much time. The app spends more time syncing (5 minutes) than it does collecting and storing location data (1 minute)
    • The incoming sync is an order of magnitude bigger than the outgoing sync
    • So the travel diary representation that we send is too big. There are also known issues with purging items from the cache properly. We should revisit and streamline our cache implementation.
  • mobility app does not appear to sync its data?! Or maybe it is so fast that it does not meet the threshold for reporting? Need to ask them.

#10065:
Network: 36.45MB received, 860.01KB sent
Wake lock GCoreFlp: 1s 223ms partial (273 times) realtime
Wake lock GeofencePendingIntentWakeLock: 4ms partial (2 times) realtime
Wake lock *sync*_edu.berkeley.eecs.cfc_tracker.provider_Account {name=dummy_account, type=openbms.org}: 5m 25s 57ms partial (10 times) realtime
TOTAL wake: 5m 26s 284ms partial realtime
Proc *wakelock*:
  CPU: 58s 910ms usr + 15s 270ms krn
Proc edu.berkeley.eecs.cfc_tracker:sync:
  CPU: 1m 35s 600ms usr + 6s 500ms krn
  10 proc starts
Proc edu.berkeley.eecs.cfc_tracker:
  CPU: 16s 610ms usr + 5s 540ms krn
  8 proc starts
Apk edu.berkeley.eecs.cfc_tracker:
  Service edu.berkeley.eecs.cfc_tracker.location.TripDiaryStateMachineService:
    Created for: 3s 601ms  uptime
    Starts: 4, launches: 4
  Service edu.berkeley.eecs.cfc_tracker.location.GeofenceExitIntentService:
    Created for: 1s 452ms  uptime
    Starts: 2, launches: 2
  Service edu.berkeley.eecs.cfc_tracker.smap.SyncService:
    Created for: 0ms  uptime
    Starts: 0, launches: 10
  Service edu.berkeley.eecs.cfc_tracker.location.ActivityRecognitionChangeIntentService:
    Created for: 18s 61ms  uptime
    Starts: 37, launches: 37
  Service edu.berkeley.eecs.cfc_tracker.location.LocationChangeIntentService:
    Created for: 1m 54s 703ms  uptime
    Starts: 94, launches: 94

Next steps

  • Get additional set of readings for the following regimes so that we can get some error bars.

    • geofence 30 secs
    • geofence 60 secs
    • geofence 30 secs high accuracy
    • ongoing 30 secs
    • ongoing 60 secs
    • ongoing 30 secs high accuracy
    • mobility data collection
  • We do not need to repeat the 2 sec interval readings since the results were unambiguous in that case.

  • During the geofence tests, I will make at least a 20 min round trip outside.
  • For the ongoing tests, I may or may not make the trip, since we don't do any duty cycling.

Error bars

Since I had only a week between the previous result and the current presentation, I was able to get error bars for only some of the regimes. I got the most data for the "no data collection" or baseline regime, since it showed a fair amount of variability, and it is key to know how much additional drain continuous collection adds.


In [32]:
no_data_collection_df = pd.read_csv("no_data_collection_error_bars.txt", header=None, names=["ignore1", "tu", "ignore3", "low","high","on","off"])
geofence_collection_df = pd.read_csv("geofence_100m_plus_fused_balanced_power_30sec_error_bars.txt", header=None, names=["ignore1", "tu", "ignore3", "low","high","on","off"])
ongoing_collection_df = pd.read_csv("ongoing_plus_fused_balanced_power_30sec_error_bars.txt", header=None, names=["ignore1", "tu", "ignore3", "low","high","on","off"])

In [33]:
no_data_collection_df[no_data_collection_df.tu=='t'][["off"]].plot(figsize=(18,6), kind="bar")


Out[33]:
<matplotlib.axes.AxesSubplot at 0x10e90c9d0>

In [34]:
geofence_collection_df[geofence_collection_df.tu=='t'][["off"]].plot(figsize=(18,6), kind="bar")


Out[34]:
<matplotlib.axes.AxesSubplot at 0x110a00f50>

In [35]:
ongoing_collection_df[ongoing_collection_df.tu=='t'][["off"]].plot(figsize=(18,6), kind="bar")


Out[35]:
<matplotlib.axes.AxesSubplot at 0x110dce950>

In [36]:
import datetime as pydt

pydt.datetime.fromtimestamp(1446068421)


Out[36]:
datetime.datetime(2015, 10, 28, 14, 40, 21)

In [37]:
no_data_collection_df[no_data_collection_df.tu=='t'].off.describe()


Out[37]:
count    10.000000
mean     10.000000
std       4.760952
min       2.000000
25%       8.250000
50%       9.500000
75%      13.000000
max      18.000000
Name: off, dtype: float64

In [38]:
geofence_collection_df[geofence_collection_df.tu=='t'].off.describe()


Out[38]:
count     4.000000
mean     13.250000
std       3.304038
min       9.000000
25%      12.000000
50%      13.500000
75%      14.750000
max      17.000000
Name: off, dtype: float64

In [39]:
ongoing_collection_df[ongoing_collection_df.tu=='t'].off.describe()


Out[39]:
count     6.000000
mean     16.833333
std       7.167054
min       4.000000
25%      15.000000
50%      18.500000
75%      21.250000
max      24.000000
Name: off, dtype: float64

Gory Details


In [17]:
raw_df.plot(subplots=True, figsize=(20,100), kind="bar", x="name")


Out[17]:
array([<matplotlib.axes.AxesSubplot object at 0x12026a590>,
       <matplotlib.axes.AxesSubplot object at 0x11f706590>,
       <matplotlib.axes.AxesSubplot object at 0x1250f11d0>,
       <matplotlib.axes.AxesSubplot object at 0x126cbd810>,
       <matplotlib.axes.AxesSubplot object at 0x127002510>,
       <matplotlib.axes.AxesSubplot object at 0x1270b0150>,
       <matplotlib.axes.AxesSubplot object at 0x127259f10>,
       <matplotlib.axes.AxesSubplot object at 0x1272d9c10>,
       <matplotlib.axes.AxesSubplot object at 0x127345b90>,
       <matplotlib.axes.AxesSubplot object at 0x1273cb890>,
       <matplotlib.axes.AxesSubplot object at 0x127431850>,
       <matplotlib.axes.AxesSubplot object at 0x1274b4cd0>,
       <matplotlib.axes.AxesSubplot object at 0x1275290d0>,
       <matplotlib.axes.AxesSubplot object at 0x1275ae3d0>,
       <matplotlib.axes.AxesSubplot object at 0x1276340d0>,
       <matplotlib.axes.AxesSubplot object at 0x127698b10>,
       <matplotlib.axes.AxesSubplot object at 0x12771e810>,
       <matplotlib.axes.AxesSubplot object at 0x1277837d0>,
       <matplotlib.axes.AxesSubplot object at 0x127909c50>], dtype=object)

In [ ]: