parse_resource_usage.py (8417B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 import pathlib 5 import re 6 import sys 7 from datetime import datetime 8 9 MEM_MATCHER = re.compile("([\\d,]*)K:\\s([\\S]*)\\s\\(") 10 11 12 def make_differential_metrics( 13 differential_name, base_measures, mem_measures, cpu_measures 14 ): 15 metrics = [] 16 17 # Setup memory differentials 18 metrics.extend([ 19 { 20 "name": f"{mem_type}-{category}-{differential_name}", 21 "unit": "Kb", 22 "values": [round(mem_usage - base_measures["mem"][mem_type][category], 2)], 23 } 24 for mem_type, mem_info in mem_measures.items() 25 for category, mem_usage in mem_info.items() 26 if category in base_measures["mem"].get(mem_type, {}) 27 ]) 28 metrics.extend([ 29 { 30 "name": f"{mem_type}-total-{differential_name}", 31 "unit": "Kb", 32 "values": [ 33 round( 34 sum(mem_info.values()) 35 - sum(base_measures["mem"][mem_type].values()), 36 2, 37 ) 38 ], 39 } 40 for mem_type, mem_info in mem_measures.items() 41 ]) 42 43 # Setup cpuTime differentials 44 metrics.extend([ 45 { 46 "name": f"cpuTime-{category}-{differential_name}", 47 "unit": "ms", 48 "values": [cpu_time - base_measures["cpu"][category]], 49 } 50 for category, cpu_time in cpu_measures.items() 51 if category in base_measures.get("cpu", {}) 52 ]) 53 metrics.append({ 54 "name": f"cpuTime-total-{differential_name}", 55 "unit": "ms", 56 "values": [ 57 round(sum(cpu_measures.values()) - sum(base_measures["cpu"].values()), 2) 58 ], 59 }) 60 61 return metrics 62 63 64 def get_chrome_process_category(process, binary): 65 if "privileged_process" in process: 66 return "gpu" 67 elif "sandboxed_process" in process: 68 return "tab" 69 elif "zygote" in process: 70 return "zygote" 71 return "main" 72 73 74 def get_fenix_process_category(process, binary): 75 # In the future, we'll also need to catch media/utility procs 76 if "tab" in process: 77 return "tab" 78 elif f"{binary}" in process: 79 return "main" 80 elif "zygote" in process: 81 return "zygote" 82 return process 83 84 85 def get_category_for_process(process, binary): 86 if "fenix" in binary: 87 return get_fenix_process_category(process, binary) 88 elif "chrome" in binary: 89 return get_chrome_process_category(process, binary) 90 raise Exception("Unknown binary for determining process category") 91 92 93 def parse_memory_usage(mem_file, binary): 94 mem_info = [] 95 with mem_file.open() as f: 96 mem_info = f.readlines() 97 98 curr_mem = "" 99 final_mems = {"rss": {}, "pss": {}} 100 for line in mem_info: 101 if not line.strip(): 102 # Anytime a blank line is hit, the current 103 # memory type being tracked changes 104 curr_mem = "" 105 continue 106 if not curr_mem: 107 if "Total RSS by process:" in line: 108 curr_mem = "rss" 109 elif "Total PSS by process:" in line: 110 curr_mem = "pss" 111 continue 112 113 match = MEM_MATCHER.search(line.strip()) 114 if not match: 115 continue 116 117 mem_usage, binary_name = match.groups() 118 if binary not in binary_name: 119 continue 120 121 name_split = binary_name.split(f"{binary}:") 122 if len(name_split) == 1: 123 name = name_split[0] 124 else: 125 name = name_split[-1] 126 127 final_mems[curr_mem][name] = round(float(mem_usage.replace(",", "")), 2) 128 129 measurements = { 130 "rss": {"tab": 0, "gpu": 0, "main": 0, "crashhelper": 0}, 131 "pss": {"tab": 0, "gpu": 0, "main": 0, "crashhelper": 0}, 132 } 133 for mem_type, mem_info in final_mems.items(): 134 for name, mem_usage in mem_info.items(): 135 final_name = get_category_for_process(name, binary) 136 if ( 137 final_name == "zygote" 138 and measurements[mem_type].get("zygote", None) is None 139 ): 140 # Only add this process if it exists (it doesn't exist on fenix) 141 measurements[mem_type]["zygote"] = 0 142 measurements[mem_type][final_name] += mem_usage 143 144 return measurements 145 146 147 def parse_cpu_usage(cpu_file, binary): 148 cpu_info = [] 149 with cpu_file.open() as f: 150 cpu_info = f.readlines() 151 152 # Gather all the final cpu times for the processes 153 final_times = {} 154 for line in cpu_info: 155 if not line.strip(): 156 continue 157 vals = line.split() 158 159 name = vals[0] 160 if f"{binary}" not in name: 161 # Sometimes the PID catches the wrong process 162 continue 163 164 name_split = name.split(f"{binary}:") 165 if len(name_split) == 1: 166 name = name_split[0] 167 else: 168 name = name_split[-1] 169 170 final_times[name] = vals[-2] 171 172 # Convert the final times to milliseconds 173 cpu_times = {"tab": 0, "gpu": 0, "main": 0, "crashhelper": 0} 174 for name, time in final_times.items(): 175 # adb shell ps -o time+= gives us MIN:SEC.HUNDREDTHS. 176 # That's why we divide dt.microseconds by 1000 for measuring in milliseconds. 177 dt = datetime.strptime(time, "%M:%S.%f") 178 milliseconds = (((dt.minute * 60) + dt.second) * 1000) + (dt.microsecond / 1000) 179 180 final_name = get_category_for_process(name, binary) 181 if final_name == "zygote" and cpu_times.get("zygote", None) is None: 182 # Only add this process if it exists (it doesn't exist on fenix) 183 cpu_times["zygote"] = 0 184 185 cpu_times[final_name] += milliseconds 186 187 return cpu_times 188 189 190 def main(): 191 args = sys.argv[1:] 192 binary = args[1] 193 testing_dir = pathlib.Path(args[0]) 194 run_background = True if args[2] == "True" else False 195 196 cpu_info_files = sorted(testing_dir.glob("cpu_info*")) 197 mem_info_files = sorted(testing_dir.glob("mem_info*")) 198 199 perf_metrics = [] 200 base_measures = {} 201 measuring_intervals = ("start", "10%", "50%", "end") 202 if binary == "com.android.chrome": 203 measuring_intervals = ("start", "10%", "50%") 204 205 for i, measurement_time in enumerate(measuring_intervals): 206 cpu_measures = parse_cpu_usage(cpu_info_files[i], binary) 207 mem_measures = parse_memory_usage(mem_info_files[i], binary) 208 209 if not base_measures: 210 base_measures["cpu"] = cpu_measures 211 base_measures["mem"] = mem_measures 212 213 perf_metrics.extend([ 214 { 215 "name": f"cpuTime-{category}-{measurement_time}", 216 "unit": "ms", 217 "values": [cpu_time], 218 } 219 for category, cpu_time in cpu_measures.items() 220 ]) 221 perf_metrics.append({ 222 "name": f"cpuTime-total-{measurement_time}", 223 "unit": "ms", 224 "values": [round(sum(cpu_measures.values()), 2)], 225 }) 226 227 perf_metrics.extend([ 228 { 229 "name": f"{mem_type}-{category}-{measurement_time}", 230 "unit": "Kb", 231 "values": [round(mem_usage, 2)], 232 } 233 for mem_type, mem_info in mem_measures.items() 234 for category, mem_usage in mem_info.items() 235 ]) 236 perf_metrics.extend([ 237 { 238 "name": f"{mem_type}-total-{measurement_time}", 239 "unit": "Kb", 240 "values": [round(sum(mem_info.values()), 2)], 241 } 242 for mem_type, mem_info in mem_measures.items() 243 ]) 244 245 if base_measures and run_background: 246 if measurement_time == "10%": 247 perf_metrics.extend( 248 make_differential_metrics( 249 "backgrounding-diff", base_measures, mem_measures, cpu_measures 250 ) 251 ) 252 elif measurement_time == "end": 253 perf_metrics.extend( 254 make_differential_metrics( 255 "background-diff", base_measures, mem_measures, cpu_measures 256 ) 257 ) 258 259 print( 260 "perfMetrics: " 261 + str(perf_metrics).replace("{", "{{").replace("}", "}}").replace("'", '"') 262 ) 263 264 265 if __name__ == "__main__": 266 main()