diff options
-rw-r--r-- | ioblame/androidFsParser.py | 175 | ||||
-rw-r--r-- | ioblame/ioblame.py | 528 | ||||
-rw-r--r-- | ioblame/uidProcessMapper.py | 125 | ||||
-rw-r--r-- | tests/kernel.config/AndroidTest.xml | 2 |
4 files changed, 829 insertions, 1 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) diff --git a/tests/kernel.config/AndroidTest.xml b/tests/kernel.config/AndroidTest.xml index 19da90af..78452e1c 100644 --- a/tests/kernel.config/AndroidTest.xml +++ b/tests/kernel.config/AndroidTest.xml @@ -26,7 +26,7 @@ </target_preparer> <!-- Make sure there is some data in the pstore then reboot --> <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> - <option name="run-command" value="echo HELLOWORLD >/dev/pmsg0" /> + <option name="run-command" value="if [ -e /dev/pmsg0 ] ; then echo HELLOWORLD > /dev/pmsg0; fi" /> <option name="throw-if-cmd-fail" value="true" /> </target_preparer> <target_preparer class="com.android.tradefed.targetprep.RebootTargetPreparer" /> |