blob: d69dda4ef39b7f8fe4768ee654205b74c15beadf [file] [log] [blame]
Lakshman Annadorai3c8f88e2022-02-16 16:24:51 -08001#!/usr/bin/env python
2#
3# Copyright (C) 2022 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17"""Reports disk I/O usage by UID/Package, process, and file level breakdowns."""
18
19from datetime import datetime
20from collections import namedtuple
21
22import androidFsParser
23import argparse
24import collections
25import os
26import psutil
27import re
28import signal
29import subprocess
30import sys
31import threading
32import time
33import uidProcessMapper
34
35# ex) lrwxrwxrwx 1 root root 16 1970-01-06 13:22 userdata -> /dev/block/sda14
36RE_LS_BLOCK_DEVICE = r"\S+\s[0-9]+\s\S+\s\S+\s+[0-9]+\s[0-9\-]+\s[0-9]+\:[0-9]+\suserdata\s\-\>\s\/dev\/block\/(\S+)"
37
38# ex) 1002 246373245 418936352 1818624 0 0 0 0 0 0 0
39RE_UID_IO_STATS_LINE = r"([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)"
40
41# ex) 253 5 dm-5 3117 0 354656 3324 0 0 0 0 0 2696 3324 0 0 0 0
42RE_DISK_STATS_LINE = r"\s+([0-9]+)\s+([0-9]+)\s([a-z\-0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)\s([0-9]+)"
43
44ADB_CMD = "adb"
45
46TEMP_TRACE_FILE = "temp_trace_file.txt"
47CARWATCHDOG_DUMP = "carwatchdog_dump.txt"
48OUTPUT_FILE = "ioblame_out.txt"
49
50WATCHDOG_BUFFER_SECS = 600
51
52DID_RECEIVE_SIGINT = False
53
54
55def signal_handler(sig, frame):
56 global DID_RECEIVE_SIGINT
57 DID_RECEIVE_SIGINT = True
58 print("Received signal interrupt")
59
60
61def init_arguments():
62 parser = argparse.ArgumentParser(
Jaegeuk Kimd4ac37c2022-08-19 17:22:04 -070063 description="Collect and process f2fs traces")
Lakshman Annadorai3c8f88e2022-02-16 16:24:51 -080064 parser.add_argument(
65 "-s",
66 "--serial",
67 dest="serial",
68 action="store",
69 help="Android device serial number")
70 parser.add_argument(
71 "-r",
72 "--trace_reads",
73 default=False,
74 action="store_true",
75 dest="traceReads",
Jaegeuk Kimd4ac37c2022-08-19 17:22:04 -070076 help="Trace f2fs_dataread_start")
Lakshman Annadorai3c8f88e2022-02-16 16:24:51 -080077 parser.add_argument(
78 "-w",
79 "--trace_writes",
80 default=False,
81 action="store_true",
82 dest="traceWrites",
Jaegeuk Kimd4ac37c2022-08-19 17:22:04 -070083 help="Trace f2fs_datawrite_start")
Lakshman Annadorai3c8f88e2022-02-16 16:24:51 -080084 parser.add_argument(
85 "-d",
86 "--trace_duration",
87 type=int,
88 default=3600,
89 dest="traceDuration",
90 help="Total trace duration in seconds")
91 parser.add_argument(
92 "-i",
93 "--sampling_interval",
94 type=int,
95 default=300,
96 dest="samplingInterval",
97 help="Sampling interval in seconds for CarWatchdog collection (applicable only on"
98 " automotive form-factor")
99 parser.add_argument(
100 "-o",
101 "--output_directory",
102 type=dir_path,
103 default=os.getcwd(),
104 dest="outputDir",
105 help="Output directory")
106
107 return parser.parse_args()
108
109
110def verify_arguments(args):
111 if args.serial is not None:
112 global ADB_CMD
113 ADB_CMD = "%s %s" % ("adb -s", args.serial)
114 if not args.traceReads and not args.traceWrites:
115 raise argparse.ArgumentTypeError(
116 "Must provide at least one of the --trace_reads or --trace_writes options"
117 )
118
119
120def dir_path(path):
121 if os.path.isdir(path):
122 return path
123 else:
124 raise argparse.ArgumentTypeError(
125 "{} is not a valid directory path".format(path))
126
127
128def run_adb_cmd(cmd):
129 r = subprocess.check_output(ADB_CMD + " " + cmd, shell=True)
130 return r.decode("utf-8")
131
132
133def run_adb_shell_cmd(cmd):
134 return run_adb_cmd("shell " + cmd)
135
136
137def run_adb_shell_cmd_strip_output(cmd):
138 return run_adb_cmd("shell " + cmd).strip()
139
140
141def run_adb_shell_cmd_ignore_err(cmd):
142 try:
143 r = subprocess.run(
144 ADB_CMD + " shell " + cmd, shell=True, capture_output=True)
145 return r.stdout.decode("utf-8")
146 except Exception:
147 return ""
148
149
150def run_shell_cmd(cmd):
151 return subprocess.check_output(cmd, shell=True)
152
153
154def run_bg_adb_shell_cmd(cmd):
155 return subprocess.Popen(ADB_CMD + " shell " + cmd, shell=True)
156
157
158def run_bg_shell_cmd(cmd):
159 return subprocess.Popen(cmd, shell=True)
160
161
162def get_block_dev():
163 model = run_adb_shell_cmd_strip_output(
164 "'getprop ro.product.name' | sed \'s/[ \\t\\r\\n]*$//\'")
165 print("Found %s Device" % model)
166
167 if "emu" in model:
168 return "vda"
169
170 result = run_adb_shell_cmd_strip_output(
171 "'ls -la /dev/block/bootdevice/by-name | grep userdata'")
172
173 match = re.compile(RE_LS_BLOCK_DEVICE).match(result)
174 if not match:
175 print("Unknown Device {} -- trying Pixel config".format(model))
176 return "sda"
177
178 return match.group(1)
179
180
181def prep_to_do_something():
182 run_adb_shell_cmd("'echo 3 > /proc/sys/vm/drop_caches'")
183 time.sleep(1)
184
185
186def setup_tracepoints(shouldTraceReads, shouldTraceWrites):
187 # This is a good point to check if the Android FS tracepoints are enabled in the
188 # kernel or not
189 isTraceEnabled = run_adb_shell_cmd(
Jaegeuk Kimd4ac37c2022-08-19 17:22:04 -0700190 "'if [ -d /sys/kernel/tracing/events/f2fs ]; then echo 0; else echo 1; fi'"
Lakshman Annadorai3c8f88e2022-02-16 16:24:51 -0800191 )
192
193 if isTraceEnabled == 0:
194 raise RuntimeError("Android FS tracing is not enabled")
195
196 run_adb_shell_cmd("'echo 0 > /sys/kernel/tracing/tracing_on;\
197 echo 0 > /sys/kernel/tracing/trace;\
198 echo 0 > /sys/kernel/tracing/events/ext4/enable;\
199 echo 0 > /sys/kernel/tracing/events/block/enable'")
200
201 if shouldTraceReads:
202 run_adb_shell_cmd(
Jaegeuk Kimd4ac37c2022-08-19 17:22:04 -0700203 "'echo 1 > /sys/kernel/tracing/events/f2fs/f2fs_dataread_start/enable'"
Lakshman Annadorai3c8f88e2022-02-16 16:24:51 -0800204 )
205
206 if shouldTraceWrites:
207 run_adb_shell_cmd(
Jaegeuk Kimd4ac37c2022-08-19 17:22:04 -0700208 "'echo 1 > /sys/kernel/tracing/events/f2fs/f2fs_datawrite_start/enable'"
Lakshman Annadorai3c8f88e2022-02-16 16:24:51 -0800209 )
210
211 run_adb_shell_cmd("'echo 1 > /sys/kernel/tracing/tracing_on'")
212
213
214def clear_tracing(shouldTraceReads, shouldTraceWrites):
215 if shouldTraceReads:
216 run_adb_shell_cmd(
Jaegeuk Kimd4ac37c2022-08-19 17:22:04 -0700217 "'echo 0 > /sys/kernel/tracing/events/f2fs/f2fs_dataread_start/enable'"
Lakshman Annadorai3c8f88e2022-02-16 16:24:51 -0800218 )
219
220 if shouldTraceWrites:
221 run_adb_shell_cmd(
Jaegeuk Kimd4ac37c2022-08-19 17:22:04 -0700222 "'echo 0 > /sys/kernel/tracing/events/f2fs/f2fs_datawrite_start/enable'"
Lakshman Annadorai3c8f88e2022-02-16 16:24:51 -0800223 )
224
225 run_adb_shell_cmd("'echo 0 > /sys/kernel/tracing/tracing_on'")
226
227
228def start_streaming_trace(traceFile):
229 return run_bg_adb_shell_cmd(
Jaegeuk Kimd4ac37c2022-08-19 17:22:04 -0700230 "'cat /sys/kernel/tracing/trace_pipe | grep -e f2fs_data -e f2fs_writepages'\
Lakshman Annadorai3c8f88e2022-02-16 16:24:51 -0800231 > {}".format(traceFile))
232
233
234def stop_streaming_trace(sub_proc):
235 process = psutil.Process(sub_proc.pid)
236 for child_proc in process.children(recursive=True):
237 child_proc.kill()
238 process.kill()
239
240
241class carwatchdog_collection(threading.Thread):
242
243 def __init__(self, traceDuration, samplingInterval):
244 threading.Thread.__init__(self)
245 self.traceDuration = traceDuration
246 self.samplingInterval = samplingInterval
247
248 def run(self):
249 isBootCompleted = 0
250
251 while isBootCompleted == 0:
252 isBootCompleted = run_adb_shell_cmd_strip_output(
253 "'getprop sys.boot_completed'")
254 time.sleep(1)
255
256 # Clean up previous state.
257 run_adb_shell_cmd(
258 "'dumpsys android.automotive.watchdog.ICarWatchdog/default\
259 --stop_perf &>/dev/null'")
260
261 run_adb_shell_cmd(
262 "'dumpsys android.automotive.watchdog.ICarWatchdog/default \
263 --start_perf --max_duration {} --interval {}'".format(
264 self.traceDuration + WATCHDOG_BUFFER_SECS, self.samplingInterval))
265
266
267def stop_carwatchdog_collection(outputDir):
268 run_adb_shell_cmd("'dumpsys android.automotive.watchdog.ICarWatchdog/default"
269 " --stop_perf' > {}/{}".format(outputDir, CARWATCHDOG_DUMP))
270
271
272def do_something(outpuDir, traceDuration, samplingInterval, uidProcessMapperObj):
273 buildChars = run_adb_shell_cmd_strip_output(
274 "'getprop ro.build.characteristics'")
275
276 carwatchdog_collection_thread = None
277 if "automotive" in buildChars:
278 carwatchdog_collection_thread = carwatchdog_collection(
279 traceDuration, samplingInterval)
280 carwatchdog_collection_thread.start()
281
282 for i in range(1, traceDuration):
283 if DID_RECEIVE_SIGINT:
284 break
285 now = time.process_time()
286 read_uid_process_mapping(uidProcessMapperObj)
287 taken = time.process_time() - now
288 if (taken < 1):
289 time.sleep(1 - taken)
290
291 read_uid_package_mapping(uidProcessMapperObj)
292
293 if "automotive" in buildChars:
294 carwatchdog_collection_thread.join()
295 stop_carwatchdog_collection(outpuDir)
296
297
298def read_uid_process_mapping(uidProcessMapperObj):
299 procStatusDump = run_adb_shell_cmd_ignore_err(
300 "'cat /proc/*/status /proc/*/task/*/status 2> /dev/null'")
301
302 uidProcessMapperObj.parse_proc_status_dump(procStatusDump)
303
304
305def read_uid_package_mapping(uidProcessMapperObj):
306 packageMappingDump = run_adb_shell_cmd_ignore_err(
307 "'pm list packages -a -U | sort | uniq'")
308
309 uidProcessMapperObj.parse_uid_package_dump(packageMappingDump)
310
311
312# Parser for "/proc/diskstats".
313class DiskStats:
314
315 def __init__(self, readIos, readSectors, writeIos, writeSectors):
316 self.readIos = readIos
317 self.readSectors = readSectors
318 self.writeIos = writeIos
319 self.writeSectors = writeSectors
320
321 def delta(self, other):
322 return DiskStats(self.readIos - other.readIos,
323 self.readSectors - other.readSectors,
324 self.writeIos - other.writeIos,
325 self.writeSectors - other.writeSectors)
326
327 def dump(self, shouldDumpReads, shouldDumpWrites, outputFile):
328 if self.readIos is None or self.readIos is None or self.readIos is None\
329 or self.readIos is None:
330 outputFile.write("Missing disk stats")
331 return
332
333 if (shouldDumpReads):
334 outputFile.write("Total dev block reads: {} KB, IOs: {}\n".format(
335 self.readSectors / 2, self.readIos))
336
337 if (shouldDumpWrites):
338 outputFile.write("Total dev block writes: {} KB, IOs: {}\n".format(
339 self.writeSectors / 2, self.writeIos))
340
341
342def get_disk_stats(blockDev):
343 line = run_adb_shell_cmd(
344 "'cat /proc/diskstats' | fgrep -w {}".format(blockDev))
345 matcher = re.compile(RE_DISK_STATS_LINE)
346 match = matcher.match(line)
347
348 if not match:
349 return None
350
351 readIos = int(match.group(4))
352 readSectors = int(match.group(6))
353 writeIos = int(match.group(8))
354 writeSectors = int(match.group(10))
355
356 return DiskStats(readIos, readSectors, writeIos, writeSectors)
357
358
359IoBytes = namedtuple("IoBytes", "rdBytes wrBytes")
360
361
362# Parser for "/proc/uid_io/stats".
363class UidIoStats:
364
365 def __init__(self):
366 self.uidIoStatsReMatcher = re.compile(RE_UID_IO_STATS_LINE)
367 self.ioBytesByUid = {} # Key: UID, Value: IoBytes
368 self.totalIoBytes = IoBytes(rdBytes=0, wrBytes=0)
369
370 def parse(self, dump):
371 totalRdBytes = 0
372 totalWrBytes = 0
373 for line in dump.split("\n"):
374 (uid, ioBytes) = self.parse_uid_io_bytes(line)
375 self.ioBytesByUid[uid] = ioBytes
376 totalRdBytes += ioBytes.rdBytes
377 totalWrBytes += ioBytes.wrBytes
378
379 self.totalIoBytes = IoBytes(rdBytes=totalRdBytes, wrBytes=totalWrBytes)
380
381 def parse_uid_io_bytes(self, line):
382 match = self.uidIoStatsReMatcher.match(line)
383 if not match:
384 return None
385 return (int(match.group(1)),
386 IoBytes(
387 rdBytes=(int(match.group(4)) + int(match.group(8))),
388 wrBytes=(int(match.group(5)) + int(match.group(9)))))
389
390 def delta(self, other):
391 deltaStats = UidIoStats()
392 deltaStats.totalIoBytes = IoBytes(
393 rdBytes=self.totalIoBytes.rdBytes - other.totalIoBytes.rdBytes,
394 wrBytes=self.totalIoBytes.wrBytes - other.totalIoBytes.wrBytes)
395
396 for uid, ioBytes in self.ioBytesByUid.items():
397 if uid not in other.ioBytesByUid:
398 deltaStats.ioBytesByUid[uid] = ioBytes
399 continue
400 otherIoBytes = other.ioBytesByUid[uid]
401 rdBytes = ioBytes.rdBytes - otherIoBytes.rdBytes if ioBytes.rdBytes > otherIoBytes.rdBytes\
402 else 0
403 wrBytes = ioBytes.wrBytes - otherIoBytes.wrBytes if ioBytes.wrBytes > otherIoBytes.wrBytes\
404 else 0
405 deltaStats.ioBytesByUid[uid] = IoBytes(rdBytes=rdBytes, wrBytes=wrBytes)
406 return deltaStats
407
408 def dumpTotal(self, mode, outputFile):
409 totalBytes = self.totalIoBytes.wrBytes if mode == "write" else self.totalIoBytes.rdBytes
410 outputFile.write("Total system-wide {} KB: {}\n".format(
411 mode, to_kib(totalBytes)))
412
413 def dump(self, uidProcessMapperObj, mode, func, outputFile):
414 sortedEntries = collections.OrderedDict(
415 sorted(
416 self.ioBytesByUid.items(),
417 key=lambda item: item[1].wrBytes
418 if mode == "write" else item[1].rdBytes,
419 reverse=True))
420 totalEntries = len(sortedEntries)
421 for i in range(totalEntries):
422 uid, ioBytes = sortedEntries.popitem(last=False)
423 totalBytes = ioBytes.wrBytes if mode == "write" else ioBytes.rdBytes
424 if totalBytes < androidFsParser.MIN_PID_BYTES:
425 continue
426 uidInfo = uidProcessMapperObj.get_uid_info(uid)
427 outputFile.write("{}, Total {} KB: {}\n".format(uidInfo.to_string(), mode,
428 to_kib(totalBytes)))
429 func(uid)
430 outputFile.write("\n" + ("=" * 100) + "\n")
431 if i < totalEntries - 1:
432 outputFile.write("\n")
433
434
435def get_uid_io_stats():
436 uidIoStatsDump = run_adb_shell_cmd_strip_output("'cat /proc/uid_io/stats'")
437 uidIoStats = UidIoStats()
438 uidIoStats.parse(uidIoStatsDump)
439 return uidIoStats
440
441
442def to_kib(bytes):
443 return bytes / 1024
444
445
446def main(argv):
447 signal.signal(signal.SIGINT, signal_handler)
448
449 args = init_arguments()
450 verify_arguments(args)
451
452 run_adb_cmd("root")
453 buildDesc = run_adb_shell_cmd_strip_output("'getprop ro.build.description'")
454 blockDev = get_block_dev()
455
456 prep_to_do_something()
457 setup_tracepoints(args.traceReads, args.traceWrites)
458 diskStatsBefore = get_disk_stats(blockDev)
459 uidIoStatsBefore = get_uid_io_stats()
460
461 traceFile = "{}/{}".format(args.outputDir, TEMP_TRACE_FILE)
462
463 startDateTime = datetime.now()
464 proc = start_streaming_trace(traceFile)
465 print("Started trace streaming")
466
467 uidProcessMapperObj = uidProcessMapper.UidProcessMapper()
468 do_something(args.outputDir, args.traceDuration, args.samplingInterval,
469 uidProcessMapperObj)
470
471 stop_streaming_trace(proc)
472 endDateTime = datetime.now()
473 print("Stopped trace streaming")
474
475 clear_tracing(args.traceReads, args.traceWrites)
476
477 diskStatsAfter = get_disk_stats(blockDev)
478 uidIoStatsAfter = get_uid_io_stats()
479 diskStatsDelta = diskStatsAfter.delta(diskStatsBefore)
480 uidIoStatsDelta = uidIoStatsAfter.delta(uidIoStatsBefore)
481
482 print("Completed device side collection")
483
484 writeParser = androidFsParser.AndroidFsParser(androidFsParser.RE_WRITE_START,
485 uidProcessMapperObj)
486 readParser = androidFsParser.AndroidFsParser(androidFsParser.RE_READ_START,
487 uidProcessMapperObj)
488 with open(traceFile) as file:
489 for line in file:
490 if args.traceWrites and writeParser.parse(line):
491 continue
492 if args.traceReads:
493 readParser.parse(line)
494
495 outputFile = open("{}/{}".format(args.outputDir, OUTPUT_FILE), "w")
496 outputFile.write("Collection datetime: {}, Total duration: {}\n".format(
497 endDateTime, endDateTime - startDateTime))
498 outputFile.write("Build description: {}\n".format(buildDesc))
499 outputFile.write(
500 "Minimum KB per process or UID: {}, Small file KB: {}\n\n".format(
501 to_kib(androidFsParser.MIN_PID_BYTES),
502 to_kib(androidFsParser.SMALL_FILE_BYTES)))
503
504 diskStatsDelta.dump(args.traceReads, args.traceWrites, outputFile)
505
506 if args.traceWrites:
507 uidIoStatsDelta.dumpTotal("write", outputFile)
508 writeParser.dumpTotal(outputFile)
509 uidIoStatsDelta.dump(uidProcessMapperObj, "write",
510 lambda uid: writeParser.dump(uid, outputFile),
511 outputFile)
512
513 if args.traceWrites and args.traceReads:
514 outputFile.write("\n\n\n")
515
516 if args.traceReads:
517 uidIoStatsDelta.dumpTotal("read", outputFile)
518 readParser.dumpTotal(outputFile)
519 uidIoStatsDelta.dump(uidProcessMapperObj, "read",
520 lambda uid: readParser.dump(uid, outputFile),
521 outputFile)
522
523 outputFile.close()
524 run_shell_cmd("rm {}/{}".format(args.outputDir, TEMP_TRACE_FILE))
525
526
527if __name__ == "__main__":
528 main(sys.argv)