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