1#!/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"""Trace parser for f2fs traces.""" 18 19import collections 20import re 21 22# 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 23RE_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]+)" 24 25# 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 26RE_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]+)" 27 28MIN_PID_BYTES = 1024 * 1024 # 1 MiB 29SMALL_FILE_BYTES = 1024 # 1 KiB 30 31 32class ProcessTrace: 33 34 def __init__(self, cmdLine, filename, numBytes): 35 self.cmdLine = cmdLine 36 self.totalBytes = numBytes 37 self.bytesByFiles = {filename: numBytes} 38 39 def add_file_trace(self, filename, numBytes): 40 self.totalBytes += numBytes 41 if filename in self.bytesByFiles: 42 self.bytesByFiles[filename] += numBytes 43 else: 44 self.bytesByFiles[filename] = numBytes 45 46 def dump(self, mode, outputFile): 47 smallFileCnt = 0 48 smallFileBytes = 0 49 for _, numBytes in self.bytesByFiles.items(): 50 if numBytes < SMALL_FILE_BYTES: 51 smallFileCnt += 1 52 smallFileBytes += numBytes 53 54 if (smallFileCnt != 0): 55 outputFile.write( 56 "Process: {}, Traced {} KB: {}, Small file count: {}, Small file KB: {}\n" 57 .format(self.cmdLine, mode, to_kib(self.totalBytes), smallFileCnt, 58 to_kib(smallFileBytes))) 59 60 else: 61 outputFile.write("Process: {}, Traced {} KB: {}\n".format( 62 self.cmdLine, mode, to_kib(self.totalBytes))) 63 64 if (smallFileCnt == len(self.bytesByFiles)): 65 return 66 67 sortedEntries = collections.OrderedDict( 68 sorted( 69 self.bytesByFiles.items(), key=lambda item: item[1], reverse=True)) 70 71 for i in range(len(sortedEntries)): 72 filename, numBytes = sortedEntries.popitem(last=False) 73 if numBytes < SMALL_FILE_BYTES: 74 # Entries are sorted by bytes. So, break on the first small file entry. 75 break 76 77 outputFile.write("File: {}, {} KB: {}\n".format(filename, mode, 78 to_kib(numBytes))) 79 80 81class UidTrace: 82 83 def __init__(self, uid, cmdLine, filename, numBytes): 84 self.uid = uid 85 self.packageName = "" 86 self.totalBytes = numBytes 87 self.traceByProcess = {cmdLine: ProcessTrace(cmdLine, filename, numBytes)} 88 89 def add_process_trace(self, cmdLine, filename, numBytes): 90 self.totalBytes += numBytes 91 if cmdLine in self.traceByProcess: 92 self.traceByProcess[cmdLine].add_file_trace(filename, numBytes) 93 else: 94 self.traceByProcess[cmdLine] = ProcessTrace(cmdLine, filename, numBytes) 95 96 def dump(self, mode, outputFile): 97 outputFile.write("Traced {} KB: {}\n\n".format(mode, 98 to_kib(self.totalBytes))) 99 100 if self.totalBytes < MIN_PID_BYTES: 101 return 102 103 sortedEntries = collections.OrderedDict( 104 sorted( 105 self.traceByProcess.items(), 106 key=lambda item: item[1].totalBytes, 107 reverse=True)) 108 totalEntries = len(sortedEntries) 109 for i in range(totalEntries): 110 _, processTrace = sortedEntries.popitem(last=False) 111 if processTrace.totalBytes < MIN_PID_BYTES: 112 # Entries are sorted by bytes. So, break on the first small PID entry. 113 break 114 115 processTrace.dump(mode, outputFile) 116 if i < totalEntries - 1: 117 outputFile.write("\n") 118 119 120class AndroidFsParser: 121 122 def __init__(self, re_string, uidProcessMapper): 123 self.traceByUid = {} # Key: uid, Value: UidTrace 124 if (re_string == RE_WRITE_START): 125 self.mode = "write" 126 else: 127 self.mode = "read" 128 self.re_matcher = re.compile(re_string) 129 self.uidProcessMapper = uidProcessMapper 130 self.totalBytes = 0 131 132 def parse(self, line): 133 match = self.re_matcher.match(line) 134 if not match: 135 return False 136 try: 137 self.do_parse_start(line, match) 138 except Exception: 139 print("cannot parse: {}".format(line)) 140 raise 141 return True 142 143 def do_parse_start(self, line, match): 144 pid = int(match.group(1)) 145 # start_time = float(match.group(2)) * 1000 #ms 146 filename = match.group(3) 147 # offset = int(match.group(4)) 148 numBytes = int(match.group(5)) 149 cmdLine = match.group(6) 150 pid = int(match.group(7)) 151 # isize = int(match.group(8)) 152 # ino = int(match.group(9)) 153 self.totalBytes += numBytes 154 uid = self.uidProcessMapper.get_uid(cmdLine, pid) 155 156 if uid in self.traceByUid: 157 self.traceByUid[uid].add_process_trace(cmdLine, filename, numBytes) 158 else: 159 self.traceByUid[uid] = UidTrace(uid, cmdLine, filename, numBytes) 160 161 def dumpTotal(self, outputFile): 162 if self.totalBytes > 0: 163 outputFile.write("Traced system-wide {} KB: {}\n\n".format( 164 self.mode, to_kib(self.totalBytes))) 165 166 def dump(self, uid, outputFile): 167 if uid not in self.traceByUid: 168 return 169 170 uidTrace = self.traceByUid[uid] 171 uidTrace.dump(self.mode, outputFile) 172 173 174def to_kib(bytes): 175 return bytes / 1024 176