| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989 |
- #!/usr/bin/env python3
- # SPDX-License-Identifier: GPL-2.0
- #
- # Run a perf script command multiple times in parallel, using perf script
- # options --cpu and --time so that each job processes a different chunk
- # of the data.
- #
- # Copyright (c) 2024, Intel Corporation.
- import subprocess
- import argparse
- import pathlib
- import shlex
- import time
- import copy
- import sys
- import os
- import re
- glb_prog_name = "parallel-perf.py"
- glb_min_interval = 10.0
- glb_min_samples = 64
- class Verbosity():
- def __init__(self, quiet=False, verbose=False, debug=False):
- self.normal = True
- self.verbose = verbose
- self.debug = debug
- self.self_test = True
- if self.debug:
- self.verbose = True
- if self.verbose:
- quiet = False
- if quiet:
- self.normal = False
- # Manage work (Start/Wait/Kill), as represented by a subprocess.Popen command
- class Work():
- def __init__(self, cmd, pipe_to, output_dir="."):
- self.popen = None
- self.consumer = None
- self.cmd = cmd
- self.pipe_to = pipe_to
- self.output_dir = output_dir
- self.cmdout_name = f"{output_dir}/cmd.txt"
- self.stdout_name = f"{output_dir}/out.txt"
- self.stderr_name = f"{output_dir}/err.txt"
- def Command(self):
- sh_cmd = [ shlex.quote(x) for x in self.cmd ]
- return " ".join(self.cmd)
- def Stdout(self):
- return open(self.stdout_name, "w")
- def Stderr(self):
- return open(self.stderr_name, "w")
- def CreateOutputDir(self):
- pathlib.Path(self.output_dir).mkdir(parents=True, exist_ok=True)
- def Start(self):
- if self.popen:
- return
- self.CreateOutputDir()
- with open(self.cmdout_name, "w") as f:
- f.write(self.Command())
- f.write("\n")
- stdout = self.Stdout()
- stderr = self.Stderr()
- if self.pipe_to:
- self.popen = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=stderr)
- args = shlex.split(self.pipe_to)
- self.consumer = subprocess.Popen(args, stdin=self.popen.stdout, stdout=stdout, stderr=stderr)
- else:
- self.popen = subprocess.Popen(self.cmd, stdout=stdout, stderr=stderr)
- def RemoveEmptyErrFile(self):
- if os.path.exists(self.stderr_name):
- if os.path.getsize(self.stderr_name) == 0:
- os.unlink(self.stderr_name)
- def Errors(self):
- if os.path.exists(self.stderr_name):
- if os.path.getsize(self.stderr_name) != 0:
- return [ f"Non-empty error file {self.stderr_name}" ]
- return []
- def TidyUp(self):
- self.RemoveEmptyErrFile()
- def RawPollWait(self, p, wait):
- if wait:
- return p.wait()
- return p.poll()
- def Poll(self, wait=False):
- if not self.popen:
- return None
- result = self.RawPollWait(self.popen, wait)
- if self.consumer:
- res = result
- result = self.RawPollWait(self.consumer, wait)
- if result != None and res == None:
- self.popen.kill()
- result = None
- elif result == 0 and res != None and res != 0:
- result = res
- if result != None:
- self.TidyUp()
- return result
- def Wait(self):
- return self.Poll(wait=True)
- def Kill(self):
- if not self.popen:
- return
- self.popen.kill()
- if self.consumer:
- self.consumer.kill()
- def KillWork(worklist, verbosity):
- for w in worklist:
- w.Kill()
- for w in worklist:
- w.Wait()
- def NumberOfCPUs():
- return os.sysconf("SC_NPROCESSORS_ONLN")
- def NanoSecsToSecsStr(x):
- if x == None:
- return ""
- x = str(x)
- if len(x) < 10:
- x = "0" * (10 - len(x)) + x
- return x[:len(x) - 9] + "." + x[-9:]
- def InsertOptionAfter(cmd, option, after):
- try:
- pos = cmd.index(after)
- cmd.insert(pos + 1, option)
- except:
- cmd.append(option)
- def CreateWorkList(cmd, pipe_to, output_dir, cpus, time_ranges_by_cpu):
- max_len = len(str(cpus[-1]))
- cpu_dir_fmt = f"cpu-%.{max_len}u"
- worklist = []
- pos = 0
- for cpu in cpus:
- if cpu >= 0:
- cpu_dir = os.path.join(output_dir, cpu_dir_fmt % cpu)
- cpu_option = f"--cpu={cpu}"
- else:
- cpu_dir = output_dir
- cpu_option = None
- tr_dir_fmt = "time-range"
- if len(time_ranges_by_cpu) > 1:
- time_ranges = time_ranges_by_cpu[pos]
- tr_dir_fmt += f"-{pos}"
- pos += 1
- else:
- time_ranges = time_ranges_by_cpu[0]
- max_len = len(str(len(time_ranges)))
- tr_dir_fmt += f"-%.{max_len}u"
- i = 0
- for r in time_ranges:
- if r == [None, None]:
- time_option = None
- work_output_dir = cpu_dir
- else:
- time_option = "--time=" + NanoSecsToSecsStr(r[0]) + "," + NanoSecsToSecsStr(r[1])
- work_output_dir = os.path.join(cpu_dir, tr_dir_fmt % i)
- i += 1
- work_cmd = list(cmd)
- if time_option != None:
- InsertOptionAfter(work_cmd, time_option, "script")
- if cpu_option != None:
- InsertOptionAfter(work_cmd, cpu_option, "script")
- w = Work(work_cmd, pipe_to, work_output_dir)
- worklist.append(w)
- return worklist
- def DoRunWork(worklist, nr_jobs, verbosity):
- nr_to_do = len(worklist)
- not_started = list(worklist)
- running = []
- done = []
- chg = False
- while True:
- nr_done = len(done)
- if chg and verbosity.normal:
- nr_run = len(running)
- print(f"\rThere are {nr_to_do} jobs: {nr_done} completed, {nr_run} running", flush=True, end=" ")
- if verbosity.verbose:
- print()
- chg = False
- if nr_done == nr_to_do:
- break
- while len(running) < nr_jobs and len(not_started):
- w = not_started.pop(0)
- running.append(w)
- if verbosity.verbose:
- print("Starting:", w.Command())
- w.Start()
- chg = True
- if len(running):
- time.sleep(0.1)
- finished = []
- not_finished = []
- while len(running):
- w = running.pop(0)
- r = w.Poll()
- if r == None:
- not_finished.append(w)
- continue
- if r == 0:
- if verbosity.verbose:
- print("Finished:", w.Command())
- finished.append(w)
- chg = True
- continue
- if verbosity.normal and not verbosity.verbose:
- print()
- print("Job failed!\n return code:", r, "\n command: ", w.Command())
- if w.pipe_to:
- print(" piped to: ", w.pipe_to)
- print("Killing outstanding jobs")
- KillWork(not_finished, verbosity)
- KillWork(running, verbosity)
- return False
- running = not_finished
- done += finished
- errorlist = []
- for w in worklist:
- errorlist += w.Errors()
- if len(errorlist):
- print("Errors:")
- for e in errorlist:
- print(e)
- elif verbosity.normal:
- print("\r"," "*50, "\rAll jobs finished successfully", flush=True)
- return True
- def RunWork(worklist, nr_jobs=NumberOfCPUs(), verbosity=Verbosity()):
- try:
- return DoRunWork(worklist, nr_jobs, verbosity)
- except:
- for w in worklist:
- w.Kill()
- raise
- return True
- def ReadHeader(perf, file_name):
- return subprocess.Popen([perf, "script", "--header-only", "--input", file_name], stdout=subprocess.PIPE).stdout.read().decode("utf-8")
- def ParseHeader(hdr):
- result = {}
- lines = hdr.split("\n")
- for line in lines:
- if ":" in line and line[0] == "#":
- pos = line.index(":")
- name = line[1:pos-1].strip()
- value = line[pos+1:].strip()
- if name in result:
- orig_name = name
- nr = 2
- while True:
- name = f"{orig_name} {nr}"
- if name not in result:
- break
- nr += 1
- result[name] = value
- return result
- def HeaderField(hdr_dict, hdr_fld):
- if hdr_fld not in hdr_dict:
- raise Exception(f"'{hdr_fld}' missing from header information")
- return hdr_dict[hdr_fld]
- # Represent the position of an option within a command string
- # and provide the option value and/or remove the option
- class OptPos():
- def Init(self, opt_element=-1, value_element=-1, opt_pos=-1, value_pos=-1, error=None):
- self.opt_element = opt_element # list element that contains option
- self.value_element = value_element # list element that contains option value
- self.opt_pos = opt_pos # string position of option
- self.value_pos = value_pos # string position of value
- self.error = error # error message string
- def __init__(self, args, short_name, long_name, default=None):
- self.args = list(args)
- self.default = default
- n = 2 + len(long_name)
- m = len(short_name)
- pos = -1
- for opt in args:
- pos += 1
- if m and opt[:2] == f"-{short_name}":
- if len(opt) == 2:
- if pos + 1 < len(args):
- self.Init(pos, pos + 1, 0, 0)
- else:
- self.Init(error = f"-{short_name} option missing value")
- else:
- self.Init(pos, pos, 0, 2)
- return
- if opt[:n] == f"--{long_name}":
- if len(opt) == n:
- if pos + 1 < len(args):
- self.Init(pos, pos + 1, 0, 0)
- else:
- self.Init(error = f"--{long_name} option missing value")
- elif opt[n] == "=":
- self.Init(pos, pos, 0, n + 1)
- else:
- self.Init(error = f"--{long_name} option expected '='")
- return
- if m and opt[:1] == "-" and opt[:2] != "--" and short_name in opt:
- ipos = opt.index(short_name)
- if "-" in opt[1:]:
- hpos = opt[1:].index("-")
- if hpos < ipos:
- continue
- if ipos + 1 == len(opt):
- if pos + 1 < len(args):
- self.Init(pos, pos + 1, ipos, 0)
- else:
- self.Init(error = f"-{short_name} option missing value")
- else:
- self.Init(pos, pos, ipos, ipos + 1)
- return
- self.Init()
- def Value(self):
- if self.opt_element >= 0:
- if self.opt_element != self.value_element:
- return self.args[self.value_element]
- else:
- return self.args[self.value_element][self.value_pos:]
- return self.default
- def Remove(self, args):
- if self.opt_element == -1:
- return
- if self.opt_element != self.value_element:
- del args[self.value_element]
- if self.opt_pos:
- args[self.opt_element] = args[self.opt_element][:self.opt_pos]
- else:
- del args[self.opt_element]
- def DetermineInputFileName(cmd):
- p = OptPos(cmd, "i", "input", "perf.data")
- if p.error:
- raise Exception(f"perf command {p.error}")
- file_name = p.Value()
- if not os.path.exists(file_name):
- raise Exception(f"perf command input file '{file_name}' not found")
- return file_name
- def ReadOption(args, short_name, long_name, err_prefix, remove=False):
- p = OptPos(args, short_name, long_name)
- if p.error:
- raise Exception(f"{err_prefix}{p.error}")
- value = p.Value()
- if remove:
- p.Remove(args)
- return value
- def ExtractOption(args, short_name, long_name, err_prefix):
- return ReadOption(args, short_name, long_name, err_prefix, True)
- def ReadPerfOption(args, short_name, long_name):
- return ReadOption(args, short_name, long_name, "perf command ")
- def ExtractPerfOption(args, short_name, long_name):
- return ExtractOption(args, short_name, long_name, "perf command ")
- def PerfDoubleQuickCommands(cmd, file_name):
- cpu_str = ReadPerfOption(cmd, "C", "cpu")
- time_str = ReadPerfOption(cmd, "", "time")
- # Use double-quick sampling to determine trace data density
- times_cmd = ["perf", "script", "--ns", "--input", file_name, "--itrace=qqi"]
- if cpu_str != None and cpu_str != "":
- times_cmd.append(f"--cpu={cpu_str}")
- if time_str != None and time_str != "":
- times_cmd.append(f"--time={time_str}")
- cnts_cmd = list(times_cmd)
- cnts_cmd.append("-Fcpu")
- times_cmd.append("-Fcpu,time")
- return cnts_cmd, times_cmd
- class CPUTimeRange():
- def __init__(self, cpu):
- self.cpu = cpu
- self.sample_cnt = 0
- self.time_ranges = None
- self.interval = 0
- self.interval_remaining = 0
- self.remaining = 0
- self.tr_pos = 0
- def CalcTimeRangesByCPU(line, cpu, cpu_time_ranges, max_time):
- cpu_time_range = cpu_time_ranges[cpu]
- cpu_time_range.remaining -= 1
- cpu_time_range.interval_remaining -= 1
- if cpu_time_range.remaining == 0:
- cpu_time_range.time_ranges[cpu_time_range.tr_pos][1] = max_time
- return
- if cpu_time_range.interval_remaining == 0:
- time = TimeVal(line[1][:-1], 0)
- time_ranges = cpu_time_range.time_ranges
- time_ranges[cpu_time_range.tr_pos][1] = time - 1
- time_ranges.append([time, max_time])
- cpu_time_range.tr_pos += 1
- cpu_time_range.interval_remaining = cpu_time_range.interval
- def CountSamplesByCPU(line, cpu, cpu_time_ranges):
- try:
- cpu_time_ranges[cpu].sample_cnt += 1
- except:
- print("exception")
- print("cpu", cpu)
- print("len(cpu_time_ranges)", len(cpu_time_ranges))
- raise
- def ProcessCommandOutputLines(cmd, per_cpu, fn, *x):
- # Assume CPU number is at beginning of line and enclosed by []
- pat = re.compile(r"\s*\[[0-9]+\]")
- p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
- while True:
- line = p.stdout.readline()
- if line:
- line = line.decode("utf-8")
- if pat.match(line):
- line = line.split()
- if per_cpu:
- # Assumes CPU number is enclosed by []
- cpu = int(line[0][1:-1])
- else:
- cpu = 0
- fn(line, cpu, *x)
- else:
- break
- p.wait()
- def IntersectTimeRanges(new_time_ranges, time_ranges):
- pos = 0
- new_pos = 0
- # Can assume len(time_ranges) != 0 and len(new_time_ranges) != 0
- # Note also, there *must* be at least one intersection.
- while pos < len(time_ranges) and new_pos < len(new_time_ranges):
- # new end < old start => no intersection, remove new
- if new_time_ranges[new_pos][1] < time_ranges[pos][0]:
- del new_time_ranges[new_pos]
- continue
- # new start > old end => no intersection, check next
- if new_time_ranges[new_pos][0] > time_ranges[pos][1]:
- pos += 1
- if pos < len(time_ranges):
- continue
- # no next, so remove remaining
- while new_pos < len(new_time_ranges):
- del new_time_ranges[new_pos]
- return
- # Found an intersection
- # new start < old start => adjust new start = old start
- if new_time_ranges[new_pos][0] < time_ranges[pos][0]:
- new_time_ranges[new_pos][0] = time_ranges[pos][0]
- # new end > old end => keep the overlap, insert the remainder
- if new_time_ranges[new_pos][1] > time_ranges[pos][1]:
- r = [ time_ranges[pos][1] + 1, new_time_ranges[new_pos][1] ]
- new_time_ranges[new_pos][1] = time_ranges[pos][1]
- new_pos += 1
- new_time_ranges.insert(new_pos, r)
- continue
- # new [start, end] is within old [start, end]
- new_pos += 1
- def SplitTimeRangesByTraceDataDensity(time_ranges, cpus, nr, cmd, file_name, per_cpu, min_size, min_interval, verbosity):
- if verbosity.normal:
- print("\rAnalyzing...", flush=True, end=" ")
- if verbosity.verbose:
- print()
- cnts_cmd, times_cmd = PerfDoubleQuickCommands(cmd, file_name)
- nr_cpus = cpus[-1] + 1 if per_cpu else 1
- if per_cpu:
- nr_cpus = cpus[-1] + 1
- cpu_time_ranges = [ CPUTimeRange(cpu) for cpu in range(nr_cpus) ]
- else:
- nr_cpus = 1
- cpu_time_ranges = [ CPUTimeRange(-1) ]
- if verbosity.debug:
- print("nr_cpus", nr_cpus)
- print("cnts_cmd", cnts_cmd)
- print("times_cmd", times_cmd)
- # Count the number of "double quick" samples per CPU
- ProcessCommandOutputLines(cnts_cmd, per_cpu, CountSamplesByCPU, cpu_time_ranges)
- tot = 0
- mx = 0
- for cpu_time_range in cpu_time_ranges:
- cnt = cpu_time_range.sample_cnt
- tot += cnt
- if cnt > mx:
- mx = cnt
- if verbosity.debug:
- print("cpu:", cpu_time_range.cpu, "sample_cnt", cnt)
- if min_size < 1:
- min_size = 1
- if mx < min_size:
- # Too little data to be worth splitting
- if verbosity.debug:
- print("Too little data to split by time")
- if nr == 0:
- nr = 1
- return [ SplitTimeRangesIntoN(time_ranges, nr, min_interval) ]
- if nr:
- divisor = nr
- min_size = 1
- else:
- divisor = NumberOfCPUs()
- interval = int(round(tot / divisor, 0))
- if interval < min_size:
- interval = min_size
- if verbosity.debug:
- print("divisor", divisor)
- print("min_size", min_size)
- print("interval", interval)
- min_time = time_ranges[0][0]
- max_time = time_ranges[-1][1]
- for cpu_time_range in cpu_time_ranges:
- cnt = cpu_time_range.sample_cnt
- if cnt == 0:
- cpu_time_range.time_ranges = copy.deepcopy(time_ranges)
- continue
- # Adjust target interval for CPU to give approximately equal interval sizes
- # Determine number of intervals, rounding to nearest integer
- n = int(round(cnt / interval, 0))
- if n < 1:
- n = 1
- # Determine interval size, rounding up
- d, m = divmod(cnt, n)
- if m:
- d += 1
- cpu_time_range.interval = d
- cpu_time_range.interval_remaining = d
- cpu_time_range.remaining = cnt
- # Init. time ranges for each CPU with the start time
- cpu_time_range.time_ranges = [ [min_time, max_time] ]
- # Set time ranges so that the same number of "double quick" samples
- # will fall into each time range.
- ProcessCommandOutputLines(times_cmd, per_cpu, CalcTimeRangesByCPU, cpu_time_ranges, max_time)
- for cpu_time_range in cpu_time_ranges:
- if cpu_time_range.sample_cnt:
- IntersectTimeRanges(cpu_time_range.time_ranges, time_ranges)
- return [cpu_time_ranges[cpu].time_ranges for cpu in cpus]
- def SplitSingleTimeRangeIntoN(time_range, n):
- if n <= 1:
- return [time_range]
- start = time_range[0]
- end = time_range[1]
- duration = int((end - start + 1) / n)
- if duration < 1:
- return [time_range]
- time_ranges = []
- for i in range(n):
- time_ranges.append([start, start + duration - 1])
- start += duration
- time_ranges[-1][1] = end
- return time_ranges
- def TimeRangeDuration(r):
- return r[1] - r[0] + 1
- def TotalDuration(time_ranges):
- duration = 0
- for r in time_ranges:
- duration += TimeRangeDuration(r)
- return duration
- def SplitTimeRangesByInterval(time_ranges, interval):
- new_ranges = []
- for r in time_ranges:
- duration = TimeRangeDuration(r)
- n = duration / interval
- n = int(round(n, 0))
- new_ranges += SplitSingleTimeRangeIntoN(r, n)
- return new_ranges
- def SplitTimeRangesIntoN(time_ranges, n, min_interval):
- if n <= len(time_ranges):
- return time_ranges
- duration = TotalDuration(time_ranges)
- interval = duration / n
- if interval < min_interval:
- interval = min_interval
- return SplitTimeRangesByInterval(time_ranges, interval)
- def RecombineTimeRanges(tr):
- new_tr = copy.deepcopy(tr)
- n = len(new_tr)
- i = 1
- while i < len(new_tr):
- # if prev end + 1 == cur start, combine them
- if new_tr[i - 1][1] + 1 == new_tr[i][0]:
- new_tr[i][0] = new_tr[i - 1][0]
- del new_tr[i - 1]
- else:
- i += 1
- return new_tr
- def OpenTimeRangeEnds(time_ranges, min_time, max_time):
- if time_ranges[0][0] <= min_time:
- time_ranges[0][0] = None
- if time_ranges[-1][1] >= max_time:
- time_ranges[-1][1] = None
- def BadTimeStr(time_str):
- 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")
- def ValidateTimeRanges(time_ranges, time_str):
- n = len(time_ranges)
- for i in range(n):
- start = time_ranges[i][0]
- end = time_ranges[i][1]
- if i != 0 and start <= time_ranges[i - 1][1]:
- BadTimeStr(time_str)
- if start > end:
- BadTimeStr(time_str)
- def TimeVal(s, dflt):
- s = s.strip()
- if s == "":
- return dflt
- a = s.split(".")
- if len(a) > 2:
- raise Exception(f"Bad time value'{s}'")
- x = int(a[0])
- if x < 0:
- raise Exception("Negative time not allowed")
- x *= 1000000000
- if len(a) > 1:
- x += int((a[1] + "000000000")[:9])
- return x
- def BadCPUStr(cpu_str):
- raise Exception(f"perf command bad cpu option: '{cpu_str}'\nCheck also 'nrcpus avail' in perf script --header-only")
- def ParseTimeStr(time_str, min_time, max_time):
- if time_str == None or time_str == "":
- return [[min_time, max_time]]
- time_ranges = []
- for r in time_str.split():
- a = r.split(",")
- if len(a) != 2:
- BadTimeStr(time_str)
- try:
- start = TimeVal(a[0], min_time)
- end = TimeVal(a[1], max_time)
- except:
- BadTimeStr(time_str)
- time_ranges.append([start, end])
- ValidateTimeRanges(time_ranges, time_str)
- return time_ranges
- def ParseCPUStr(cpu_str, nr_cpus):
- if cpu_str == None or cpu_str == "":
- return [-1]
- cpus = []
- for r in cpu_str.split(","):
- a = r.split("-")
- if len(a) < 1 or len(a) > 2:
- BadCPUStr(cpu_str)
- try:
- start = int(a[0].strip())
- if len(a) > 1:
- end = int(a[1].strip())
- else:
- end = start
- except:
- BadCPUStr(cpu_str)
- if start < 0 or end < 0 or end < start or end >= nr_cpus:
- BadCPUStr(cpu_str)
- cpus.extend(range(start, end + 1))
- cpus = list(set(cpus)) # Remove duplicates
- cpus.sort()
- return cpus
- class ParallelPerf():
- def __init__(self, a):
- for arg_name in vars(a):
- setattr(self, arg_name, getattr(a, arg_name))
- self.orig_nr = self.nr
- self.orig_cmd = list(self.cmd)
- self.perf = self.cmd[0]
- if os.path.exists(self.output_dir):
- raise Exception(f"Output '{self.output_dir}' already exists")
- if self.jobs < 0 or self.nr < 0 or self.interval < 0:
- raise Exception("Bad options (negative values): try -h option for help")
- if self.nr != 0 and self.interval != 0:
- raise Exception("Cannot specify number of time subdivisions and time interval")
- if self.jobs == 0:
- self.jobs = NumberOfCPUs()
- if self.nr == 0 and self.interval == 0:
- if self.per_cpu:
- self.nr = 1
- else:
- self.nr = self.jobs
- def Init(self):
- if self.verbosity.debug:
- print("cmd", self.cmd)
- self.file_name = DetermineInputFileName(self.cmd)
- self.hdr = ReadHeader(self.perf, self.file_name)
- self.hdr_dict = ParseHeader(self.hdr)
- self.cmd_line = HeaderField(self.hdr_dict, "cmdline")
- def ExtractTimeInfo(self):
- self.min_time = TimeVal(HeaderField(self.hdr_dict, "time of first sample"), 0)
- self.max_time = TimeVal(HeaderField(self.hdr_dict, "time of last sample"), 0)
- self.time_str = ExtractPerfOption(self.cmd, "", "time")
- self.time_ranges = ParseTimeStr(self.time_str, self.min_time, self.max_time)
- if self.verbosity.debug:
- print("time_ranges", self.time_ranges)
- def ExtractCPUInfo(self):
- if self.per_cpu:
- nr_cpus = int(HeaderField(self.hdr_dict, "nrcpus avail"))
- self.cpu_str = ExtractPerfOption(self.cmd, "C", "cpu")
- if self.cpu_str == None or self.cpu_str == "":
- self.cpus = [ x for x in range(nr_cpus) ]
- else:
- self.cpus = ParseCPUStr(self.cpu_str, nr_cpus)
- else:
- self.cpu_str = None
- self.cpus = [-1]
- if self.verbosity.debug:
- print("cpus", self.cpus)
- def IsIntelPT(self):
- return self.cmd_line.find("intel_pt") >= 0
- def SplitTimeRanges(self):
- if self.IsIntelPT() and self.interval == 0:
- self.split_time_ranges_for_each_cpu = \
- SplitTimeRangesByTraceDataDensity(self.time_ranges, self.cpus, self.orig_nr,
- self.orig_cmd, self.file_name, self.per_cpu,
- self.min_size, self.min_interval, self.verbosity)
- elif self.nr:
- self.split_time_ranges_for_each_cpu = [ SplitTimeRangesIntoN(self.time_ranges, self.nr, self.min_interval) ]
- else:
- self.split_time_ranges_for_each_cpu = [ SplitTimeRangesByInterval(self.time_ranges, self.interval) ]
- def CheckTimeRanges(self):
- for tr in self.split_time_ranges_for_each_cpu:
- # Re-combined time ranges should be the same
- new_tr = RecombineTimeRanges(tr)
- if new_tr != self.time_ranges:
- if self.verbosity.debug:
- print("tr", tr)
- print("new_tr", new_tr)
- raise Exception("Self test failed!")
- def OpenTimeRangeEnds(self):
- for time_ranges in self.split_time_ranges_for_each_cpu:
- OpenTimeRangeEnds(time_ranges, self.min_time, self.max_time)
- def CreateWorkList(self):
- self.worklist = CreateWorkList(self.cmd, self.pipe_to, self.output_dir, self.cpus, self.split_time_ranges_for_each_cpu)
- def PerfDataRecordedPerCPU(self):
- if "--per-thread" in self.cmd_line.split():
- return False
- return True
- def DefaultToPerCPU(self):
- # --no-per-cpu option takes precedence
- if self.no_per_cpu:
- return False
- if not self.PerfDataRecordedPerCPU():
- return False
- # Default to per-cpu for Intel PT data that was recorded per-cpu,
- # because decoding can be done for each CPU separately.
- if self.IsIntelPT():
- return True
- return False
- def Config(self):
- self.Init()
- self.ExtractTimeInfo()
- if not self.per_cpu:
- self.per_cpu = self.DefaultToPerCPU()
- if self.verbosity.debug:
- print("per_cpu", self.per_cpu)
- self.ExtractCPUInfo()
- self.SplitTimeRanges()
- if self.verbosity.self_test:
- self.CheckTimeRanges()
- # Prefer open-ended time range to starting / ending with min_time / max_time resp.
- self.OpenTimeRangeEnds()
- self.CreateWorkList()
- def Run(self):
- if self.dry_run:
- print(len(self.worklist),"jobs:")
- for w in self.worklist:
- print(w.Command())
- return True
- result = RunWork(self.worklist, self.jobs, verbosity=self.verbosity)
- if self.verbosity.verbose:
- print(glb_prog_name, "done")
- return result
- def RunParallelPerf(a):
- pp = ParallelPerf(a)
- pp.Config()
- return pp.Run()
- def Main(args):
- ap = argparse.ArgumentParser(
- prog=glb_prog_name, formatter_class = argparse.RawDescriptionHelpFormatter,
- description =
- """
- Run a perf script command multiple times in parallel, using perf script options
- --cpu and --time so that each job processes a different chunk of the data.
- """,
- epilog =
- """
- Follow the options by '--' and then the perf script command e.g.
- $ perf record -a -- sleep 10
- $ parallel-perf.py --nr=4 -- perf script --ns
- All jobs finished successfully
- $ tree parallel-perf-output/
- parallel-perf-output/
- ├── time-range-0
- │ ├── cmd.txt
- │ └── out.txt
- ├── time-range-1
- │ ├── cmd.txt
- │ └── out.txt
- ├── time-range-2
- │ ├── cmd.txt
- │ └── out.txt
- └── time-range-3
- ├── cmd.txt
- └── out.txt
- $ find parallel-perf-output -name cmd.txt | sort | xargs grep -H .
- parallel-perf-output/time-range-0/cmd.txt:perf script --time=,9466.504461499 --ns
- parallel-perf-output/time-range-1/cmd.txt:perf script --time=9466.504461500,9469.005396999 --ns
- parallel-perf-output/time-range-2/cmd.txt:perf script --time=9469.005397000,9471.506332499 --ns
- parallel-perf-output/time-range-3/cmd.txt:perf script --time=9471.506332500, --ns
- Any perf script command can be used, including the use of perf script options
- --dlfilter and --script, so that the benefit of running parallel jobs
- naturally extends to them also.
- If option --pipe-to is used, standard output is first piped through that
- command. Beware, if the command fails (e.g. grep with no matches), it will be
- considered a fatal error.
- Final standard output is redirected to files named out.txt in separate
- subdirectories under the output directory. Similarly, standard error is
- written to files named err.txt. In addition, files named cmd.txt contain the
- corresponding perf script command. After processing, err.txt files are removed
- if they are empty.
- If any job exits with a non-zero exit code, then all jobs are killed and no
- more are started. A message is printed if any job results in a non-empty
- err.txt file.
- There is a separate output subdirectory for each time range. If the --per-cpu
- option is used, these are further grouped under cpu-n subdirectories, e.g.
- $ parallel-perf.py --per-cpu --nr=2 -- perf script --ns --cpu=0,1
- All jobs finished successfully
- $ tree parallel-perf-output
- parallel-perf-output/
- ├── cpu-0
- │ ├── time-range-0
- │ │ ├── cmd.txt
- │ │ └── out.txt
- │ └── time-range-1
- │ ├── cmd.txt
- │ └── out.txt
- └── cpu-1
- ├── time-range-0
- │ ├── cmd.txt
- │ └── out.txt
- └── time-range-1
- ├── cmd.txt
- └── out.txt
- $ find parallel-perf-output -name cmd.txt | sort | xargs grep -H .
- parallel-perf-output/cpu-0/time-range-0/cmd.txt:perf script --cpu=0 --time=,9469.005396999 --ns
- parallel-perf-output/cpu-0/time-range-1/cmd.txt:perf script --cpu=0 --time=9469.005397000, --ns
- parallel-perf-output/cpu-1/time-range-0/cmd.txt:perf script --cpu=1 --time=,9469.005396999 --ns
- parallel-perf-output/cpu-1/time-range-1/cmd.txt:perf script --cpu=1 --time=9469.005397000, --ns
- Subdivisions of time range, and cpus if the --per-cpu option is used, are
- expressed by the --time and --cpu perf script options respectively. If the
- supplied perf script command has a --time option, then that time range is
- subdivided, otherwise the time range given by 'time of first sample' to
- 'time of last sample' is used (refer perf script --header-only). Similarly, the
- supplied perf script command may provide a --cpu option, and only those CPUs
- will be processed.
- To prevent time intervals becoming too small, the --min-interval option can
- be used.
- Note there is special handling for processing Intel PT traces. If an interval is
- not specified and the perf record command contained the intel_pt event, then the
- time range will be subdivided in order to produce subdivisions that contain
- approximately the same amount of trace data. That is accomplished by counting
- double-quick (--itrace=qqi) samples, and choosing time ranges that encompass
- approximately the same number of samples. In that case, time ranges may not be
- the same for each CPU processed. For Intel PT, --per-cpu is the default, but
- that can be overridden by --no-per-cpu. Note, for Intel PT, double-quick
- decoding produces 1 sample for each PSB synchronization packet, which in turn
- come after a certain number of bytes output, determined by psb_period (refer
- perf Intel PT documentation). The minimum number of double-quick samples that
- will define a time range can be set by the --min_size option, which defaults to
- 64.
- """)
- ap.add_argument("-o", "--output-dir", default="parallel-perf-output", help="output directory (default 'parallel-perf-output')")
- 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)")
- ap.add_argument("-n", "--nr", type=int, default=0, help="number of time subdivisions (default is the number of jobs)")
- 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)")
- ap.add_argument("-c", "--per-cpu", action="store_true", help="process data for each CPU in parallel")
- ap.add_argument("-m", "--min-interval", type=float, default=glb_min_interval, help=f"minimum interval (default {glb_min_interval} seconds)")
- ap.add_argument("-p", "--pipe-to", help="command to pipe output to (optional)")
- ap.add_argument("-N", "--no-per-cpu", action="store_true", help="do not process data for each CPU in parallel")
- ap.add_argument("-b", "--min_size", type=int, default=glb_min_samples, help="minimum data size (for Intel PT in PSBs)")
- ap.add_argument("-D", "--dry-run", action="store_true", help="do not run any jobs, just show the perf script commands")
- ap.add_argument("-q", "--quiet", action="store_true", help="do not print any messages except errors")
- ap.add_argument("-v", "--verbose", action="store_true", help="print more messages")
- ap.add_argument("-d", "--debug", action="store_true", help="print debugging messages")
- cmd_line = list(args)
- try:
- split_pos = cmd_line.index("--")
- cmd = cmd_line[split_pos + 1:]
- args = cmd_line[:split_pos]
- except:
- cmd = None
- args = cmd_line
- a = ap.parse_args(args=args[1:])
- a.cmd = cmd
- a.verbosity = Verbosity(a.quiet, a.verbose, a.debug)
- try:
- if a.cmd == None:
- if len(args) <= 1:
- ap.print_help()
- return True
- raise Exception("Command line must contain '--' before perf command")
- return RunParallelPerf(a)
- except Exception as e:
- print("Fatal error: ", str(e))
- if a.debug:
- raise
- return False
- if __name__ == "__main__":
- if not Main(sys.argv):
- sys.exit(1)
|