Lakshman Annadorai | 3c8f88e | 2022-02-16 16:24:51 -0800 | [diff] [blame] | 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 | # |
Jaegeuk Kim | d4ac37c | 2022-08-19 17:22:04 -0700 | [diff] [blame] | 17 | """Trace parser for f2fs traces.""" |
Lakshman Annadorai | 3c8f88e | 2022-02-16 16:24:51 -0800 | [diff] [blame] | 18 | |
| 19 | import collections |
| 20 | import re |
| 21 | |
Jaegeuk Kim | d4ac37c | 2022-08-19 17:22:04 -0700 | [diff] [blame] | 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 |
| 23 | 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]+)" |
Lakshman Annadorai | 3c8f88e | 2022-02-16 16:24:51 -0800 | [diff] [blame] | 24 | |
Jaegeuk Kim | d4ac37c | 2022-08-19 17:22:04 -0700 | [diff] [blame] | 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 |
| 26 | 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]+)" |
Lakshman Annadorai | 3c8f88e | 2022-02-16 16:24:51 -0800 | [diff] [blame] | 27 | |
| 28 | MIN_PID_BYTES = 1024 * 1024 # 1 MiB |
| 29 | SMALL_FILE_BYTES = 1024 # 1 KiB |
| 30 | |
| 31 | |
| 32 | class 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 | |
| 81 | class 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 | |
| 120 | class 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 | |
| 174 | def to_kib(bytes): |
| 175 | return bytes / 1024 |