1e0c48bf9SAdrian Hunter#!/usr/bin/env python3 2e0c48bf9SAdrian Hunter# SPDX-License-Identifier: GPL-2.0 3e0c48bf9SAdrian Hunter# 4e0c48bf9SAdrian Hunter# Run a perf script command multiple times in parallel, using perf script 5e0c48bf9SAdrian Hunter# options --cpu and --time so that each job processes a different chunk 6e0c48bf9SAdrian Hunter# of the data. 7e0c48bf9SAdrian Hunter# 8e0c48bf9SAdrian Hunter# Copyright (c) 2024, Intel Corporation. 9e0c48bf9SAdrian Hunter 10e0c48bf9SAdrian Hunterimport subprocess 11e0c48bf9SAdrian Hunterimport argparse 12e0c48bf9SAdrian Hunterimport pathlib 13e0c48bf9SAdrian Hunterimport shlex 14e0c48bf9SAdrian Hunterimport time 15e0c48bf9SAdrian Hunterimport copy 16e0c48bf9SAdrian Hunterimport sys 17e0c48bf9SAdrian Hunterimport os 18e0c48bf9SAdrian Hunterimport re 19e0c48bf9SAdrian Hunter 20e0c48bf9SAdrian Hunterglb_prog_name = "parallel-perf.py" 21e0c48bf9SAdrian Hunterglb_min_interval = 10.0 22e0c48bf9SAdrian Hunterglb_min_samples = 64 23e0c48bf9SAdrian Hunter 24e0c48bf9SAdrian Hunterclass Verbosity(): 25e0c48bf9SAdrian Hunter 26e0c48bf9SAdrian Hunter def __init__(self, quiet=False, verbose=False, debug=False): 27e0c48bf9SAdrian Hunter self.normal = True 28e0c48bf9SAdrian Hunter self.verbose = verbose 29e0c48bf9SAdrian Hunter self.debug = debug 30e0c48bf9SAdrian Hunter self.self_test = True 31e0c48bf9SAdrian Hunter if self.debug: 32e0c48bf9SAdrian Hunter self.verbose = True 33e0c48bf9SAdrian Hunter if self.verbose: 34e0c48bf9SAdrian Hunter quiet = False 35e0c48bf9SAdrian Hunter if quiet: 36e0c48bf9SAdrian Hunter self.normal = False 37e0c48bf9SAdrian Hunter 38e0c48bf9SAdrian Hunter# Manage work (Start/Wait/Kill), as represented by a subprocess.Popen command 39e0c48bf9SAdrian Hunterclass Work(): 40e0c48bf9SAdrian Hunter 41e0c48bf9SAdrian Hunter def __init__(self, cmd, pipe_to, output_dir="."): 42e0c48bf9SAdrian Hunter self.popen = None 43e0c48bf9SAdrian Hunter self.consumer = None 44e0c48bf9SAdrian Hunter self.cmd = cmd 45e0c48bf9SAdrian Hunter self.pipe_to = pipe_to 46e0c48bf9SAdrian Hunter self.output_dir = output_dir 47e0c48bf9SAdrian Hunter self.cmdout_name = f"{output_dir}/cmd.txt" 48e0c48bf9SAdrian Hunter self.stdout_name = f"{output_dir}/out.txt" 49e0c48bf9SAdrian Hunter self.stderr_name = f"{output_dir}/err.txt" 50e0c48bf9SAdrian Hunter 51e0c48bf9SAdrian Hunter def Command(self): 52e0c48bf9SAdrian Hunter sh_cmd = [ shlex.quote(x) for x in self.cmd ] 53e0c48bf9SAdrian Hunter return " ".join(self.cmd) 54e0c48bf9SAdrian Hunter 55e0c48bf9SAdrian Hunter def Stdout(self): 56e0c48bf9SAdrian Hunter return open(self.stdout_name, "w") 57e0c48bf9SAdrian Hunter 58e0c48bf9SAdrian Hunter def Stderr(self): 59e0c48bf9SAdrian Hunter return open(self.stderr_name, "w") 60e0c48bf9SAdrian Hunter 61e0c48bf9SAdrian Hunter def CreateOutputDir(self): 62e0c48bf9SAdrian Hunter pathlib.Path(self.output_dir).mkdir(parents=True, exist_ok=True) 63e0c48bf9SAdrian Hunter 64e0c48bf9SAdrian Hunter def Start(self): 65e0c48bf9SAdrian Hunter if self.popen: 66e0c48bf9SAdrian Hunter return 67e0c48bf9SAdrian Hunter self.CreateOutputDir() 68e0c48bf9SAdrian Hunter with open(self.cmdout_name, "w") as f: 69e0c48bf9SAdrian Hunter f.write(self.Command()) 70e0c48bf9SAdrian Hunter f.write("\n") 71e0c48bf9SAdrian Hunter stdout = self.Stdout() 72e0c48bf9SAdrian Hunter stderr = self.Stderr() 73e0c48bf9SAdrian Hunter if self.pipe_to: 74e0c48bf9SAdrian Hunter self.popen = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=stderr) 75e0c48bf9SAdrian Hunter args = shlex.split(self.pipe_to) 76e0c48bf9SAdrian Hunter self.consumer = subprocess.Popen(args, stdin=self.popen.stdout, stdout=stdout, stderr=stderr) 77e0c48bf9SAdrian Hunter else: 78e0c48bf9SAdrian Hunter self.popen = subprocess.Popen(self.cmd, stdout=stdout, stderr=stderr) 79e0c48bf9SAdrian Hunter 80e0c48bf9SAdrian Hunter def RemoveEmptyErrFile(self): 81e0c48bf9SAdrian Hunter if os.path.exists(self.stderr_name): 82e0c48bf9SAdrian Hunter if os.path.getsize(self.stderr_name) == 0: 83e0c48bf9SAdrian Hunter os.unlink(self.stderr_name) 84e0c48bf9SAdrian Hunter 85e0c48bf9SAdrian Hunter def Errors(self): 86e0c48bf9SAdrian Hunter if os.path.exists(self.stderr_name): 87e0c48bf9SAdrian Hunter if os.path.getsize(self.stderr_name) != 0: 88e0c48bf9SAdrian Hunter return [ f"Non-empty error file {self.stderr_name}" ] 89e0c48bf9SAdrian Hunter return [] 90e0c48bf9SAdrian Hunter 91e0c48bf9SAdrian Hunter def TidyUp(self): 92e0c48bf9SAdrian Hunter self.RemoveEmptyErrFile() 93e0c48bf9SAdrian Hunter 94e0c48bf9SAdrian Hunter def RawPollWait(self, p, wait): 95e0c48bf9SAdrian Hunter if wait: 96e0c48bf9SAdrian Hunter return p.wait() 97e0c48bf9SAdrian Hunter return p.poll() 98e0c48bf9SAdrian Hunter 99e0c48bf9SAdrian Hunter def Poll(self, wait=False): 100e0c48bf9SAdrian Hunter if not self.popen: 101e0c48bf9SAdrian Hunter return None 102e0c48bf9SAdrian Hunter result = self.RawPollWait(self.popen, wait) 103e0c48bf9SAdrian Hunter if self.consumer: 104e0c48bf9SAdrian Hunter res = result 105e0c48bf9SAdrian Hunter result = self.RawPollWait(self.consumer, wait) 106e0c48bf9SAdrian Hunter if result != None and res == None: 107e0c48bf9SAdrian Hunter self.popen.kill() 108e0c48bf9SAdrian Hunter result = None 109e0c48bf9SAdrian Hunter elif result == 0 and res != None and res != 0: 110e0c48bf9SAdrian Hunter result = res 111e0c48bf9SAdrian Hunter if result != None: 112e0c48bf9SAdrian Hunter self.TidyUp() 113e0c48bf9SAdrian Hunter return result 114e0c48bf9SAdrian Hunter 115e0c48bf9SAdrian Hunter def Wait(self): 116e0c48bf9SAdrian Hunter return self.Poll(wait=True) 117e0c48bf9SAdrian Hunter 118e0c48bf9SAdrian Hunter def Kill(self): 119e0c48bf9SAdrian Hunter if not self.popen: 120e0c48bf9SAdrian Hunter return 121e0c48bf9SAdrian Hunter self.popen.kill() 122e0c48bf9SAdrian Hunter if self.consumer: 123e0c48bf9SAdrian Hunter self.consumer.kill() 124e0c48bf9SAdrian Hunter 125e0c48bf9SAdrian Hunterdef KillWork(worklist, verbosity): 126e0c48bf9SAdrian Hunter for w in worklist: 127e0c48bf9SAdrian Hunter w.Kill() 128e0c48bf9SAdrian Hunter for w in worklist: 129e0c48bf9SAdrian Hunter w.Wait() 130e0c48bf9SAdrian Hunter 131e0c48bf9SAdrian Hunterdef NumberOfCPUs(): 132e0c48bf9SAdrian Hunter return os.sysconf("SC_NPROCESSORS_ONLN") 133e0c48bf9SAdrian Hunter 134e0c48bf9SAdrian Hunterdef NanoSecsToSecsStr(x): 135e0c48bf9SAdrian Hunter if x == None: 136e0c48bf9SAdrian Hunter return "" 137e0c48bf9SAdrian Hunter x = str(x) 138e0c48bf9SAdrian Hunter if len(x) < 10: 139e0c48bf9SAdrian Hunter x = "0" * (10 - len(x)) + x 140e0c48bf9SAdrian Hunter return x[:len(x) - 9] + "." + x[-9:] 141e0c48bf9SAdrian Hunter 142e0c48bf9SAdrian Hunterdef InsertOptionAfter(cmd, option, after): 143e0c48bf9SAdrian Hunter try: 144e0c48bf9SAdrian Hunter pos = cmd.index(after) 145e0c48bf9SAdrian Hunter cmd.insert(pos + 1, option) 146e0c48bf9SAdrian Hunter except: 147e0c48bf9SAdrian Hunter cmd.append(option) 148e0c48bf9SAdrian Hunter 149e0c48bf9SAdrian Hunterdef CreateWorkList(cmd, pipe_to, output_dir, cpus, time_ranges_by_cpu): 150e0c48bf9SAdrian Hunter max_len = len(str(cpus[-1])) 151e0c48bf9SAdrian Hunter cpu_dir_fmt = f"cpu-%.{max_len}u" 152e0c48bf9SAdrian Hunter worklist = [] 153e0c48bf9SAdrian Hunter pos = 0 154e0c48bf9SAdrian Hunter for cpu in cpus: 155e0c48bf9SAdrian Hunter if cpu >= 0: 156e0c48bf9SAdrian Hunter cpu_dir = os.path.join(output_dir, cpu_dir_fmt % cpu) 157e0c48bf9SAdrian Hunter cpu_option = f"--cpu={cpu}" 158e0c48bf9SAdrian Hunter else: 159e0c48bf9SAdrian Hunter cpu_dir = output_dir 160e0c48bf9SAdrian Hunter cpu_option = None 161e0c48bf9SAdrian Hunter 162e0c48bf9SAdrian Hunter tr_dir_fmt = "time-range" 163e0c48bf9SAdrian Hunter 164e0c48bf9SAdrian Hunter if len(time_ranges_by_cpu) > 1: 165e0c48bf9SAdrian Hunter time_ranges = time_ranges_by_cpu[pos] 166e0c48bf9SAdrian Hunter tr_dir_fmt += f"-{pos}" 167e0c48bf9SAdrian Hunter pos += 1 168e0c48bf9SAdrian Hunter else: 169e0c48bf9SAdrian Hunter time_ranges = time_ranges_by_cpu[0] 170e0c48bf9SAdrian Hunter 171e0c48bf9SAdrian Hunter max_len = len(str(len(time_ranges))) 172e0c48bf9SAdrian Hunter tr_dir_fmt += f"-%.{max_len}u" 173e0c48bf9SAdrian Hunter 174e0c48bf9SAdrian Hunter i = 0 175e0c48bf9SAdrian Hunter for r in time_ranges: 176e0c48bf9SAdrian Hunter if r == [None, None]: 177e0c48bf9SAdrian Hunter time_option = None 178e0c48bf9SAdrian Hunter work_output_dir = cpu_dir 179e0c48bf9SAdrian Hunter else: 180e0c48bf9SAdrian Hunter time_option = "--time=" + NanoSecsToSecsStr(r[0]) + "," + NanoSecsToSecsStr(r[1]) 181e0c48bf9SAdrian Hunter work_output_dir = os.path.join(cpu_dir, tr_dir_fmt % i) 182e0c48bf9SAdrian Hunter i += 1 183e0c48bf9SAdrian Hunter work_cmd = list(cmd) 184e0c48bf9SAdrian Hunter if time_option != None: 185e0c48bf9SAdrian Hunter InsertOptionAfter(work_cmd, time_option, "script") 186e0c48bf9SAdrian Hunter if cpu_option != None: 187e0c48bf9SAdrian Hunter InsertOptionAfter(work_cmd, cpu_option, "script") 188e0c48bf9SAdrian Hunter w = Work(work_cmd, pipe_to, work_output_dir) 189e0c48bf9SAdrian Hunter worklist.append(w) 190e0c48bf9SAdrian Hunter return worklist 191e0c48bf9SAdrian Hunter 192e0c48bf9SAdrian Hunterdef DoRunWork(worklist, nr_jobs, verbosity): 193e0c48bf9SAdrian Hunter nr_to_do = len(worklist) 194e0c48bf9SAdrian Hunter not_started = list(worklist) 195e0c48bf9SAdrian Hunter running = [] 196e0c48bf9SAdrian Hunter done = [] 197e0c48bf9SAdrian Hunter chg = False 198e0c48bf9SAdrian Hunter while True: 199e0c48bf9SAdrian Hunter nr_done = len(done) 200e0c48bf9SAdrian Hunter if chg and verbosity.normal: 201e0c48bf9SAdrian Hunter nr_run = len(running) 202e0c48bf9SAdrian Hunter print(f"\rThere are {nr_to_do} jobs: {nr_done} completed, {nr_run} running", flush=True, end=" ") 203e0c48bf9SAdrian Hunter if verbosity.verbose: 204e0c48bf9SAdrian Hunter print() 205e0c48bf9SAdrian Hunter chg = False 206e0c48bf9SAdrian Hunter if nr_done == nr_to_do: 207e0c48bf9SAdrian Hunter break 208e0c48bf9SAdrian Hunter while len(running) < nr_jobs and len(not_started): 209e0c48bf9SAdrian Hunter w = not_started.pop(0) 210e0c48bf9SAdrian Hunter running.append(w) 211e0c48bf9SAdrian Hunter if verbosity.verbose: 212e0c48bf9SAdrian Hunter print("Starting:", w.Command()) 213e0c48bf9SAdrian Hunter w.Start() 214e0c48bf9SAdrian Hunter chg = True 215e0c48bf9SAdrian Hunter if len(running): 216e0c48bf9SAdrian Hunter time.sleep(0.1) 217e0c48bf9SAdrian Hunter finished = [] 218e0c48bf9SAdrian Hunter not_finished = [] 219e0c48bf9SAdrian Hunter while len(running): 220e0c48bf9SAdrian Hunter w = running.pop(0) 221e0c48bf9SAdrian Hunter r = w.Poll() 222e0c48bf9SAdrian Hunter if r == None: 223e0c48bf9SAdrian Hunter not_finished.append(w) 224e0c48bf9SAdrian Hunter continue 225e0c48bf9SAdrian Hunter if r == 0: 226e0c48bf9SAdrian Hunter if verbosity.verbose: 227e0c48bf9SAdrian Hunter print("Finished:", w.Command()) 228e0c48bf9SAdrian Hunter finished.append(w) 229e0c48bf9SAdrian Hunter chg = True 230e0c48bf9SAdrian Hunter continue 231e0c48bf9SAdrian Hunter if verbosity.normal and not verbosity.verbose: 232e0c48bf9SAdrian Hunter print() 233e0c48bf9SAdrian Hunter print("Job failed!\n return code:", r, "\n command: ", w.Command()) 234e0c48bf9SAdrian Hunter if w.pipe_to: 235e0c48bf9SAdrian Hunter print(" piped to: ", w.pipe_to) 236e0c48bf9SAdrian Hunter print("Killing outstanding jobs") 237e0c48bf9SAdrian Hunter KillWork(not_finished, verbosity) 238e0c48bf9SAdrian Hunter KillWork(running, verbosity) 239e0c48bf9SAdrian Hunter return False 240e0c48bf9SAdrian Hunter running = not_finished 241e0c48bf9SAdrian Hunter done += finished 242e0c48bf9SAdrian Hunter errorlist = [] 243e0c48bf9SAdrian Hunter for w in worklist: 244e0c48bf9SAdrian Hunter errorlist += w.Errors() 245e0c48bf9SAdrian Hunter if len(errorlist): 246e0c48bf9SAdrian Hunter print("Errors:") 247e0c48bf9SAdrian Hunter for e in errorlist: 248e0c48bf9SAdrian Hunter print(e) 249e0c48bf9SAdrian Hunter elif verbosity.normal: 250e0c48bf9SAdrian Hunter print("\r"," "*50, "\rAll jobs finished successfully", flush=True) 251e0c48bf9SAdrian Hunter return True 252e0c48bf9SAdrian Hunter 253e0c48bf9SAdrian Hunterdef RunWork(worklist, nr_jobs=NumberOfCPUs(), verbosity=Verbosity()): 254e0c48bf9SAdrian Hunter try: 255e0c48bf9SAdrian Hunter return DoRunWork(worklist, nr_jobs, verbosity) 256e0c48bf9SAdrian Hunter except: 257e0c48bf9SAdrian Hunter for w in worklist: 258e0c48bf9SAdrian Hunter w.Kill() 259e0c48bf9SAdrian Hunter raise 260e0c48bf9SAdrian Hunter return True 261e0c48bf9SAdrian Hunter 262e0c48bf9SAdrian Hunterdef ReadHeader(perf, file_name): 263e0c48bf9SAdrian Hunter return subprocess.Popen([perf, "script", "--header-only", "--input", file_name], stdout=subprocess.PIPE).stdout.read().decode("utf-8") 264e0c48bf9SAdrian Hunter 265e0c48bf9SAdrian Hunterdef ParseHeader(hdr): 266e0c48bf9SAdrian Hunter result = {} 267e0c48bf9SAdrian Hunter lines = hdr.split("\n") 268e0c48bf9SAdrian Hunter for line in lines: 269e0c48bf9SAdrian Hunter if ":" in line and line[0] == "#": 270e0c48bf9SAdrian Hunter pos = line.index(":") 271e0c48bf9SAdrian Hunter name = line[1:pos-1].strip() 272e0c48bf9SAdrian Hunter value = line[pos+1:].strip() 273e0c48bf9SAdrian Hunter if name in result: 274e0c48bf9SAdrian Hunter orig_name = name 275e0c48bf9SAdrian Hunter nr = 2 276e0c48bf9SAdrian Hunter while True: 277e0c48bf9SAdrian Hunter name = f"{orig_name} {nr}" 278e0c48bf9SAdrian Hunter if name not in result: 279e0c48bf9SAdrian Hunter break 280e0c48bf9SAdrian Hunter nr += 1 281e0c48bf9SAdrian Hunter result[name] = value 282e0c48bf9SAdrian Hunter return result 283e0c48bf9SAdrian Hunter 284e0c48bf9SAdrian Hunterdef HeaderField(hdr_dict, hdr_fld): 285e0c48bf9SAdrian Hunter if hdr_fld not in hdr_dict: 286e0c48bf9SAdrian Hunter raise Exception(f"'{hdr_fld}' missing from header information") 287e0c48bf9SAdrian Hunter return hdr_dict[hdr_fld] 288e0c48bf9SAdrian Hunter 289e0c48bf9SAdrian Hunter# Represent the position of an option within a command string 290e0c48bf9SAdrian Hunter# and provide the option value and/or remove the option 291e0c48bf9SAdrian Hunterclass OptPos(): 292e0c48bf9SAdrian Hunter 293e0c48bf9SAdrian Hunter def Init(self, opt_element=-1, value_element=-1, opt_pos=-1, value_pos=-1, error=None): 294e0c48bf9SAdrian Hunter self.opt_element = opt_element # list element that contains option 295e0c48bf9SAdrian Hunter self.value_element = value_element # list element that contains option value 296e0c48bf9SAdrian Hunter self.opt_pos = opt_pos # string position of option 297e0c48bf9SAdrian Hunter self.value_pos = value_pos # string position of value 298e0c48bf9SAdrian Hunter self.error = error # error message string 299e0c48bf9SAdrian Hunter 300e0c48bf9SAdrian Hunter def __init__(self, args, short_name, long_name, default=None): 301e0c48bf9SAdrian Hunter self.args = list(args) 302e0c48bf9SAdrian Hunter self.default = default 303e0c48bf9SAdrian Hunter n = 2 + len(long_name) 304e0c48bf9SAdrian Hunter m = len(short_name) 305e0c48bf9SAdrian Hunter pos = -1 306e0c48bf9SAdrian Hunter for opt in args: 307e0c48bf9SAdrian Hunter pos += 1 308e0c48bf9SAdrian Hunter if m and opt[:2] == f"-{short_name}": 309e0c48bf9SAdrian Hunter if len(opt) == 2: 310e0c48bf9SAdrian Hunter if pos + 1 < len(args): 311e0c48bf9SAdrian Hunter self.Init(pos, pos + 1, 0, 0) 312e0c48bf9SAdrian Hunter else: 313e0c48bf9SAdrian Hunter self.Init(error = f"-{short_name} option missing value") 314e0c48bf9SAdrian Hunter else: 315e0c48bf9SAdrian Hunter self.Init(pos, pos, 0, 2) 316e0c48bf9SAdrian Hunter return 317e0c48bf9SAdrian Hunter if opt[:n] == f"--{long_name}": 318e0c48bf9SAdrian Hunter if len(opt) == n: 319e0c48bf9SAdrian Hunter if pos + 1 < len(args): 320e0c48bf9SAdrian Hunter self.Init(pos, pos + 1, 0, 0) 321e0c48bf9SAdrian Hunter else: 322e0c48bf9SAdrian Hunter self.Init(error = f"--{long_name} option missing value") 323e0c48bf9SAdrian Hunter elif opt[n] == "=": 324e0c48bf9SAdrian Hunter self.Init(pos, pos, 0, n + 1) 325e0c48bf9SAdrian Hunter else: 326e0c48bf9SAdrian Hunter self.Init(error = f"--{long_name} option expected '='") 327e0c48bf9SAdrian Hunter return 328e0c48bf9SAdrian Hunter if m and opt[:1] == "-" and opt[:2] != "--" and short_name in opt: 329e0c48bf9SAdrian Hunter ipos = opt.index(short_name) 330e0c48bf9SAdrian Hunter if "-" in opt[1:]: 331e0c48bf9SAdrian Hunter hpos = opt[1:].index("-") 332e0c48bf9SAdrian Hunter if hpos < ipos: 333e0c48bf9SAdrian Hunter continue 334e0c48bf9SAdrian Hunter if ipos + 1 == len(opt): 335e0c48bf9SAdrian Hunter if pos + 1 < len(args): 336e0c48bf9SAdrian Hunter self.Init(pos, pos + 1, ipos, 0) 337e0c48bf9SAdrian Hunter else: 338e0c48bf9SAdrian Hunter self.Init(error = f"-{short_name} option missing value") 339e0c48bf9SAdrian Hunter else: 340e0c48bf9SAdrian Hunter self.Init(pos, pos, ipos, ipos + 1) 341e0c48bf9SAdrian Hunter return 342e0c48bf9SAdrian Hunter self.Init() 343e0c48bf9SAdrian Hunter 344e0c48bf9SAdrian Hunter def Value(self): 345e0c48bf9SAdrian Hunter if self.opt_element >= 0: 346e0c48bf9SAdrian Hunter if self.opt_element != self.value_element: 347e0c48bf9SAdrian Hunter return self.args[self.value_element] 348e0c48bf9SAdrian Hunter else: 349e0c48bf9SAdrian Hunter return self.args[self.value_element][self.value_pos:] 350e0c48bf9SAdrian Hunter return self.default 351e0c48bf9SAdrian Hunter 352e0c48bf9SAdrian Hunter def Remove(self, args): 353e0c48bf9SAdrian Hunter if self.opt_element == -1: 354e0c48bf9SAdrian Hunter return 355e0c48bf9SAdrian Hunter if self.opt_element != self.value_element: 356e0c48bf9SAdrian Hunter del args[self.value_element] 357e0c48bf9SAdrian Hunter if self.opt_pos: 358e0c48bf9SAdrian Hunter args[self.opt_element] = args[self.opt_element][:self.opt_pos] 359e0c48bf9SAdrian Hunter else: 360e0c48bf9SAdrian Hunter del args[self.opt_element] 361e0c48bf9SAdrian Hunter 362e0c48bf9SAdrian Hunterdef DetermineInputFileName(cmd): 363e0c48bf9SAdrian Hunter p = OptPos(cmd, "i", "input", "perf.data") 364e0c48bf9SAdrian Hunter if p.error: 365e0c48bf9SAdrian Hunter raise Exception(f"perf command {p.error}") 366e0c48bf9SAdrian Hunter file_name = p.Value() 367e0c48bf9SAdrian Hunter if not os.path.exists(file_name): 368e0c48bf9SAdrian Hunter raise Exception(f"perf command input file '{file_name}' not found") 369e0c48bf9SAdrian Hunter return file_name 370e0c48bf9SAdrian Hunter 371e0c48bf9SAdrian Hunterdef ReadOption(args, short_name, long_name, err_prefix, remove=False): 372e0c48bf9SAdrian Hunter p = OptPos(args, short_name, long_name) 373e0c48bf9SAdrian Hunter if p.error: 374e0c48bf9SAdrian Hunter raise Exception(f"{err_prefix}{p.error}") 375e0c48bf9SAdrian Hunter value = p.Value() 376e0c48bf9SAdrian Hunter if remove: 377e0c48bf9SAdrian Hunter p.Remove(args) 378e0c48bf9SAdrian Hunter return value 379e0c48bf9SAdrian Hunter 380e0c48bf9SAdrian Hunterdef ExtractOption(args, short_name, long_name, err_prefix): 381e0c48bf9SAdrian Hunter return ReadOption(args, short_name, long_name, err_prefix, True) 382e0c48bf9SAdrian Hunter 383e0c48bf9SAdrian Hunterdef ReadPerfOption(args, short_name, long_name): 384e0c48bf9SAdrian Hunter return ReadOption(args, short_name, long_name, "perf command ") 385e0c48bf9SAdrian Hunter 386e0c48bf9SAdrian Hunterdef ExtractPerfOption(args, short_name, long_name): 387e0c48bf9SAdrian Hunter return ExtractOption(args, short_name, long_name, "perf command ") 388e0c48bf9SAdrian Hunter 389e0c48bf9SAdrian Hunterdef PerfDoubleQuickCommands(cmd, file_name): 390e0c48bf9SAdrian Hunter cpu_str = ReadPerfOption(cmd, "C", "cpu") 391e0c48bf9SAdrian Hunter time_str = ReadPerfOption(cmd, "", "time") 392e0c48bf9SAdrian Hunter # Use double-quick sampling to determine trace data density 393e0c48bf9SAdrian Hunter times_cmd = ["perf", "script", "--ns", "--input", file_name, "--itrace=qqi"] 394e0c48bf9SAdrian Hunter if cpu_str != None and cpu_str != "": 395e0c48bf9SAdrian Hunter times_cmd.append(f"--cpu={cpu_str}") 396e0c48bf9SAdrian Hunter if time_str != None and time_str != "": 397e0c48bf9SAdrian Hunter times_cmd.append(f"--time={time_str}") 398e0c48bf9SAdrian Hunter cnts_cmd = list(times_cmd) 399e0c48bf9SAdrian Hunter cnts_cmd.append("-Fcpu") 400e0c48bf9SAdrian Hunter times_cmd.append("-Fcpu,time") 401e0c48bf9SAdrian Hunter return cnts_cmd, times_cmd 402e0c48bf9SAdrian Hunter 403e0c48bf9SAdrian Hunterclass CPUTimeRange(): 404e0c48bf9SAdrian Hunter def __init__(self, cpu): 405e0c48bf9SAdrian Hunter self.cpu = cpu 406e0c48bf9SAdrian Hunter self.sample_cnt = 0 407e0c48bf9SAdrian Hunter self.time_ranges = None 408e0c48bf9SAdrian Hunter self.interval = 0 409e0c48bf9SAdrian Hunter self.interval_remaining = 0 410e0c48bf9SAdrian Hunter self.remaining = 0 411e0c48bf9SAdrian Hunter self.tr_pos = 0 412e0c48bf9SAdrian Hunter 413e0c48bf9SAdrian Hunterdef CalcTimeRangesByCPU(line, cpu, cpu_time_ranges, max_time): 414e0c48bf9SAdrian Hunter cpu_time_range = cpu_time_ranges[cpu] 415e0c48bf9SAdrian Hunter cpu_time_range.remaining -= 1 416e0c48bf9SAdrian Hunter cpu_time_range.interval_remaining -= 1 417e0c48bf9SAdrian Hunter if cpu_time_range.remaining == 0: 418e0c48bf9SAdrian Hunter cpu_time_range.time_ranges[cpu_time_range.tr_pos][1] = max_time 419e0c48bf9SAdrian Hunter return 420e0c48bf9SAdrian Hunter if cpu_time_range.interval_remaining == 0: 421e0c48bf9SAdrian Hunter time = TimeVal(line[1][:-1], 0) 422e0c48bf9SAdrian Hunter time_ranges = cpu_time_range.time_ranges 423e0c48bf9SAdrian Hunter time_ranges[cpu_time_range.tr_pos][1] = time - 1 424e0c48bf9SAdrian Hunter time_ranges.append([time, max_time]) 425e0c48bf9SAdrian Hunter cpu_time_range.tr_pos += 1 426e0c48bf9SAdrian Hunter cpu_time_range.interval_remaining = cpu_time_range.interval 427e0c48bf9SAdrian Hunter 428e0c48bf9SAdrian Hunterdef CountSamplesByCPU(line, cpu, cpu_time_ranges): 429e0c48bf9SAdrian Hunter try: 430e0c48bf9SAdrian Hunter cpu_time_ranges[cpu].sample_cnt += 1 431e0c48bf9SAdrian Hunter except: 432e0c48bf9SAdrian Hunter print("exception") 433e0c48bf9SAdrian Hunter print("cpu", cpu) 434e0c48bf9SAdrian Hunter print("len(cpu_time_ranges)", len(cpu_time_ranges)) 435e0c48bf9SAdrian Hunter raise 436e0c48bf9SAdrian Hunter 437e0c48bf9SAdrian Hunterdef ProcessCommandOutputLines(cmd, per_cpu, fn, *x): 438e0c48bf9SAdrian Hunter # Assume CPU number is at beginning of line and enclosed by [] 439e0c48bf9SAdrian Hunter pat = re.compile(r"\s*\[[0-9]+\]") 440e0c48bf9SAdrian Hunter p = subprocess.Popen(cmd, stdout=subprocess.PIPE) 441e0c48bf9SAdrian Hunter while True: 442*7d49ced8SAthira Rajeev line = p.stdout.readline() 443*7d49ced8SAthira Rajeev if line: 444e0c48bf9SAdrian Hunter line = line.decode("utf-8") 445e0c48bf9SAdrian Hunter if pat.match(line): 446e0c48bf9SAdrian Hunter line = line.split() 447e0c48bf9SAdrian Hunter if per_cpu: 448e0c48bf9SAdrian Hunter # Assumes CPU number is enclosed by [] 449e0c48bf9SAdrian Hunter cpu = int(line[0][1:-1]) 450e0c48bf9SAdrian Hunter else: 451e0c48bf9SAdrian Hunter cpu = 0 452e0c48bf9SAdrian Hunter fn(line, cpu, *x) 453e0c48bf9SAdrian Hunter else: 454e0c48bf9SAdrian Hunter break 455e0c48bf9SAdrian Hunter p.wait() 456e0c48bf9SAdrian Hunter 457e0c48bf9SAdrian Hunterdef IntersectTimeRanges(new_time_ranges, time_ranges): 458e0c48bf9SAdrian Hunter pos = 0 459e0c48bf9SAdrian Hunter new_pos = 0 460e0c48bf9SAdrian Hunter # Can assume len(time_ranges) != 0 and len(new_time_ranges) != 0 461e0c48bf9SAdrian Hunter # Note also, there *must* be at least one intersection. 462e0c48bf9SAdrian Hunter while pos < len(time_ranges) and new_pos < len(new_time_ranges): 463e0c48bf9SAdrian Hunter # new end < old start => no intersection, remove new 464e0c48bf9SAdrian Hunter if new_time_ranges[new_pos][1] < time_ranges[pos][0]: 465e0c48bf9SAdrian Hunter del new_time_ranges[new_pos] 466e0c48bf9SAdrian Hunter continue 467e0c48bf9SAdrian Hunter # new start > old end => no intersection, check next 468e0c48bf9SAdrian Hunter if new_time_ranges[new_pos][0] > time_ranges[pos][1]: 469e0c48bf9SAdrian Hunter pos += 1 470e0c48bf9SAdrian Hunter if pos < len(time_ranges): 471e0c48bf9SAdrian Hunter continue 472e0c48bf9SAdrian Hunter # no next, so remove remaining 473e0c48bf9SAdrian Hunter while new_pos < len(new_time_ranges): 474e0c48bf9SAdrian Hunter del new_time_ranges[new_pos] 475e0c48bf9SAdrian Hunter return 476e0c48bf9SAdrian Hunter # Found an intersection 477e0c48bf9SAdrian Hunter # new start < old start => adjust new start = old start 478e0c48bf9SAdrian Hunter if new_time_ranges[new_pos][0] < time_ranges[pos][0]: 479e0c48bf9SAdrian Hunter new_time_ranges[new_pos][0] = time_ranges[pos][0] 480e0c48bf9SAdrian Hunter # new end > old end => keep the overlap, insert the remainder 481e0c48bf9SAdrian Hunter if new_time_ranges[new_pos][1] > time_ranges[pos][1]: 482e0c48bf9SAdrian Hunter r = [ time_ranges[pos][1] + 1, new_time_ranges[new_pos][1] ] 483e0c48bf9SAdrian Hunter new_time_ranges[new_pos][1] = time_ranges[pos][1] 484e0c48bf9SAdrian Hunter new_pos += 1 485e0c48bf9SAdrian Hunter new_time_ranges.insert(new_pos, r) 486e0c48bf9SAdrian Hunter continue 487e0c48bf9SAdrian Hunter # new [start, end] is within old [start, end] 488e0c48bf9SAdrian Hunter new_pos += 1 489e0c48bf9SAdrian Hunter 490e0c48bf9SAdrian Hunterdef SplitTimeRangesByTraceDataDensity(time_ranges, cpus, nr, cmd, file_name, per_cpu, min_size, min_interval, verbosity): 491e0c48bf9SAdrian Hunter if verbosity.normal: 492e0c48bf9SAdrian Hunter print("\rAnalyzing...", flush=True, end=" ") 493e0c48bf9SAdrian Hunter if verbosity.verbose: 494e0c48bf9SAdrian Hunter print() 495e0c48bf9SAdrian Hunter cnts_cmd, times_cmd = PerfDoubleQuickCommands(cmd, file_name) 496e0c48bf9SAdrian Hunter 497e0c48bf9SAdrian Hunter nr_cpus = cpus[-1] + 1 if per_cpu else 1 498e0c48bf9SAdrian Hunter if per_cpu: 499e0c48bf9SAdrian Hunter nr_cpus = cpus[-1] + 1 500e0c48bf9SAdrian Hunter cpu_time_ranges = [ CPUTimeRange(cpu) for cpu in range(nr_cpus) ] 501e0c48bf9SAdrian Hunter else: 502e0c48bf9SAdrian Hunter nr_cpus = 1 503e0c48bf9SAdrian Hunter cpu_time_ranges = [ CPUTimeRange(-1) ] 504e0c48bf9SAdrian Hunter 505e0c48bf9SAdrian Hunter if verbosity.debug: 506e0c48bf9SAdrian Hunter print("nr_cpus", nr_cpus) 507e0c48bf9SAdrian Hunter print("cnts_cmd", cnts_cmd) 508e0c48bf9SAdrian Hunter print("times_cmd", times_cmd) 509e0c48bf9SAdrian Hunter 510e0c48bf9SAdrian Hunter # Count the number of "double quick" samples per CPU 511e0c48bf9SAdrian Hunter ProcessCommandOutputLines(cnts_cmd, per_cpu, CountSamplesByCPU, cpu_time_ranges) 512e0c48bf9SAdrian Hunter 513e0c48bf9SAdrian Hunter tot = 0 514e0c48bf9SAdrian Hunter mx = 0 515e0c48bf9SAdrian Hunter for cpu_time_range in cpu_time_ranges: 516e0c48bf9SAdrian Hunter cnt = cpu_time_range.sample_cnt 517e0c48bf9SAdrian Hunter tot += cnt 518e0c48bf9SAdrian Hunter if cnt > mx: 519e0c48bf9SAdrian Hunter mx = cnt 520e0c48bf9SAdrian Hunter if verbosity.debug: 521e0c48bf9SAdrian Hunter print("cpu:", cpu_time_range.cpu, "sample_cnt", cnt) 522e0c48bf9SAdrian Hunter 523e0c48bf9SAdrian Hunter if min_size < 1: 524e0c48bf9SAdrian Hunter min_size = 1 525e0c48bf9SAdrian Hunter 526e0c48bf9SAdrian Hunter if mx < min_size: 527e0c48bf9SAdrian Hunter # Too little data to be worth splitting 528e0c48bf9SAdrian Hunter if verbosity.debug: 529e0c48bf9SAdrian Hunter print("Too little data to split by time") 530e0c48bf9SAdrian Hunter if nr == 0: 531e0c48bf9SAdrian Hunter nr = 1 532e0c48bf9SAdrian Hunter return [ SplitTimeRangesIntoN(time_ranges, nr, min_interval) ] 533e0c48bf9SAdrian Hunter 534e0c48bf9SAdrian Hunter if nr: 535e0c48bf9SAdrian Hunter divisor = nr 536e0c48bf9SAdrian Hunter min_size = 1 537e0c48bf9SAdrian Hunter else: 538e0c48bf9SAdrian Hunter divisor = NumberOfCPUs() 539e0c48bf9SAdrian Hunter 540e0c48bf9SAdrian Hunter interval = int(round(tot / divisor, 0)) 541e0c48bf9SAdrian Hunter if interval < min_size: 542e0c48bf9SAdrian Hunter interval = min_size 543e0c48bf9SAdrian Hunter 544e0c48bf9SAdrian Hunter if verbosity.debug: 545e0c48bf9SAdrian Hunter print("divisor", divisor) 546e0c48bf9SAdrian Hunter print("min_size", min_size) 547e0c48bf9SAdrian Hunter print("interval", interval) 548e0c48bf9SAdrian Hunter 549e0c48bf9SAdrian Hunter min_time = time_ranges[0][0] 550e0c48bf9SAdrian Hunter max_time = time_ranges[-1][1] 551e0c48bf9SAdrian Hunter 552e0c48bf9SAdrian Hunter for cpu_time_range in cpu_time_ranges: 553e0c48bf9SAdrian Hunter cnt = cpu_time_range.sample_cnt 554e0c48bf9SAdrian Hunter if cnt == 0: 555e0c48bf9SAdrian Hunter cpu_time_range.time_ranges = copy.deepcopy(time_ranges) 556e0c48bf9SAdrian Hunter continue 557e0c48bf9SAdrian Hunter # Adjust target interval for CPU to give approximately equal interval sizes 558e0c48bf9SAdrian Hunter # Determine number of intervals, rounding to nearest integer 559e0c48bf9SAdrian Hunter n = int(round(cnt / interval, 0)) 560e0c48bf9SAdrian Hunter if n < 1: 561e0c48bf9SAdrian Hunter n = 1 562e0c48bf9SAdrian Hunter # Determine interval size, rounding up 563e0c48bf9SAdrian Hunter d, m = divmod(cnt, n) 564e0c48bf9SAdrian Hunter if m: 565e0c48bf9SAdrian Hunter d += 1 566e0c48bf9SAdrian Hunter cpu_time_range.interval = d 567e0c48bf9SAdrian Hunter cpu_time_range.interval_remaining = d 568e0c48bf9SAdrian Hunter cpu_time_range.remaining = cnt 569e0c48bf9SAdrian Hunter # Init. time ranges for each CPU with the start time 570e0c48bf9SAdrian Hunter cpu_time_range.time_ranges = [ [min_time, max_time] ] 571e0c48bf9SAdrian Hunter 572e0c48bf9SAdrian Hunter # Set time ranges so that the same number of "double quick" samples 573e0c48bf9SAdrian Hunter # will fall into each time range. 574e0c48bf9SAdrian Hunter ProcessCommandOutputLines(times_cmd, per_cpu, CalcTimeRangesByCPU, cpu_time_ranges, max_time) 575e0c48bf9SAdrian Hunter 576e0c48bf9SAdrian Hunter for cpu_time_range in cpu_time_ranges: 577e0c48bf9SAdrian Hunter if cpu_time_range.sample_cnt: 578e0c48bf9SAdrian Hunter IntersectTimeRanges(cpu_time_range.time_ranges, time_ranges) 579e0c48bf9SAdrian Hunter 580e0c48bf9SAdrian Hunter return [cpu_time_ranges[cpu].time_ranges for cpu in cpus] 581e0c48bf9SAdrian Hunter 582e0c48bf9SAdrian Hunterdef SplitSingleTimeRangeIntoN(time_range, n): 583e0c48bf9SAdrian Hunter if n <= 1: 584e0c48bf9SAdrian Hunter return [time_range] 585e0c48bf9SAdrian Hunter start = time_range[0] 586e0c48bf9SAdrian Hunter end = time_range[1] 587e0c48bf9SAdrian Hunter duration = int((end - start + 1) / n) 588e0c48bf9SAdrian Hunter if duration < 1: 589e0c48bf9SAdrian Hunter return [time_range] 590e0c48bf9SAdrian Hunter time_ranges = [] 591e0c48bf9SAdrian Hunter for i in range(n): 592e0c48bf9SAdrian Hunter time_ranges.append([start, start + duration - 1]) 593e0c48bf9SAdrian Hunter start += duration 594e0c48bf9SAdrian Hunter time_ranges[-1][1] = end 595e0c48bf9SAdrian Hunter return time_ranges 596e0c48bf9SAdrian Hunter 597e0c48bf9SAdrian Hunterdef TimeRangeDuration(r): 598e0c48bf9SAdrian Hunter return r[1] - r[0] + 1 599e0c48bf9SAdrian Hunter 600e0c48bf9SAdrian Hunterdef TotalDuration(time_ranges): 601e0c48bf9SAdrian Hunter duration = 0 602e0c48bf9SAdrian Hunter for r in time_ranges: 603e0c48bf9SAdrian Hunter duration += TimeRangeDuration(r) 604e0c48bf9SAdrian Hunter return duration 605e0c48bf9SAdrian Hunter 606e0c48bf9SAdrian Hunterdef SplitTimeRangesByInterval(time_ranges, interval): 607e0c48bf9SAdrian Hunter new_ranges = [] 608e0c48bf9SAdrian Hunter for r in time_ranges: 609e0c48bf9SAdrian Hunter duration = TimeRangeDuration(r) 610e0c48bf9SAdrian Hunter n = duration / interval 611e0c48bf9SAdrian Hunter n = int(round(n, 0)) 612e0c48bf9SAdrian Hunter new_ranges += SplitSingleTimeRangeIntoN(r, n) 613e0c48bf9SAdrian Hunter return new_ranges 614e0c48bf9SAdrian Hunter 615e0c48bf9SAdrian Hunterdef SplitTimeRangesIntoN(time_ranges, n, min_interval): 616e0c48bf9SAdrian Hunter if n <= len(time_ranges): 617e0c48bf9SAdrian Hunter return time_ranges 618e0c48bf9SAdrian Hunter duration = TotalDuration(time_ranges) 619e0c48bf9SAdrian Hunter interval = duration / n 620e0c48bf9SAdrian Hunter if interval < min_interval: 621e0c48bf9SAdrian Hunter interval = min_interval 622e0c48bf9SAdrian Hunter return SplitTimeRangesByInterval(time_ranges, interval) 623e0c48bf9SAdrian Hunter 624e0c48bf9SAdrian Hunterdef RecombineTimeRanges(tr): 625e0c48bf9SAdrian Hunter new_tr = copy.deepcopy(tr) 626e0c48bf9SAdrian Hunter n = len(new_tr) 627e0c48bf9SAdrian Hunter i = 1 628e0c48bf9SAdrian Hunter while i < len(new_tr): 629e0c48bf9SAdrian Hunter # if prev end + 1 == cur start, combine them 630e0c48bf9SAdrian Hunter if new_tr[i - 1][1] + 1 == new_tr[i][0]: 631e0c48bf9SAdrian Hunter new_tr[i][0] = new_tr[i - 1][0] 632e0c48bf9SAdrian Hunter del new_tr[i - 1] 633e0c48bf9SAdrian Hunter else: 634e0c48bf9SAdrian Hunter i += 1 635e0c48bf9SAdrian Hunter return new_tr 636e0c48bf9SAdrian Hunter 637e0c48bf9SAdrian Hunterdef OpenTimeRangeEnds(time_ranges, min_time, max_time): 638e0c48bf9SAdrian Hunter if time_ranges[0][0] <= min_time: 639e0c48bf9SAdrian Hunter time_ranges[0][0] = None 640e0c48bf9SAdrian Hunter if time_ranges[-1][1] >= max_time: 641e0c48bf9SAdrian Hunter time_ranges[-1][1] = None 642e0c48bf9SAdrian Hunter 643e0c48bf9SAdrian Hunterdef BadTimeStr(time_str): 644e0c48bf9SAdrian Hunter raise Exception(f"perf command bad time option: '{time_str}'\nCheck also 'time of first sample' and 'time of last sample' in perf script --header-only") 645e0c48bf9SAdrian Hunter 646e0c48bf9SAdrian Hunterdef ValidateTimeRanges(time_ranges, time_str): 647e0c48bf9SAdrian Hunter n = len(time_ranges) 648e0c48bf9SAdrian Hunter for i in range(n): 649e0c48bf9SAdrian Hunter start = time_ranges[i][0] 650e0c48bf9SAdrian Hunter end = time_ranges[i][1] 651e0c48bf9SAdrian Hunter if i != 0 and start <= time_ranges[i - 1][1]: 652e0c48bf9SAdrian Hunter BadTimeStr(time_str) 653e0c48bf9SAdrian Hunter if start > end: 654e0c48bf9SAdrian Hunter BadTimeStr(time_str) 655e0c48bf9SAdrian Hunter 656e0c48bf9SAdrian Hunterdef TimeVal(s, dflt): 657e0c48bf9SAdrian Hunter s = s.strip() 658e0c48bf9SAdrian Hunter if s == "": 659e0c48bf9SAdrian Hunter return dflt 660e0c48bf9SAdrian Hunter a = s.split(".") 661e0c48bf9SAdrian Hunter if len(a) > 2: 662e0c48bf9SAdrian Hunter raise Exception(f"Bad time value'{s}'") 663e0c48bf9SAdrian Hunter x = int(a[0]) 664e0c48bf9SAdrian Hunter if x < 0: 665e0c48bf9SAdrian Hunter raise Exception("Negative time not allowed") 666e0c48bf9SAdrian Hunter x *= 1000000000 667e0c48bf9SAdrian Hunter if len(a) > 1: 668e0c48bf9SAdrian Hunter x += int((a[1] + "000000000")[:9]) 669e0c48bf9SAdrian Hunter return x 670e0c48bf9SAdrian Hunter 671e0c48bf9SAdrian Hunterdef BadCPUStr(cpu_str): 672e0c48bf9SAdrian Hunter raise Exception(f"perf command bad cpu option: '{cpu_str}'\nCheck also 'nrcpus avail' in perf script --header-only") 673e0c48bf9SAdrian Hunter 674e0c48bf9SAdrian Hunterdef ParseTimeStr(time_str, min_time, max_time): 675e0c48bf9SAdrian Hunter if time_str == None or time_str == "": 676e0c48bf9SAdrian Hunter return [[min_time, max_time]] 677e0c48bf9SAdrian Hunter time_ranges = [] 678e0c48bf9SAdrian Hunter for r in time_str.split(): 679e0c48bf9SAdrian Hunter a = r.split(",") 680e0c48bf9SAdrian Hunter if len(a) != 2: 681e0c48bf9SAdrian Hunter BadTimeStr(time_str) 682e0c48bf9SAdrian Hunter try: 683e0c48bf9SAdrian Hunter start = TimeVal(a[0], min_time) 684e0c48bf9SAdrian Hunter end = TimeVal(a[1], max_time) 685e0c48bf9SAdrian Hunter except: 686e0c48bf9SAdrian Hunter BadTimeStr(time_str) 687e0c48bf9SAdrian Hunter time_ranges.append([start, end]) 688e0c48bf9SAdrian Hunter ValidateTimeRanges(time_ranges, time_str) 689e0c48bf9SAdrian Hunter return time_ranges 690e0c48bf9SAdrian Hunter 691e0c48bf9SAdrian Hunterdef ParseCPUStr(cpu_str, nr_cpus): 692e0c48bf9SAdrian Hunter if cpu_str == None or cpu_str == "": 693e0c48bf9SAdrian Hunter return [-1] 694e0c48bf9SAdrian Hunter cpus = [] 695e0c48bf9SAdrian Hunter for r in cpu_str.split(","): 696e0c48bf9SAdrian Hunter a = r.split("-") 697e0c48bf9SAdrian Hunter if len(a) < 1 or len(a) > 2: 698e0c48bf9SAdrian Hunter BadCPUStr(cpu_str) 699e0c48bf9SAdrian Hunter try: 700e0c48bf9SAdrian Hunter start = int(a[0].strip()) 701e0c48bf9SAdrian Hunter if len(a) > 1: 702e0c48bf9SAdrian Hunter end = int(a[1].strip()) 703e0c48bf9SAdrian Hunter else: 704e0c48bf9SAdrian Hunter end = start 705e0c48bf9SAdrian Hunter except: 706e0c48bf9SAdrian Hunter BadCPUStr(cpu_str) 707e0c48bf9SAdrian Hunter if start < 0 or end < 0 or end < start or end >= nr_cpus: 708e0c48bf9SAdrian Hunter BadCPUStr(cpu_str) 709e0c48bf9SAdrian Hunter cpus.extend(range(start, end + 1)) 710e0c48bf9SAdrian Hunter cpus = list(set(cpus)) # Remove duplicates 711e0c48bf9SAdrian Hunter cpus.sort() 712e0c48bf9SAdrian Hunter return cpus 713e0c48bf9SAdrian Hunter 714e0c48bf9SAdrian Hunterclass ParallelPerf(): 715e0c48bf9SAdrian Hunter 716e0c48bf9SAdrian Hunter def __init__(self, a): 717e0c48bf9SAdrian Hunter for arg_name in vars(a): 718e0c48bf9SAdrian Hunter setattr(self, arg_name, getattr(a, arg_name)) 719e0c48bf9SAdrian Hunter self.orig_nr = self.nr 720e0c48bf9SAdrian Hunter self.orig_cmd = list(self.cmd) 721e0c48bf9SAdrian Hunter self.perf = self.cmd[0] 722e0c48bf9SAdrian Hunter if os.path.exists(self.output_dir): 723e0c48bf9SAdrian Hunter raise Exception(f"Output '{self.output_dir}' already exists") 724e0c48bf9SAdrian Hunter if self.jobs < 0 or self.nr < 0 or self.interval < 0: 725e0c48bf9SAdrian Hunter raise Exception("Bad options (negative values): try -h option for help") 726e0c48bf9SAdrian Hunter if self.nr != 0 and self.interval != 0: 727e0c48bf9SAdrian Hunter raise Exception("Cannot specify number of time subdivisions and time interval") 728e0c48bf9SAdrian Hunter if self.jobs == 0: 729e0c48bf9SAdrian Hunter self.jobs = NumberOfCPUs() 730e0c48bf9SAdrian Hunter if self.nr == 0 and self.interval == 0: 731e0c48bf9SAdrian Hunter if self.per_cpu: 732e0c48bf9SAdrian Hunter self.nr = 1 733e0c48bf9SAdrian Hunter else: 734e0c48bf9SAdrian Hunter self.nr = self.jobs 735e0c48bf9SAdrian Hunter 736e0c48bf9SAdrian Hunter def Init(self): 737e0c48bf9SAdrian Hunter if self.verbosity.debug: 738e0c48bf9SAdrian Hunter print("cmd", self.cmd) 739e0c48bf9SAdrian Hunter self.file_name = DetermineInputFileName(self.cmd) 740e0c48bf9SAdrian Hunter self.hdr = ReadHeader(self.perf, self.file_name) 741e0c48bf9SAdrian Hunter self.hdr_dict = ParseHeader(self.hdr) 742e0c48bf9SAdrian Hunter self.cmd_line = HeaderField(self.hdr_dict, "cmdline") 743e0c48bf9SAdrian Hunter 744e0c48bf9SAdrian Hunter def ExtractTimeInfo(self): 745e0c48bf9SAdrian Hunter self.min_time = TimeVal(HeaderField(self.hdr_dict, "time of first sample"), 0) 746e0c48bf9SAdrian Hunter self.max_time = TimeVal(HeaderField(self.hdr_dict, "time of last sample"), 0) 747e0c48bf9SAdrian Hunter self.time_str = ExtractPerfOption(self.cmd, "", "time") 748e0c48bf9SAdrian Hunter self.time_ranges = ParseTimeStr(self.time_str, self.min_time, self.max_time) 749e0c48bf9SAdrian Hunter if self.verbosity.debug: 750e0c48bf9SAdrian Hunter print("time_ranges", self.time_ranges) 751e0c48bf9SAdrian Hunter 752e0c48bf9SAdrian Hunter def ExtractCPUInfo(self): 753e0c48bf9SAdrian Hunter if self.per_cpu: 754e0c48bf9SAdrian Hunter nr_cpus = int(HeaderField(self.hdr_dict, "nrcpus avail")) 755e0c48bf9SAdrian Hunter self.cpu_str = ExtractPerfOption(self.cmd, "C", "cpu") 756e0c48bf9SAdrian Hunter if self.cpu_str == None or self.cpu_str == "": 757e0c48bf9SAdrian Hunter self.cpus = [ x for x in range(nr_cpus) ] 758e0c48bf9SAdrian Hunter else: 759e0c48bf9SAdrian Hunter self.cpus = ParseCPUStr(self.cpu_str, nr_cpus) 760e0c48bf9SAdrian Hunter else: 761e0c48bf9SAdrian Hunter self.cpu_str = None 762e0c48bf9SAdrian Hunter self.cpus = [-1] 763e0c48bf9SAdrian Hunter if self.verbosity.debug: 764e0c48bf9SAdrian Hunter print("cpus", self.cpus) 765e0c48bf9SAdrian Hunter 766e0c48bf9SAdrian Hunter def IsIntelPT(self): 767e0c48bf9SAdrian Hunter return self.cmd_line.find("intel_pt") >= 0 768e0c48bf9SAdrian Hunter 769e0c48bf9SAdrian Hunter def SplitTimeRanges(self): 770e0c48bf9SAdrian Hunter if self.IsIntelPT() and self.interval == 0: 771e0c48bf9SAdrian Hunter self.split_time_ranges_for_each_cpu = \ 772e0c48bf9SAdrian Hunter SplitTimeRangesByTraceDataDensity(self.time_ranges, self.cpus, self.orig_nr, 773e0c48bf9SAdrian Hunter self.orig_cmd, self.file_name, self.per_cpu, 774e0c48bf9SAdrian Hunter self.min_size, self.min_interval, self.verbosity) 775e0c48bf9SAdrian Hunter elif self.nr: 776e0c48bf9SAdrian Hunter self.split_time_ranges_for_each_cpu = [ SplitTimeRangesIntoN(self.time_ranges, self.nr, self.min_interval) ] 777e0c48bf9SAdrian Hunter else: 778e0c48bf9SAdrian Hunter self.split_time_ranges_for_each_cpu = [ SplitTimeRangesByInterval(self.time_ranges, self.interval) ] 779e0c48bf9SAdrian Hunter 780e0c48bf9SAdrian Hunter def CheckTimeRanges(self): 781e0c48bf9SAdrian Hunter for tr in self.split_time_ranges_for_each_cpu: 782e0c48bf9SAdrian Hunter # Re-combined time ranges should be the same 783e0c48bf9SAdrian Hunter new_tr = RecombineTimeRanges(tr) 784e0c48bf9SAdrian Hunter if new_tr != self.time_ranges: 785e0c48bf9SAdrian Hunter if self.verbosity.debug: 786e0c48bf9SAdrian Hunter print("tr", tr) 787e0c48bf9SAdrian Hunter print("new_tr", new_tr) 788e0c48bf9SAdrian Hunter raise Exception("Self test failed!") 789e0c48bf9SAdrian Hunter 790e0c48bf9SAdrian Hunter def OpenTimeRangeEnds(self): 791e0c48bf9SAdrian Hunter for time_ranges in self.split_time_ranges_for_each_cpu: 792e0c48bf9SAdrian Hunter OpenTimeRangeEnds(time_ranges, self.min_time, self.max_time) 793e0c48bf9SAdrian Hunter 794e0c48bf9SAdrian Hunter def CreateWorkList(self): 795e0c48bf9SAdrian Hunter self.worklist = CreateWorkList(self.cmd, self.pipe_to, self.output_dir, self.cpus, self.split_time_ranges_for_each_cpu) 796e0c48bf9SAdrian Hunter 797e0c48bf9SAdrian Hunter def PerfDataRecordedPerCPU(self): 798e0c48bf9SAdrian Hunter if "--per-thread" in self.cmd_line.split(): 799e0c48bf9SAdrian Hunter return False 800e0c48bf9SAdrian Hunter return True 801e0c48bf9SAdrian Hunter 802e0c48bf9SAdrian Hunter def DefaultToPerCPU(self): 803e0c48bf9SAdrian Hunter # --no-per-cpu option takes precedence 804e0c48bf9SAdrian Hunter if self.no_per_cpu: 805e0c48bf9SAdrian Hunter return False 806e0c48bf9SAdrian Hunter if not self.PerfDataRecordedPerCPU(): 807e0c48bf9SAdrian Hunter return False 808e0c48bf9SAdrian Hunter # Default to per-cpu for Intel PT data that was recorded per-cpu, 809e0c48bf9SAdrian Hunter # because decoding can be done for each CPU separately. 810e0c48bf9SAdrian Hunter if self.IsIntelPT(): 811e0c48bf9SAdrian Hunter return True 812e0c48bf9SAdrian Hunter return False 813e0c48bf9SAdrian Hunter 814e0c48bf9SAdrian Hunter def Config(self): 815e0c48bf9SAdrian Hunter self.Init() 816e0c48bf9SAdrian Hunter self.ExtractTimeInfo() 817e0c48bf9SAdrian Hunter if not self.per_cpu: 818e0c48bf9SAdrian Hunter self.per_cpu = self.DefaultToPerCPU() 819e0c48bf9SAdrian Hunter if self.verbosity.debug: 820e0c48bf9SAdrian Hunter print("per_cpu", self.per_cpu) 821e0c48bf9SAdrian Hunter self.ExtractCPUInfo() 822e0c48bf9SAdrian Hunter self.SplitTimeRanges() 823e0c48bf9SAdrian Hunter if self.verbosity.self_test: 824e0c48bf9SAdrian Hunter self.CheckTimeRanges() 825e0c48bf9SAdrian Hunter # Prefer open-ended time range to starting / ending with min_time / max_time resp. 826e0c48bf9SAdrian Hunter self.OpenTimeRangeEnds() 827e0c48bf9SAdrian Hunter self.CreateWorkList() 828e0c48bf9SAdrian Hunter 829e0c48bf9SAdrian Hunter def Run(self): 830e0c48bf9SAdrian Hunter if self.dry_run: 831e0c48bf9SAdrian Hunter print(len(self.worklist),"jobs:") 832e0c48bf9SAdrian Hunter for w in self.worklist: 833e0c48bf9SAdrian Hunter print(w.Command()) 834e0c48bf9SAdrian Hunter return True 835e0c48bf9SAdrian Hunter result = RunWork(self.worklist, self.jobs, verbosity=self.verbosity) 836e0c48bf9SAdrian Hunter if self.verbosity.verbose: 837e0c48bf9SAdrian Hunter print(glb_prog_name, "done") 838e0c48bf9SAdrian Hunter return result 839e0c48bf9SAdrian Hunter 840e0c48bf9SAdrian Hunterdef RunParallelPerf(a): 841e0c48bf9SAdrian Hunter pp = ParallelPerf(a) 842e0c48bf9SAdrian Hunter pp.Config() 843e0c48bf9SAdrian Hunter return pp.Run() 844e0c48bf9SAdrian Hunter 845e0c48bf9SAdrian Hunterdef Main(args): 846e0c48bf9SAdrian Hunter ap = argparse.ArgumentParser( 847e0c48bf9SAdrian Hunter prog=glb_prog_name, formatter_class = argparse.RawDescriptionHelpFormatter, 848e0c48bf9SAdrian Hunter description = 849e0c48bf9SAdrian Hunter""" 850e0c48bf9SAdrian HunterRun a perf script command multiple times in parallel, using perf script options 851e0c48bf9SAdrian Hunter--cpu and --time so that each job processes a different chunk of the data. 852e0c48bf9SAdrian Hunter""", 853e0c48bf9SAdrian Hunter epilog = 854e0c48bf9SAdrian Hunter""" 855e0c48bf9SAdrian HunterFollow the options by '--' and then the perf script command e.g. 856e0c48bf9SAdrian Hunter 857e0c48bf9SAdrian Hunter $ perf record -a -- sleep 10 858e0c48bf9SAdrian Hunter $ parallel-perf.py --nr=4 -- perf script --ns 859e0c48bf9SAdrian Hunter All jobs finished successfully 860e0c48bf9SAdrian Hunter $ tree parallel-perf-output/ 861e0c48bf9SAdrian Hunter parallel-perf-output/ 862e0c48bf9SAdrian Hunter ├── time-range-0 863e0c48bf9SAdrian Hunter │ ├── cmd.txt 864e0c48bf9SAdrian Hunter │ └── out.txt 865e0c48bf9SAdrian Hunter ├── time-range-1 866e0c48bf9SAdrian Hunter │ ├── cmd.txt 867e0c48bf9SAdrian Hunter │ └── out.txt 868e0c48bf9SAdrian Hunter ├── time-range-2 869e0c48bf9SAdrian Hunter │ ├── cmd.txt 870e0c48bf9SAdrian Hunter │ └── out.txt 871e0c48bf9SAdrian Hunter └── time-range-3 872e0c48bf9SAdrian Hunter ├── cmd.txt 873e0c48bf9SAdrian Hunter └── out.txt 874e0c48bf9SAdrian Hunter $ find parallel-perf-output -name cmd.txt | sort | xargs grep -H . 875e0c48bf9SAdrian Hunter parallel-perf-output/time-range-0/cmd.txt:perf script --time=,9466.504461499 --ns 876e0c48bf9SAdrian Hunter parallel-perf-output/time-range-1/cmd.txt:perf script --time=9466.504461500,9469.005396999 --ns 877e0c48bf9SAdrian Hunter parallel-perf-output/time-range-2/cmd.txt:perf script --time=9469.005397000,9471.506332499 --ns 878e0c48bf9SAdrian Hunter parallel-perf-output/time-range-3/cmd.txt:perf script --time=9471.506332500, --ns 879e0c48bf9SAdrian Hunter 880e0c48bf9SAdrian HunterAny perf script command can be used, including the use of perf script options 881e0c48bf9SAdrian Hunter--dlfilter and --script, so that the benefit of running parallel jobs 882e0c48bf9SAdrian Hunternaturally extends to them also. 883e0c48bf9SAdrian Hunter 884e0c48bf9SAdrian HunterIf option --pipe-to is used, standard output is first piped through that 885e0c48bf9SAdrian Huntercommand. Beware, if the command fails (e.g. grep with no matches), it will be 886e0c48bf9SAdrian Hunterconsidered a fatal error. 887e0c48bf9SAdrian Hunter 888e0c48bf9SAdrian HunterFinal standard output is redirected to files named out.txt in separate 889e0c48bf9SAdrian Huntersubdirectories under the output directory. Similarly, standard error is 890e0c48bf9SAdrian Hunterwritten to files named err.txt. In addition, files named cmd.txt contain the 891e0c48bf9SAdrian Huntercorresponding perf script command. After processing, err.txt files are removed 892e0c48bf9SAdrian Hunterif they are empty. 893e0c48bf9SAdrian Hunter 894e0c48bf9SAdrian HunterIf any job exits with a non-zero exit code, then all jobs are killed and no 895e0c48bf9SAdrian Huntermore are started. A message is printed if any job results in a non-empty 896e0c48bf9SAdrian Huntererr.txt file. 897e0c48bf9SAdrian Hunter 898e0c48bf9SAdrian HunterThere is a separate output subdirectory for each time range. If the --per-cpu 899e0c48bf9SAdrian Hunteroption is used, these are further grouped under cpu-n subdirectories, e.g. 900e0c48bf9SAdrian Hunter 901e0c48bf9SAdrian Hunter $ parallel-perf.py --per-cpu --nr=2 -- perf script --ns --cpu=0,1 902e0c48bf9SAdrian Hunter All jobs finished successfully 903e0c48bf9SAdrian Hunter $ tree parallel-perf-output 904e0c48bf9SAdrian Hunter parallel-perf-output/ 905e0c48bf9SAdrian Hunter ├── cpu-0 906e0c48bf9SAdrian Hunter │ ├── time-range-0 907e0c48bf9SAdrian Hunter │ │ ├── cmd.txt 908e0c48bf9SAdrian Hunter │ │ └── out.txt 909e0c48bf9SAdrian Hunter │ └── time-range-1 910e0c48bf9SAdrian Hunter │ ├── cmd.txt 911e0c48bf9SAdrian Hunter │ └── out.txt 912e0c48bf9SAdrian Hunter └── cpu-1 913e0c48bf9SAdrian Hunter ├── time-range-0 914e0c48bf9SAdrian Hunter │ ├── cmd.txt 915e0c48bf9SAdrian Hunter │ └── out.txt 916e0c48bf9SAdrian Hunter └── time-range-1 917e0c48bf9SAdrian Hunter ├── cmd.txt 918e0c48bf9SAdrian Hunter └── out.txt 919e0c48bf9SAdrian Hunter $ find parallel-perf-output -name cmd.txt | sort | xargs grep -H . 920e0c48bf9SAdrian Hunter parallel-perf-output/cpu-0/time-range-0/cmd.txt:perf script --cpu=0 --time=,9469.005396999 --ns 921e0c48bf9SAdrian Hunter parallel-perf-output/cpu-0/time-range-1/cmd.txt:perf script --cpu=0 --time=9469.005397000, --ns 922e0c48bf9SAdrian Hunter parallel-perf-output/cpu-1/time-range-0/cmd.txt:perf script --cpu=1 --time=,9469.005396999 --ns 923e0c48bf9SAdrian Hunter parallel-perf-output/cpu-1/time-range-1/cmd.txt:perf script --cpu=1 --time=9469.005397000, --ns 924e0c48bf9SAdrian Hunter 925e0c48bf9SAdrian HunterSubdivisions of time range, and cpus if the --per-cpu option is used, are 926e0c48bf9SAdrian Hunterexpressed by the --time and --cpu perf script options respectively. If the 927e0c48bf9SAdrian Huntersupplied perf script command has a --time option, then that time range is 928e0c48bf9SAdrian Huntersubdivided, otherwise the time range given by 'time of first sample' to 929e0c48bf9SAdrian Hunter'time of last sample' is used (refer perf script --header-only). Similarly, the 930e0c48bf9SAdrian Huntersupplied perf script command may provide a --cpu option, and only those CPUs 931e0c48bf9SAdrian Hunterwill be processed. 932e0c48bf9SAdrian Hunter 933e0c48bf9SAdrian HunterTo prevent time intervals becoming too small, the --min-interval option can 934e0c48bf9SAdrian Hunterbe used. 935e0c48bf9SAdrian Hunter 936e0c48bf9SAdrian HunterNote there is special handling for processing Intel PT traces. If an interval is 937e0c48bf9SAdrian Hunternot specified and the perf record command contained the intel_pt event, then the 938e0c48bf9SAdrian Huntertime range will be subdivided in order to produce subdivisions that contain 939e0c48bf9SAdrian Hunterapproximately the same amount of trace data. That is accomplished by counting 940e0c48bf9SAdrian Hunterdouble-quick (--itrace=qqi) samples, and choosing time ranges that encompass 941e0c48bf9SAdrian Hunterapproximately the same number of samples. In that case, time ranges may not be 942e0c48bf9SAdrian Hunterthe same for each CPU processed. For Intel PT, --per-cpu is the default, but 943e0c48bf9SAdrian Hunterthat can be overridden by --no-per-cpu. Note, for Intel PT, double-quick 944e0c48bf9SAdrian Hunterdecoding produces 1 sample for each PSB synchronization packet, which in turn 945e0c48bf9SAdrian Huntercome after a certain number of bytes output, determined by psb_period (refer 946e0c48bf9SAdrian Hunterperf Intel PT documentation). The minimum number of double-quick samples that 947e0c48bf9SAdrian Hunterwill define a time range can be set by the --min_size option, which defaults to 948e0c48bf9SAdrian Hunter64. 949e0c48bf9SAdrian Hunter""") 950e0c48bf9SAdrian Hunter ap.add_argument("-o", "--output-dir", default="parallel-perf-output", help="output directory (default 'parallel-perf-output')") 951e0c48bf9SAdrian Hunter ap.add_argument("-j", "--jobs", type=int, default=0, help="maximum number of jobs to run in parallel at one time (default is the number of CPUs)") 952e0c48bf9SAdrian Hunter ap.add_argument("-n", "--nr", type=int, default=0, help="number of time subdivisions (default is the number of jobs)") 953e0c48bf9SAdrian Hunter ap.add_argument("-i", "--interval", type=float, default=0, help="subdivide the time range using this time interval (in seconds e.g. 0.1 for a tenth of a second)") 954e0c48bf9SAdrian Hunter ap.add_argument("-c", "--per-cpu", action="store_true", help="process data for each CPU in parallel") 955e0c48bf9SAdrian Hunter ap.add_argument("-m", "--min-interval", type=float, default=glb_min_interval, help=f"minimum interval (default {glb_min_interval} seconds)") 956e0c48bf9SAdrian Hunter ap.add_argument("-p", "--pipe-to", help="command to pipe output to (optional)") 957e0c48bf9SAdrian Hunter ap.add_argument("-N", "--no-per-cpu", action="store_true", help="do not process data for each CPU in parallel") 958e0c48bf9SAdrian Hunter ap.add_argument("-b", "--min_size", type=int, default=glb_min_samples, help="minimum data size (for Intel PT in PSBs)") 959e0c48bf9SAdrian Hunter ap.add_argument("-D", "--dry-run", action="store_true", help="do not run any jobs, just show the perf script commands") 960e0c48bf9SAdrian Hunter ap.add_argument("-q", "--quiet", action="store_true", help="do not print any messages except errors") 961e0c48bf9SAdrian Hunter ap.add_argument("-v", "--verbose", action="store_true", help="print more messages") 962e0c48bf9SAdrian Hunter ap.add_argument("-d", "--debug", action="store_true", help="print debugging messages") 963e0c48bf9SAdrian Hunter cmd_line = list(args) 964e0c48bf9SAdrian Hunter try: 965e0c48bf9SAdrian Hunter split_pos = cmd_line.index("--") 966e0c48bf9SAdrian Hunter cmd = cmd_line[split_pos + 1:] 967e0c48bf9SAdrian Hunter args = cmd_line[:split_pos] 968e0c48bf9SAdrian Hunter except: 969e0c48bf9SAdrian Hunter cmd = None 970e0c48bf9SAdrian Hunter args = cmd_line 971e0c48bf9SAdrian Hunter a = ap.parse_args(args=args[1:]) 972e0c48bf9SAdrian Hunter a.cmd = cmd 973e0c48bf9SAdrian Hunter a.verbosity = Verbosity(a.quiet, a.verbose, a.debug) 974e0c48bf9SAdrian Hunter try: 975e0c48bf9SAdrian Hunter if a.cmd == None: 976e0c48bf9SAdrian Hunter if len(args) <= 1: 977e0c48bf9SAdrian Hunter ap.print_help() 978e0c48bf9SAdrian Hunter return True 979e0c48bf9SAdrian Hunter raise Exception("Command line must contain '--' before perf command") 980e0c48bf9SAdrian Hunter return RunParallelPerf(a) 981e0c48bf9SAdrian Hunter except Exception as e: 982e0c48bf9SAdrian Hunter print("Fatal error: ", str(e)) 983e0c48bf9SAdrian Hunter if a.debug: 984e0c48bf9SAdrian Hunter raise 985e0c48bf9SAdrian Hunter return False 986e0c48bf9SAdrian Hunter 987e0c48bf9SAdrian Hunterif __name__ == "__main__": 988e0c48bf9SAdrian Hunter if not Main(sys.argv): 989e0c48bf9SAdrian Hunter sys.exit(1) 990