| #!/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 f2fs traces.""" |
| |
| import collections |
| import re |
| |
| # ex) bt_stack_manage-21277 [000] .... 5879.043608: f2fs_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+f2fs_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: f2fs_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+f2fs_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 |