# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import pathlib import re import sys from datetime import datetime MEM_MATCHER = re.compile("([\\d,]*)K:\\s([\\S]*)\\s\\(") def make_differential_metrics( differential_name, base_measures, mem_measures, cpu_measures ): metrics = [] # Setup memory differentials metrics.extend( [ { "name": f"{mem_type}-{category}-{differential_name}", "unit": "Kb", "values": [ round(mem_usage - base_measures["mem"][mem_type][category], 2) ], } for mem_type, mem_info in mem_measures.items() for category, mem_usage in mem_info.items() ] ) metrics.extend( [ { "name": f"{mem_type}-total-{differential_name}", "unit": "Kb", "values": [ round( sum(mem_info.values()) - sum(base_measures["mem"][mem_type].values()), 2, ) ], } for mem_type, mem_info in mem_measures.items() ] ) # Setup cpuTime differentials metrics.extend( [ { "name": f"cpuTime-{category}-{differential_name}", "unit": "ms", "values": [cpu_time - base_measures["cpu"][category]], } for category, cpu_time in cpu_measures.items() ] ) metrics.append( { "name": f"cpuTime-total-{differential_name}", "unit": "ms", "values": [ round( sum(cpu_measures.values()) - sum(base_measures["cpu"].values()), 2 ) ], } ) return metrics def get_chrome_process_category(process, binary): if "privileged_process" in process: return "gpu" elif "sandboxed_process" in process: return "tab" elif "zygote" in process: return "zygote" return "main" def get_fenix_process_category(process, binary): # In the future, we'll also need to catch media/utility procs if "tab" in process: return "tab" elif f"{binary}" in process: return "main" elif "zygote" in process: return "zygote" return process def get_category_for_process(process, binary): if "fenix" in binary: return get_fenix_process_category(process, binary) elif "chrome" in binary: return get_chrome_process_category(process, binary) raise Exception("Unknown binary for determining process category") def parse_memory_usage(mem_file, binary): mem_info = [] with mem_file.open() as f: mem_info = f.readlines() curr_mem = "" final_mems = {"rss": {}, "pss": {}} for line in mem_info: if not line.strip(): # Anytime a blank line is hit, the current # memory type being tracked changes curr_mem = "" continue if not curr_mem: if "Total RSS by process:" in line: curr_mem = "rss" elif "Total PSS by process:" in line: curr_mem = "pss" continue match = MEM_MATCHER.search(line.strip()) if not match: continue mem_usage, binary_name = match.groups() if binary not in binary_name: continue name_split = binary_name.split(f"{binary}:") if len(name_split) == 1: name = name_split[0] else: name = name_split[-1] final_mems[curr_mem][name] = round(float(mem_usage.replace(",", "")), 2) measurements = { "rss": {"tab": 0, "gpu": 0, "main": 0, "crashhelper": 0}, "pss": {"tab": 0, "gpu": 0, "main": 0, "crashhelper": 0}, } for mem_type, mem_info in final_mems.items(): for name, mem_usage in mem_info.items(): final_name = get_category_for_process(name, binary) if ( final_name == "zygote" and measurements[mem_type].get("zygote", None) is None ): # Only add this process if it exists (it doesn't exist on fenix) measurements[mem_type]["zygote"] = 0 measurements[mem_type][final_name] += mem_usage return measurements def parse_cpu_usage(cpu_file, binary): cpu_info = [] with cpu_file.open() as f: cpu_info = f.readlines() # Gather all the final cpu times for the processes final_times = {} for line in cpu_info: if not line.strip(): continue vals = line.split() name = vals[0] if f"{binary}" not in name: # Sometimes the PID catches the wrong process continue name_split = name.split(f"{binary}:") if len(name_split) == 1: name = name_split[0] else: name = name_split[-1] final_times[name] = vals[-2] # Convert the final times to milliseconds cpu_times = {"tab": 0, "gpu": 0, "main": 0, "crashhelper": 0} for name, time in final_times.items(): # adb shell ps -o time+= gives us MIN:SEC.HUNDREDTHS. # That's why we divide dt.microseconds by 1000 for measuring in milliseconds. dt = datetime.strptime(time, "%M:%S.%f") milliseconds = (((dt.minute * 60) + dt.second) * 1000) + (dt.microsecond / 1000) final_name = get_category_for_process(name, binary) if final_name == "zygote" and cpu_times.get("zygote", None) is None: # Only add this process if it exists (it doesn't exist on fenix) cpu_times["zygote"] = 0 cpu_times[final_name] += milliseconds return cpu_times def main(): args = sys.argv[1:] binary = args[1] testing_dir = pathlib.Path(args[0]) run_background = True if args[2] == "True" else False cpu_info_files = sorted(testing_dir.glob("cpu_info*")) mem_info_files = sorted(testing_dir.glob("mem_info*")) perf_metrics = [] base_measures = {} for i, measurement_time in enumerate(("start", "10%", "50%", "end")): cpu_measures = parse_cpu_usage(cpu_info_files[i], binary) mem_measures = parse_memory_usage(mem_info_files[i], binary) if not base_measures: base_measures["cpu"] = cpu_measures base_measures["mem"] = mem_measures perf_metrics.extend( [ { "name": f"cpuTime-{category}-{measurement_time}", "unit": "ms", "values": [cpu_time], } for category, cpu_time in cpu_measures.items() ] ) perf_metrics.append( { "name": f"cpuTime-total-{measurement_time}", "unit": "ms", "values": [round(sum(cpu_measures.values()), 2)], } ) perf_metrics.extend( [ { "name": f"{mem_type}-{category}-{measurement_time}", "unit": "Kb", "values": [round(mem_usage, 2)], } for mem_type, mem_info in mem_measures.items() for category, mem_usage in mem_info.items() ] ) perf_metrics.extend( [ { "name": f"{mem_type}-total-{measurement_time}", "unit": "Kb", "values": [round(sum(mem_info.values()), 2)], } for mem_type, mem_info in mem_measures.items() ] ) if base_measures and run_background: if measurement_time == "10%": perf_metrics.extend( make_differential_metrics( "backgrounding-diff", base_measures, mem_measures, cpu_measures ) ) elif measurement_time == "end": perf_metrics.extend( make_differential_metrics( "background-diff", base_measures, mem_measures, cpu_measures ) ) print( "perfMetrics: " + str(perf_metrics).replace("{", "{{").replace("}", "}}").replace("'", '"') ) if __name__ == "__main__": main()