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