1#!/usr/bin/env python3
2
3import argparse
4import datetime
5import json
6import matplotlib.pyplot as plt
7import sys
8
9class UidSnapshot(object):
10    def __init__(self, activity):
11        self.uid = activity['uid']
12        self.foregroundWrittenBytes = activity['foregroundWrittenBytes']
13        self.foregroundFsyncCalls = activity['foregroundFsyncCalls']
14        self.backgroundFsyncCalls = activity['backgroundFsyncCalls']
15        self.backgroundWrittenBytes = activity['backgroundWrittenBytes']
16        self.appPackages = activity['appPackages']
17        self.runtimeMs = activity['runtimeMs']
18        self.totalWrittenBytes = self.foregroundWrittenBytes + self.backgroundWrittenBytes
19        self.totalFsyncCalls = self.backgroundFsyncCalls + self.foregroundFsyncCalls
20        if self.appPackages is None: self.appPackages = []
21
22class Snapshot(object):
23    def __init__(self, activity, uptime):
24        self.uptime = uptime
25        self.uids = {}
26        self.foregroundWrittenBytes = 0
27        self.foregroundFsyncCalls = 0
28        self.backgroundFsyncCalls = 0
29        self.backgroundWrittenBytes = 0
30        self.totalWrittenBytes = 0
31        self.totalFsyncCalls = 0
32        for entry in activity:
33            uid = entry['uid']
34            snapshot = UidSnapshot(entry)
35            self.foregroundWrittenBytes += snapshot.foregroundWrittenBytes
36            self.foregroundFsyncCalls += snapshot.foregroundFsyncCalls
37            self.backgroundFsyncCalls += snapshot.backgroundFsyncCalls
38            self.backgroundWrittenBytes += snapshot.backgroundWrittenBytes
39            self.totalWrittenBytes += snapshot.totalWrittenBytes
40            self.totalFsyncCalls += snapshot.totalFsyncCalls
41            self.uids[uid] = snapshot
42
43class Document(object):
44    def __init__(self, f):
45        self.snapshots = []
46        uptimes = [0, 0]
47        for line in f:
48            line = json.loads(line)
49            if line['type'] != 'snapshot': continue
50            activity = line['activity']
51            uptime = line['uptime']
52            if uptime < uptimes[0]: uptimes[0] = uptime
53            if uptime > uptimes[1]: uptimes[1] = uptime
54            self.snapshots.append(Snapshot(activity, uptime))
55        self.runtime = datetime.timedelta(milliseconds=uptimes[1]-uptimes[0])
56
57def merge(l1, l2):
58    s1 = set(l1)
59    s2 = set(l2)
60    return list(s1 | s2)
61
62
63thresholds = [
64    (1024 * 1024 * 1024 * 1024, "TB"),
65    (1024 * 1024 * 1024, "GB"),
66    (1024 * 1024, "MB"),
67    (1024, "KB"),
68    (1, "bytes")
69]
70def prettyPrintBytes(n):
71    for t in thresholds:
72        if n >= t[0]:
73            return "%.1f %s" % (n / (t[0] + 0.0), t[1])
74    return "0 bytes"
75
76# knowledge extracted from android_filesystem_config.h
77wellKnownUids = {
78    0 :         ["linux kernel"],
79    1010 :      ["wifi"],
80    1013 :      ["mediaserver"],
81    1017 :      ["keystore"],
82    1019 :      ["DRM server"],
83    1021 :      ["GPS"],
84    1023 :      ["media storage write access"],
85    1036 :      ["logd"],
86    1040 :      ["mediaextractor"],
87    1041 :      ["audioserver"],
88    1046 :      ["mediacodec"],
89    1047 :      ["cameraserver"],
90    1053 :      ["webview zygote"],
91    1054 :      ["vehicle hal"],
92    1058 :      ["tombstoned"],
93    1066 :      ["statsd"],
94    1067 :      ["incidentd"],
95    9999 :      ["nobody"],
96}
97
98class UserActivity(object):
99    def __init__(self, uid):
100        self.uid = uid
101        self.snapshots = []
102        self.appPackages = wellKnownUids.get(uid, [])
103        self.foregroundWrittenBytes = 0
104        self.foregroundFsyncCalls = 0
105        self.backgroundFsyncCalls = 0
106        self.backgroundWrittenBytes = 0
107        self.totalWrittenBytes = 0
108        self.totalFsyncCalls = 0
109
110    def addSnapshot(self, snapshot):
111        assert snapshot.uid == self.uid
112        self.snapshots.append(snapshot)
113        self.foregroundWrittenBytes += snapshot.foregroundWrittenBytes
114        self.foregroundFsyncCalls += snapshot.foregroundFsyncCalls
115        self.backgroundFsyncCalls += snapshot.backgroundFsyncCalls
116        self.backgroundWrittenBytes += snapshot.backgroundWrittenBytes
117        self.totalWrittenBytes += snapshot.totalWrittenBytes
118        self.totalFsyncCalls += snapshot.totalFsyncCalls
119        self.appPackages = merge(self.appPackages, snapshot.appPackages)
120
121    def plot(self, foreground=True, background=True, total=True):
122        plt.figure()
123        plt.title("I/O activity for UID %s" % (self.uid))
124        X = range(0,len(self.snapshots))
125        minY = 0
126        maxY = 0
127        if foreground:
128            Y = [s.foregroundWrittenBytes for s in self.snapshots]
129            if any([y > 0 for y in Y]):
130                plt.plot(X, Y, 'b-')
131                minY = min(minY, min(Y))
132                maxY = max(maxY, max(Y))
133        if background:
134            Y = [s.backgroundWrittenBytes for s in self.snapshots]
135            if any([y > 0 for y in Y]):
136                plt.plot(X, Y, 'g-')
137                minY = min(minY, min(Y))
138                maxY = max(maxY, max(Y))
139        if total:
140            Y = [s.totalWrittenBytes for s in self.snapshots]
141            if any([y > 0 for y in Y]):
142                plt.plot(X, Y, 'r-')
143                minY = min(minY, min(Y))
144                maxY = max(maxY, max(Y))
145
146        i = int((maxY - minY) / 5)
147        Yt = list(range(minY, maxY, i))
148        Yl = [prettyPrintBytes(y) for y in Yt]
149        plt.yticks(Yt, Yl)
150        Xt = list(range(0, len(X)))
151        plt.xticks(Xt)
152
153class SystemActivity(object):
154    def __init__(self):
155        self.uids = {}
156        self.snapshots = []
157        self.foregroundWrittenBytes = 0
158        self.foregroundFsyncCalls = 0
159        self.backgroundFsyncCalls = 0
160        self.backgroundWrittenBytes = 0
161        self.totalWrittenBytes = 0
162        self.totalFsyncCalls = 0
163
164    def addSnapshot(self, snapshot):
165        self.snapshots.append(snapshot)
166        self.foregroundWrittenBytes += snapshot.foregroundWrittenBytes
167        self.foregroundFsyncCalls += snapshot.foregroundFsyncCalls
168        self.backgroundFsyncCalls += snapshot.backgroundFsyncCalls
169        self.backgroundWrittenBytes += snapshot.backgroundWrittenBytes
170        self.totalWrittenBytes += snapshot.totalWrittenBytes
171        self.totalFsyncCalls += snapshot.totalFsyncCalls
172        for uid in snapshot.uids:
173            if uid not in self.uids: self.uids[uid] = UserActivity(uid)
174            self.uids[uid].addSnapshot(snapshot.uids[uid])
175
176    def loadDocument(self, doc):
177        for snapshot in doc.snapshots:
178            self.addSnapshot(snapshot)
179
180    def sorted(self, f):
181        return sorted(self.uids.values(), key=f, reverse=True)
182
183    def pie(self):
184        plt.figure()
185        plt.title("Total disk writes per UID")
186        A = [(K, self.uids[K].totalWrittenBytes) for K in self.uids]
187        A = filter(lambda i: i[1] > 0, A)
188        A = list(sorted(A, key=lambda i: i[1], reverse=True))
189        X = [i[1] for i in A]
190        L = [i[0] for i in A]
191        plt.pie(X, labels=L, counterclock=False, startangle=90)
192
193parser = argparse.ArgumentParser("Process FlashApp logs into reports")
194parser.add_argument("filename")
195parser.add_argument("--reportuid", action="append", default=[])
196parser.add_argument("--plotuid", action="append", default=[])
197parser.add_argument("--totalpie", action="store_true", default=False)
198
199args = parser.parse_args()
200
201class UidFilter(object):
202    def __call__(self, uid):
203        return False
204
205class UidFilterAcceptAll(UidFilter):
206    def __call__(self, uid):
207        return True
208
209class UidFilterAcceptSome(UidFilter):
210    def __init__(self, uids):
211        self.uids = uids
212
213    def __call__(self, uid):
214        return uid in self.uids
215
216uidset = set()
217plotfilter = None
218for uid in args.plotuid:
219    if uid == "all":
220        plotfilter = UidFilterAcceptAll()
221        break
222    else:
223        uidset.add(int(uid))
224if plotfilter is None: plotfilter = UidFilterAcceptSome(uidset)
225
226uidset = set()
227reportfilter = None
228for uid in args.reportuid:
229    if uid == "all":
230        reportfilter = UidFilterAcceptAll()
231        break
232    else:
233        uidset.add(int(uid))
234if reportfilter is None:
235    if len(uidset) == 0:
236        reportfilter = UidFilterAcceptAll()
237    else:
238        reportfilter = UidFilterAcceptSome(uidset)
239
240document = Document(open(args.filename))
241print("System runtime: %s\n" % (document.runtime))
242system = SystemActivity()
243system.loadDocument(document)
244
245print("Total bytes written: %s (of which %s in foreground and %s in background)\n" % (
246        prettyPrintBytes(system.totalWrittenBytes),
247        prettyPrintBytes(system.foregroundWrittenBytes),
248        prettyPrintBytes(system.backgroundWrittenBytes)))
249
250writemost = filter(lambda ua: ua.totalWrittenBytes > 0, system.sorted(lambda ua: ua.totalWrittenBytes))
251for entry in writemost:
252    if reportfilter(entry.uid):
253        print("user id %d (%s) wrote %s (of which %s in foreground and %s in background)" % (
254            entry.uid,
255            ','.join(entry.appPackages),
256            prettyPrintBytes(entry.totalWrittenBytes),
257            prettyPrintBytes(entry.foregroundWrittenBytes),
258            prettyPrintBytes(entry.backgroundWrittenBytes)))
259    if plotfilter(entry.uid):
260        entry.plot()
261        plt.show()
262
263if args.totalpie:
264    system.pie()
265    plt.show()
266
267