summaryrefslogtreecommitdiff
path: root/ioblame
diff options
context:
space:
mode:
authorLakshman Annadorai <lakshmana@google.com>2022-02-16 16:24:51 -0800
committerLakshman Annadorai <lakshmana@google.com>2022-03-09 23:41:10 +0000
commit3c8f88e4c04028e7775fbad8444030ee44b1e99d (patch)
tree3387b2699342fea95eefdda3cb9bf9209aac6516 /ioblame
parent691b4b480d94fe1a246b83aa22a8498188e6baf6 (diff)
downloadextras-3c8f88e4c04028e7775fbad8444030ee44b1e99d.tar.gz
Add ioblame python script.
- This script provides UID -> PID -> File level breakdowns of reads and writes. - Improves the post processing time significatly for large trace files. - Use package metadata from package manager to map UIDs to package names. - Outputs per-UID disk I/O usage reported by the kernel in addition to the disk I/O usage reported in the Android FS trace. - On automotive form-factors, uses CarWatchdog native service to monitor disk I/O stats and dumps them in the output directory. Test: python3 ioblame.py --trace_writes Bug: 219544262 Change-Id: I7d42f66e9a354197e0f444afddd1f0b6ad1bba47
Diffstat (limited to 'ioblame')
-rw-r--r--ioblame/androidFsParser.py175
-rw-r--r--ioblame/ioblame.py528
-rw-r--r--ioblame/uidProcessMapper.py125
3 files changed, 828 insertions, 0 deletions
diff --git a/ioblame/androidFsParser.py b/ioblame/androidFsParser.py
new file mode 100644
index 00000000..6ea38dc3
--- /dev/null
+++ b/ioblame/androidFsParser.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Trace parser for android_fs traces."""
+
+import collections
+import re
+
+# ex) bt_stack_manage-21277 [000] .... 5879.043608: android_fs_datawrite_start: entry_name /misc/bluedroid/bt_config.bak.new, offset 0, bytes 408, cmdline bt_stack_manage, pid 21277, i_size 0, ino 9103
+RE_WRITE_START = r".+-([0-9]+).*\s+([0-9]+\.[0-9]+):\s+android_fs_datawrite_start:\sentry_name\s(\S+)\,\soffset\s([0-9]+)\,\sbytes\s([0-9]+)\,\scmdline\s(\S+)\,\spid\s([0-9]+)\,\si_size\s([0-9]+)\,\sino\s([0-9]+)"
+
+# ex) dumpsys-21321 [001] .... 5877.599324: android_fs_dataread_start: entry_name /system/lib64/libbinder.so, offset 311296, bytes 4096, cmdline dumpsys, pid 21321, i_size 848848, ino 2397
+RE_READ_START = r".+-([0-9]+).*\s+([0-9]+\.[0-9]+):\s+android_fs_dataread_start:\sentry_name\s(\S+)\,\soffset\s([0-9]+)\,\sbytes\s([0-9]+)\,\scmdline\s(\S+)\,\spid\s([0-9]+)\,\si_size\s([0-9]+)\,\sino\s([0-9]+)"
+
+MIN_PID_BYTES = 1024 * 1024 # 1 MiB
+SMALL_FILE_BYTES = 1024 # 1 KiB
+
+
+class ProcessTrace:
+
+ def __init__(self, cmdLine, filename, numBytes):
+ self.cmdLine = cmdLine
+ self.totalBytes = numBytes
+ self.bytesByFiles = {filename: numBytes}
+
+ def add_file_trace(self, filename, numBytes):
+ self.totalBytes += numBytes
+ if filename in self.bytesByFiles:
+ self.bytesByFiles[filename] += numBytes
+ else:
+ self.bytesByFiles[filename] = numBytes
+
+ def dump(self, mode, outputFile):
+ smallFileCnt = 0
+ smallFileBytes = 0
+ for _, numBytes in self.bytesByFiles.items():
+ if numBytes < SMALL_FILE_BYTES:
+ smallFileCnt += 1
+ smallFileBytes += numBytes
+
+ if (smallFileCnt != 0):
+ outputFile.write(
+ "Process: {}, Traced {} KB: {}, Small file count: {}, Small file KB: {}\n"
+ .format(self.cmdLine, mode, to_kib(self.totalBytes), smallFileCnt,
+ to_kib(smallFileBytes)))
+
+ else:
+ outputFile.write("Process: {}, Traced {} KB: {}\n".format(
+ self.cmdLine, mode, to_kib(self.totalBytes)))
+
+ if (smallFileCnt == len(self.bytesByFiles)):
+ return
+
+ sortedEntries = collections.OrderedDict(
+ sorted(
+ self.bytesByFiles.items(), key=lambda item: item[1], reverse=True))
+
+ for i in range(len(sortedEntries)):
+ filename, numBytes = sortedEntries.popitem(last=False)
+ if numBytes < SMALL_FILE_BYTES:
+ # Entries are sorted by bytes. So, break on the first small file entry.
+ break
+
+ outputFile.write("File: {}, {} KB: {}\n".format(filename, mode,
+ to_kib(numBytes)))
+
+
+class UidTrace:
+
+ def __init__(self, uid, cmdLine, filename, numBytes):
+ self.uid = uid
+ self.packageName = ""
+ self.totalBytes = numBytes
+ self.traceByProcess = {cmdLine: ProcessTrace(cmdLine, filename, numBytes)}
+
+ def add_process_trace(self, cmdLine, filename, numBytes):
+ self.totalBytes += numBytes
+ if cmdLine in self.traceByProcess:
+ self.traceByProcess[cmdLine].add_file_trace(filename, numBytes)
+ else:
+ self.traceByProcess[cmdLine] = ProcessTrace(cmdLine, filename, numBytes)
+
+ def dump(self, mode, outputFile):
+ outputFile.write("Traced {} KB: {}\n\n".format(mode,
+ to_kib(self.totalBytes)))
+
+ if self.totalBytes < MIN_PID_BYTES:
+ return
+
+ sortedEntries = collections.OrderedDict(
+ sorted(
+ self.traceByProcess.items(),
+ key=lambda item: item[1].totalBytes,
+ reverse=True))
+ totalEntries = len(sortedEntries)
+ for i in range(totalEntries):
+ _, processTrace = sortedEntries.popitem(last=False)
+ if processTrace.totalBytes < MIN_PID_BYTES:
+ # Entries are sorted by bytes. So, break on the first small PID entry.
+ break
+
+ processTrace.dump(mode, outputFile)
+ if i < totalEntries - 1:
+ outputFile.write("\n")
+
+
+class AndroidFsParser:
+
+ def __init__(self, re_string, uidProcessMapper):
+ self.traceByUid = {} # Key: uid, Value: UidTrace
+ if (re_string == RE_WRITE_START):
+ self.mode = "write"
+ else:
+ self.mode = "read"
+ self.re_matcher = re.compile(re_string)
+ self.uidProcessMapper = uidProcessMapper
+ self.totalBytes = 0
+
+ def parse(self, line):
+ match = self.re_matcher.match(line)
+ if not match:
+ return False
+ try:
+ self.do_parse_start(line, match)
+ except Exception:
+ print("cannot parse: {}".format(line))
+ raise
+ return True
+
+ def do_parse_start(self, line, match):
+ pid = int(match.group(1))
+ # start_time = float(match.group(2)) * 1000 #ms
+ filename = match.group(3)
+ # offset = int(match.group(4))
+ numBytes = int(match.group(5))
+ cmdLine = match.group(6)
+ pid = int(match.group(7))
+ # isize = int(match.group(8))
+ # ino = int(match.group(9))
+ self.totalBytes += numBytes
+ uid = self.uidProcessMapper.get_uid(cmdLine, pid)
+
+ if uid in self.traceByUid:
+ self.traceByUid[uid].add_process_trace(cmdLine, filename, numBytes)
+ else:
+ self.traceByUid[uid] = UidTrace(uid, cmdLine, filename, numBytes)
+
+ def dumpTotal(self, outputFile):
+ if self.totalBytes > 0:
+ outputFile.write("Traced system-wide {} KB: {}\n\n".format(
+ self.mode, to_kib(self.totalBytes)))
+
+ def dump(self, uid, outputFile):
+ if uid not in self.traceByUid:
+ return
+
+ uidTrace = self.traceByUid[uid]
+ uidTrace.dump(self.mode, outputFile)
+
+
+def to_kib(bytes):
+ return bytes / 1024
diff --git a/ioblame/ioblame.py b/ioblame/ioblame.py
new file mode 100644
index 00000000..e00a5ecc
--- /dev/null
+++ b/ioblame/ioblame.py
@@ -0,0 +1,528 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Reports disk I/O usage by UID/Package, process, and file level breakdowns."""
+
+from datetime import datetime
+from collections import namedtuple
+
+import androidFsParser
+import argparse
+import collections
+import os
+import psutil
+import re
+import signal
+import subprocess
+import sys
+import threading
+import time
+import uidProcessMapper
+
+# ex) lrwxrwxrwx 1 root root 16 1970-01-06 13:22 userdata -> /dev/block/sda14
+RE_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+)"
+
+# ex) 1002 246373245 418936352 1818624 0 0 0 0 0 0 0
+RE_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]+)"
+
+# ex) 253 5 dm-5 3117 0 354656 3324 0 0 0 0 0 2696 3324 0 0 0 0
+RE_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]+)"
+
+ADB_CMD = "adb"
+
+TEMP_TRACE_FILE = "temp_trace_file.txt"
+CARWATCHDOG_DUMP = "carwatchdog_dump.txt"
+OUTPUT_FILE = "ioblame_out.txt"
+
+WATCHDOG_BUFFER_SECS = 600
+
+DID_RECEIVE_SIGINT = False
+
+
+def signal_handler(sig, frame):
+ global DID_RECEIVE_SIGINT
+ DID_RECEIVE_SIGINT = True
+ print("Received signal interrupt")
+
+
+def init_arguments():
+ parser = argparse.ArgumentParser(
+ description="Collect and process android_fs traces")
+ parser.add_argument(
+ "-s",
+ "--serial",
+ dest="serial",
+ action="store",
+ help="Android device serial number")
+ parser.add_argument(
+ "-r",
+ "--trace_reads",
+ default=False,
+ action="store_true",
+ dest="traceReads",
+ help="Trace android_fs_dataread_start")
+ parser.add_argument(
+ "-w",
+ "--trace_writes",
+ default=False,
+ action="store_true",
+ dest="traceWrites",
+ help="Trace android_fs_datawrite_start")
+ parser.add_argument(
+ "-d",
+ "--trace_duration",
+ type=int,
+ default=3600,
+ dest="traceDuration",
+ help="Total trace duration in seconds")
+ parser.add_argument(
+ "-i",
+ "--sampling_interval",
+ type=int,
+ default=300,
+ dest="samplingInterval",
+ help="Sampling interval in seconds for CarWatchdog collection (applicable only on"
+ " automotive form-factor")
+ parser.add_argument(
+ "-o",
+ "--output_directory",
+ type=dir_path,
+ default=os.getcwd(),
+ dest="outputDir",
+ help="Output directory")
+
+ return parser.parse_args()
+
+
+def verify_arguments(args):
+ if args.serial is not None:
+ global ADB_CMD
+ ADB_CMD = "%s %s" % ("adb -s", args.serial)
+ if not args.traceReads and not args.traceWrites:
+ raise argparse.ArgumentTypeError(
+ "Must provide at least one of the --trace_reads or --trace_writes options"
+ )
+
+
+def dir_path(path):
+ if os.path.isdir(path):
+ return path
+ else:
+ raise argparse.ArgumentTypeError(
+ "{} is not a valid directory path".format(path))
+
+
+def run_adb_cmd(cmd):
+ r = subprocess.check_output(ADB_CMD + " " + cmd, shell=True)
+ return r.decode("utf-8")
+
+
+def run_adb_shell_cmd(cmd):
+ return run_adb_cmd("shell " + cmd)
+
+
+def run_adb_shell_cmd_strip_output(cmd):
+ return run_adb_cmd("shell " + cmd).strip()
+
+
+def run_adb_shell_cmd_ignore_err(cmd):
+ try:
+ r = subprocess.run(
+ ADB_CMD + " shell " + cmd, shell=True, capture_output=True)
+ return r.stdout.decode("utf-8")
+ except Exception:
+ return ""
+
+
+def run_shell_cmd(cmd):
+ return subprocess.check_output(cmd, shell=True)
+
+
+def run_bg_adb_shell_cmd(cmd):
+ return subprocess.Popen(ADB_CMD + " shell " + cmd, shell=True)
+
+
+def run_bg_shell_cmd(cmd):
+ return subprocess.Popen(cmd, shell=True)
+
+
+def get_block_dev():
+ model = run_adb_shell_cmd_strip_output(
+ "'getprop ro.product.name' | sed \'s/[ \\t\\r\\n]*$//\'")
+ print("Found %s Device" % model)
+
+ if "emu" in model:
+ return "vda"
+
+ result = run_adb_shell_cmd_strip_output(
+ "'ls -la /dev/block/bootdevice/by-name | grep userdata'")
+
+ match = re.compile(RE_LS_BLOCK_DEVICE).match(result)
+ if not match:
+ print("Unknown Device {} -- trying Pixel config".format(model))
+ return "sda"
+
+ return match.group(1)
+
+
+def prep_to_do_something():
+ run_adb_shell_cmd("'echo 3 > /proc/sys/vm/drop_caches'")
+ time.sleep(1)
+
+
+def setup_tracepoints(shouldTraceReads, shouldTraceWrites):
+ # This is a good point to check if the Android FS tracepoints are enabled in the
+ # kernel or not
+ isTraceEnabled = run_adb_shell_cmd(
+ "'if [ -d /sys/kernel/tracing/events/android_fs ]; then echo 0; else echo 1; fi'"
+ )
+
+ if isTraceEnabled == 0:
+ raise RuntimeError("Android FS tracing is not enabled")
+
+ run_adb_shell_cmd("'echo 0 > /sys/kernel/tracing/tracing_on;\
+ echo 0 > /sys/kernel/tracing/trace;\
+ echo 0 > /sys/kernel/tracing/events/ext4/enable;\
+ echo 0 > /sys/kernel/tracing/events/block/enable'")
+
+ if shouldTraceReads:
+ run_adb_shell_cmd(
+ "'echo 1 > /sys/kernel/tracing/events/android_fs/android_fs_dataread_start/enable'"
+ )
+
+ if shouldTraceWrites:
+ run_adb_shell_cmd(
+ "'echo 1 > /sys/kernel/tracing/events/android_fs/android_fs_datawrite_start/enable'"
+ )
+
+ run_adb_shell_cmd("'echo 1 > /sys/kernel/tracing/tracing_on'")
+
+
+def clear_tracing(shouldTraceReads, shouldTraceWrites):
+ if shouldTraceReads:
+ run_adb_shell_cmd(
+ "'echo 0 > /sys/kernel/tracing/events/android_fs/android_fs_dataread_start/enable'"
+ )
+
+ if shouldTraceWrites:
+ run_adb_shell_cmd(
+ "'echo 0 > /sys/kernel/tracing/events/android_fs/android_fs_datawrite_start/enable'"
+ )
+
+ run_adb_shell_cmd("'echo 0 > /sys/kernel/tracing/tracing_on'")
+
+
+def start_streaming_trace(traceFile):
+ return run_bg_adb_shell_cmd(
+ "'cat /sys/kernel/tracing/trace_pipe | grep -e android_fs_data -e android_fs_writepages'\
+ > {}".format(traceFile))
+
+
+def stop_streaming_trace(sub_proc):
+ process = psutil.Process(sub_proc.pid)
+ for child_proc in process.children(recursive=True):
+ child_proc.kill()
+ process.kill()
+
+
+class carwatchdog_collection(threading.Thread):
+
+ def __init__(self, traceDuration, samplingInterval):
+ threading.Thread.__init__(self)
+ self.traceDuration = traceDuration
+ self.samplingInterval = samplingInterval
+
+ def run(self):
+ isBootCompleted = 0
+
+ while isBootCompleted == 0:
+ isBootCompleted = run_adb_shell_cmd_strip_output(
+ "'getprop sys.boot_completed'")
+ time.sleep(1)
+
+ # Clean up previous state.
+ run_adb_shell_cmd(
+ "'dumpsys android.automotive.watchdog.ICarWatchdog/default\
+ --stop_perf &>/dev/null'")
+
+ run_adb_shell_cmd(
+ "'dumpsys android.automotive.watchdog.ICarWatchdog/default \
+ --start_perf --max_duration {} --interval {}'".format(
+ self.traceDuration + WATCHDOG_BUFFER_SECS, self.samplingInterval))
+
+
+def stop_carwatchdog_collection(outputDir):
+ run_adb_shell_cmd("'dumpsys android.automotive.watchdog.ICarWatchdog/default"
+ " --stop_perf' > {}/{}".format(outputDir, CARWATCHDOG_DUMP))
+
+
+def do_something(outpuDir, traceDuration, samplingInterval, uidProcessMapperObj):
+ buildChars = run_adb_shell_cmd_strip_output(
+ "'getprop ro.build.characteristics'")
+
+ carwatchdog_collection_thread = None
+ if "automotive" in buildChars:
+ carwatchdog_collection_thread = carwatchdog_collection(
+ traceDuration, samplingInterval)
+ carwatchdog_collection_thread.start()
+
+ for i in range(1, traceDuration):
+ if DID_RECEIVE_SIGINT:
+ break
+ now = time.process_time()
+ read_uid_process_mapping(uidProcessMapperObj)
+ taken = time.process_time() - now
+ if (taken < 1):
+ time.sleep(1 - taken)
+
+ read_uid_package_mapping(uidProcessMapperObj)
+
+ if "automotive" in buildChars:
+ carwatchdog_collection_thread.join()
+ stop_carwatchdog_collection(outpuDir)
+
+
+def read_uid_process_mapping(uidProcessMapperObj):
+ procStatusDump = run_adb_shell_cmd_ignore_err(
+ "'cat /proc/*/status /proc/*/task/*/status 2> /dev/null'")
+
+ uidProcessMapperObj.parse_proc_status_dump(procStatusDump)
+
+
+def read_uid_package_mapping(uidProcessMapperObj):
+ packageMappingDump = run_adb_shell_cmd_ignore_err(
+ "'pm list packages -a -U | sort | uniq'")
+
+ uidProcessMapperObj.parse_uid_package_dump(packageMappingDump)
+
+
+# Parser for "/proc/diskstats".
+class DiskStats:
+
+ def __init__(self, readIos, readSectors, writeIos, writeSectors):
+ self.readIos = readIos
+ self.readSectors = readSectors
+ self.writeIos = writeIos
+ self.writeSectors = writeSectors
+
+ def delta(self, other):
+ return DiskStats(self.readIos - other.readIos,
+ self.readSectors - other.readSectors,
+ self.writeIos - other.writeIos,
+ self.writeSectors - other.writeSectors)
+
+ def dump(self, shouldDumpReads, shouldDumpWrites, outputFile):
+ if self.readIos is None or self.readIos is None or self.readIos is None\
+ or self.readIos is None:
+ outputFile.write("Missing disk stats")
+ return
+
+ if (shouldDumpReads):
+ outputFile.write("Total dev block reads: {} KB, IOs: {}\n".format(
+ self.readSectors / 2, self.readIos))
+
+ if (shouldDumpWrites):
+ outputFile.write("Total dev block writes: {} KB, IOs: {}\n".format(
+ self.writeSectors / 2, self.writeIos))
+
+
+def get_disk_stats(blockDev):
+ line = run_adb_shell_cmd(
+ "'cat /proc/diskstats' | fgrep -w {}".format(blockDev))
+ matcher = re.compile(RE_DISK_STATS_LINE)
+ match = matcher.match(line)
+
+ if not match:
+ return None
+
+ readIos = int(match.group(4))
+ readSectors = int(match.group(6))
+ writeIos = int(match.group(8))
+ writeSectors = int(match.group(10))
+
+ return DiskStats(readIos, readSectors, writeIos, writeSectors)
+
+
+IoBytes = namedtuple("IoBytes", "rdBytes wrBytes")
+
+
+# Parser for "/proc/uid_io/stats".
+class UidIoStats:
+
+ def __init__(self):
+ self.uidIoStatsReMatcher = re.compile(RE_UID_IO_STATS_LINE)
+ self.ioBytesByUid = {} # Key: UID, Value: IoBytes
+ self.totalIoBytes = IoBytes(rdBytes=0, wrBytes=0)
+
+ def parse(self, dump):
+ totalRdBytes = 0
+ totalWrBytes = 0
+ for line in dump.split("\n"):
+ (uid, ioBytes) = self.parse_uid_io_bytes(line)
+ self.ioBytesByUid[uid] = ioBytes
+ totalRdBytes += ioBytes.rdBytes
+ totalWrBytes += ioBytes.wrBytes
+
+ self.totalIoBytes = IoBytes(rdBytes=totalRdBytes, wrBytes=totalWrBytes)
+
+ def parse_uid_io_bytes(self, line):
+ match = self.uidIoStatsReMatcher.match(line)
+ if not match:
+ return None
+ return (int(match.group(1)),
+ IoBytes(
+ rdBytes=(int(match.group(4)) + int(match.group(8))),
+ wrBytes=(int(match.group(5)) + int(match.group(9)))))
+
+ def delta(self, other):
+ deltaStats = UidIoStats()
+ deltaStats.totalIoBytes = IoBytes(
+ rdBytes=self.totalIoBytes.rdBytes - other.totalIoBytes.rdBytes,
+ wrBytes=self.totalIoBytes.wrBytes - other.totalIoBytes.wrBytes)
+
+ for uid, ioBytes in self.ioBytesByUid.items():
+ if uid not in other.ioBytesByUid:
+ deltaStats.ioBytesByUid[uid] = ioBytes
+ continue
+ otherIoBytes = other.ioBytesByUid[uid]
+ rdBytes = ioBytes.rdBytes - otherIoBytes.rdBytes if ioBytes.rdBytes > otherIoBytes.rdBytes\
+ else 0
+ wrBytes = ioBytes.wrBytes - otherIoBytes.wrBytes if ioBytes.wrBytes > otherIoBytes.wrBytes\
+ else 0
+ deltaStats.ioBytesByUid[uid] = IoBytes(rdBytes=rdBytes, wrBytes=wrBytes)
+ return deltaStats
+
+ def dumpTotal(self, mode, outputFile):
+ totalBytes = self.totalIoBytes.wrBytes if mode == "write" else self.totalIoBytes.rdBytes
+ outputFile.write("Total system-wide {} KB: {}\n".format(
+ mode, to_kib(totalBytes)))
+
+ def dump(self, uidProcessMapperObj, mode, func, outputFile):
+ sortedEntries = collections.OrderedDict(
+ sorted(
+ self.ioBytesByUid.items(),
+ key=lambda item: item[1].wrBytes
+ if mode == "write" else item[1].rdBytes,
+ reverse=True))
+ totalEntries = len(sortedEntries)
+ for i in range(totalEntries):
+ uid, ioBytes = sortedEntries.popitem(last=False)
+ totalBytes = ioBytes.wrBytes if mode == "write" else ioBytes.rdBytes
+ if totalBytes < androidFsParser.MIN_PID_BYTES:
+ continue
+ uidInfo = uidProcessMapperObj.get_uid_info(uid)
+ outputFile.write("{}, Total {} KB: {}\n".format(uidInfo.to_string(), mode,
+ to_kib(totalBytes)))
+ func(uid)
+ outputFile.write("\n" + ("=" * 100) + "\n")
+ if i < totalEntries - 1:
+ outputFile.write("\n")
+
+
+def get_uid_io_stats():
+ uidIoStatsDump = run_adb_shell_cmd_strip_output("'cat /proc/uid_io/stats'")
+ uidIoStats = UidIoStats()
+ uidIoStats.parse(uidIoStatsDump)
+ return uidIoStats
+
+
+def to_kib(bytes):
+ return bytes / 1024
+
+
+def main(argv):
+ signal.signal(signal.SIGINT, signal_handler)
+
+ args = init_arguments()
+ verify_arguments(args)
+
+ run_adb_cmd("root")
+ buildDesc = run_adb_shell_cmd_strip_output("'getprop ro.build.description'")
+ blockDev = get_block_dev()
+
+ prep_to_do_something()
+ setup_tracepoints(args.traceReads, args.traceWrites)
+ diskStatsBefore = get_disk_stats(blockDev)
+ uidIoStatsBefore = get_uid_io_stats()
+
+ traceFile = "{}/{}".format(args.outputDir, TEMP_TRACE_FILE)
+
+ startDateTime = datetime.now()
+ proc = start_streaming_trace(traceFile)
+ print("Started trace streaming")
+
+ uidProcessMapperObj = uidProcessMapper.UidProcessMapper()
+ do_something(args.outputDir, args.traceDuration, args.samplingInterval,
+ uidProcessMapperObj)
+
+ stop_streaming_trace(proc)
+ endDateTime = datetime.now()
+ print("Stopped trace streaming")
+
+ clear_tracing(args.traceReads, args.traceWrites)
+
+ diskStatsAfter = get_disk_stats(blockDev)
+ uidIoStatsAfter = get_uid_io_stats()
+ diskStatsDelta = diskStatsAfter.delta(diskStatsBefore)
+ uidIoStatsDelta = uidIoStatsAfter.delta(uidIoStatsBefore)
+
+ print("Completed device side collection")
+
+ writeParser = androidFsParser.AndroidFsParser(androidFsParser.RE_WRITE_START,
+ uidProcessMapperObj)
+ readParser = androidFsParser.AndroidFsParser(androidFsParser.RE_READ_START,
+ uidProcessMapperObj)
+ with open(traceFile) as file:
+ for line in file:
+ if args.traceWrites and writeParser.parse(line):
+ continue
+ if args.traceReads:
+ readParser.parse(line)
+
+ outputFile = open("{}/{}".format(args.outputDir, OUTPUT_FILE), "w")
+ outputFile.write("Collection datetime: {}, Total duration: {}\n".format(
+ endDateTime, endDateTime - startDateTime))
+ outputFile.write("Build description: {}\n".format(buildDesc))
+ outputFile.write(
+ "Minimum KB per process or UID: {}, Small file KB: {}\n\n".format(
+ to_kib(androidFsParser.MIN_PID_BYTES),
+ to_kib(androidFsParser.SMALL_FILE_BYTES)))
+
+ diskStatsDelta.dump(args.traceReads, args.traceWrites, outputFile)
+
+ if args.traceWrites:
+ uidIoStatsDelta.dumpTotal("write", outputFile)
+ writeParser.dumpTotal(outputFile)
+ uidIoStatsDelta.dump(uidProcessMapperObj, "write",
+ lambda uid: writeParser.dump(uid, outputFile),
+ outputFile)
+
+ if args.traceWrites and args.traceReads:
+ outputFile.write("\n\n\n")
+
+ if args.traceReads:
+ uidIoStatsDelta.dumpTotal("read", outputFile)
+ readParser.dumpTotal(outputFile)
+ uidIoStatsDelta.dump(uidProcessMapperObj, "read",
+ lambda uid: readParser.dump(uid, outputFile),
+ outputFile)
+
+ outputFile.close()
+ run_shell_cmd("rm {}/{}".format(args.outputDir, TEMP_TRACE_FILE))
+
+
+if __name__ == "__main__":
+ main(sys.argv)
diff --git a/ioblame/uidProcessMapper.py b/ioblame/uidProcessMapper.py
new file mode 100644
index 00000000..0c0566b9
--- /dev/null
+++ b/ioblame/uidProcessMapper.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Package <-> UID <-> Process mapper."""
+
+import re
+
+# ex) Name: init
+PROC_STATUS_NAME_LINE = r"Name:\s+(\S+)"
+
+# ex) Pid: 1
+PROC_STATUS_PID_LINE = r"Pid:\s+([0-9]+)"
+
+# ex) Uid: 0 0 0 0
+PROC_STATUS_UID_LINE = r"Uid:\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)"
+
+# ex) package:com.google.android.car.uxr.sample uid:1000
+PACKAGE_UID_LINE = r"package:(\S+)\suid:([0-9]+)"
+
+USER_ID_OFFSET = 100000
+AID_APP_START = 10000
+UNKNOWN_UID = -1
+
+
+class UidInfo:
+
+ def __init__(self, uid, packageName=None):
+ self.uid = uid
+ self.packageName = packageName
+
+ def to_string(self):
+ appId = int(self.uid % USER_ID_OFFSET)
+ if self.uid == UNKNOWN_UID:
+ return "UID: UNKNOWN"
+ elif self.packageName is None and appId < AID_APP_START:
+ return "User ID: {}, Native service AID: {}".format(
+ int(self.uid / USER_ID_OFFSET), appId)
+ elif self.packageName is None:
+ return "User ID: {}, App ID: {}".format(
+ int(self.uid / USER_ID_OFFSET), appId)
+ else:
+ return "User ID: {}, Package name: {}".format(
+ int(self.uid / USER_ID_OFFSET), self.packageName)
+
+
+class UidProcessMapper:
+
+ def __init__(self):
+ self.nameReMatcher = re.compile(PROC_STATUS_NAME_LINE)
+ self.pidReMatcher = re.compile(PROC_STATUS_PID_LINE)
+ self.uidReMatcher = re.compile(PROC_STATUS_UID_LINE)
+ self.packageUidMatcher = re.compile(PACKAGE_UID_LINE)
+ self.uidByProcessDict = {} # Key: Process Name, Value: {PID: UID}
+ self.packageNameByAppId = {} # Key: App ID, Value: Package name
+
+ def parse_proc_status_dump(self, dump):
+ name, pid, uid = "", "", ""
+
+ for line in dump.split("\n"):
+ if line.startswith("Name:"):
+ name = self.match_re(self.nameReMatcher, line)
+ pid, uid = "", ""
+ elif line.startswith("Pid:"):
+ pid = self.match_re(self.pidReMatcher, line)
+ uid = ""
+ elif line.startswith("Uid:"):
+ uid = self.match_re(self.uidReMatcher, line)
+ if name != "" and pid != "" and uid != "":
+ self.add_mapping(name, int(pid), int(uid))
+ name, pid, uid = "", "", ""
+
+ def parse_uid_package_dump(self, dump):
+ for line in dump.split("\n"):
+ if line == "":
+ continue
+
+ match = self.packageUidMatcher.match(line)
+ if (match):
+ packageName = match.group(1)
+ appId = int(match.group(2))
+ if appId in self.packageNameByAppId:
+ self.packageNameByAppId[appId].add(packageName)
+ else:
+ self.packageNameByAppId[appId] = {packageName}
+ else:
+ print("'{}' line doesn't match '{}' regex".format(
+ line, self.packageUidMatcher))
+
+ def match_re(self, reMatcher, line):
+ match = reMatcher.match(line)
+ if not match:
+ return ""
+ return match.group(1)
+
+ def add_mapping(self, name, pid, uid):
+ if name in self.uidByProcessDict:
+ self.uidByProcessDict[name][pid] = uid
+ else:
+ self.uidByProcessDict[name] = {pid: uid}
+
+ def get_uid(self, name, pid):
+ if name in self.uidByProcessDict:
+ if pid in self.uidByProcessDict[name]:
+ return self.uidByProcessDict[name][pid]
+ return UNKNOWN_UID
+
+ def get_uid_info(self, uid):
+ appId = uid % USER_ID_OFFSET
+ if appId in self.packageNameByAppId:
+ return UidInfo(uid, " | ".join(self.packageNameByAppId[appId]))
+ else:
+ return UidInfo(uid)