curl_verbose: int,
download_parallel: int = 0,
server_addr: Optional[str] = None,
- with_dtrace: bool = False,
with_flame: bool = False,
socks_args: Optional[List[str]] = None,
limit_rate: Optional[str] = None):
self.server_port = server_port
self._silent_curl = not curl_verbose
self._download_parallel = download_parallel
- self._with_dtrace = with_dtrace
self._with_flame = with_flame
self._socks_args = socks_args
self._limit_rate = limit_rate
def mk_curl_client(self):
return CurlClient(env=self.env, silent=self._silent_curl,
server_addr=self.server_addr,
- with_dtrace=self._with_dtrace,
with_flame=self._with_flame,
socks_args=self._socks_args)
verbose=args.verbose,
curl_verbose=args.curl_verbose,
download_parallel=args.download_parallel,
- with_dtrace=args.dtrace,
with_flame=args.flame,
socks_args=socks_args,
limit_rate=args.limit_rate)
server_port=server_port,
verbose=args.verbose, curl_verbose=args.curl_verbose,
download_parallel=args.download_parallel,
- with_dtrace=args.dtrace,
with_flame=args.flame,
socks_args=socks_args,
limit_rate=args.limit_rate)
server_port=server_port,
verbose=args.verbose, curl_verbose=args.curl_verbose,
download_parallel=args.download_parallel,
- with_dtrace=args.dtrace,
+ with_flame=args.flame,
socks_args=socks_args,
limit_rate=args.limit_rate)
card.setup_resources(server_docs, downloads)
help="only start the servers")
parser.add_argument("--remote", action='store', type=str,
default=None, help="score against the remote server at <ip>:<port>")
- parser.add_argument("--dtrace", action='store_true',
- default = False, help="produce dtrace of curl")
parser.add_argument("--flame", action='store_true',
- default = False, help="produce a flame graph on curl, implies --dtrace")
+ default = False, help="produce a flame graph on curl")
parser.add_argument("--limit-rate", action='store', type=str,
default=None, help="use curl's --limit-rate")
f'stats={self.stats}]'
+class PerfProfile:
+
+ def __init__(self, pid: int, run_dir):
+ self._pid = pid
+ self._run_dir = run_dir
+ self._proc = None
+ self._rc = 0
+ self._file = os.path.join(self._run_dir, 'curl.perf_stacks')
+
+ def start(self):
+ if os.path.exists(self._file):
+ os.remove(self._file)
+ args = [
+ 'sudo', 'perf', 'record', '-F', '99', '-p', f'{self._pid}',
+ '-g', '--', 'sleep', '60'
+ ]
+ self._proc = subprocess.Popen(args, text=True, cwd=self._run_dir, shell=False)
+ assert self._proc
+
+ def finish(self):
+ if self._proc:
+ self._proc.terminate()
+ self._rc = self._proc.returncode
+ with open(self._file, 'w') as cout:
+ p = subprocess.run([
+ 'sudo', 'perf', 'script'
+ ], stdout=cout, cwd=self._run_dir, shell=False)
+ rc = p.returncode
+ if rc != 0:
+ raise Exception(f'perf returned error {rc}')
+
+ @property
+ def file(self):
+ return self._file
+
+
class DTraceProfile:
def __init__(self, pid: int, run_dir):
self._run_dir = run_dir
self._proc = None
self._rc = 0
- self._file = os.path.join(self._run_dir, 'curl.user_stacks')
+ self._file = os.path.join(self._run_dir, 'curl.dtrace_stacks')
def start(self):
if os.path.exists(self._file):
run_env: Optional[Dict[str, str]] = None,
server_addr: Optional[str] = None,
with_dtrace: bool = False,
+ with_perf: bool = False,
with_flame: bool = False,
socks_args: Optional[List[str]] = None):
self.env = env
self._headerfile = f'{self._run_dir}/curl.headers'
self._log_path = f'{self._run_dir}/curl.log'
self._with_dtrace = with_dtrace
+ self._with_perf = with_perf
self._with_flame = with_flame
+ self._fg_dir = None
if self._with_flame:
- self._with_dtrace = True
+ self._fg_dir = os.path.join(self.env.project_dir, '../FlameGraph')
+ if 'FLAMEGRAPH' in os.environ:
+ self._fg_dir = os.environ['FLAMEGRAPH']
+ if not os.path.exists(self._fg_dir):
+ raise Exception(f'FlameGraph checkout not found in {self._fg_dir}, set env variable FLAMEGRAPH')
+ if sys.platform.startswith('linux'):
+ self._with_perf = True
+ elif sys.platform.startswith('darwin'):
+ self._with_dtrace = True
+ else:
+ raise Exception(f'flame graphs unsupported on {sys.platform}')
self._socks_args = socks_args
self._silent = silent
self._run_env = run_env
exception = None
profile = None
tcpdump = None
+ perf = None
dtrace = None
if with_tcpdump:
tcpdump = RunTcpDump(self.env, self._run_dir)
profile = RunProfile(p.pid, started_at, self._run_dir)
if intext is not None and False:
p.communicate(input=intext.encode(), timeout=1)
- if self._with_dtrace:
+ if self._with_perf:
+ perf = PerfProfile(p.pid, self._run_dir)
+ perf.start()
+ elif self._with_dtrace:
dtrace = DTraceProfile(p.pid, self._run_dir)
dtrace.start()
ptimeout = 0.0
ended_at = datetime.now()
if tcpdump:
tcpdump.finish()
+ if perf:
+ perf.finish()
if dtrace:
dtrace.finish()
- if self._with_flame and dtrace:
- self._generate_flame(dtrace, args)
+ if self._with_flame:
+ self._generate_flame(args, dtrace=dtrace, perf=perf)
coutput = open(self._stdoutfile).readlines()
cerrput = open(self._stderrfile).readlines()
return ExecResult(args=args, exit_code=exitcode, exception=exception,
fin_response(response)
return r
- def _generate_flame(self, dtrace: DTraceProfile, curl_args: List[str]):
- log.info('generating flame graph from dtrace for this run')
+ def _perf_collapse(self, perf: PerfProfile, file_err):
+ if not os.path.exists(perf.file):
+ raise Exception(f'dtrace output file does not exist: {perf.file}')
+ fg_collapse = os.path.join(self._fg_dir, 'stackcollapse-perf.pl')
+ if not os.path.exists(fg_collapse):
+ raise Exception(f'FlameGraph script not found: {fg_collapse}')
+ stacks_collapsed = f'{perf.file}.collapsed'
+ log.info(f'collapsing stacks into {stacks_collapsed}')
+ with open(stacks_collapsed, 'w') as cout, open(file_err, 'w') as cerr:
+ p = subprocess.run([
+ fg_collapse, perf.file
+ ], stdout=cout, stderr=cerr, cwd=self._run_dir, shell=False)
+ rc = p.returncode
+ if rc != 0:
+ raise Exception(f'{fg_collapse} returned error {rc}')
+ return stacks_collapsed
+
+ def _dtrace_collapse(self, dtrace: DTraceProfile, file_err):
if not os.path.exists(dtrace.file):
raise Exception(f'dtrace output file does not exist: {dtrace.file}')
- if 'FLAMEGRAPH' not in os.environ:
- raise Exception('Env variable FLAMEGRAPH not set')
- fg_dir = os.environ['FLAMEGRAPH']
- if not os.path.exists(fg_dir):
- raise Exception(f'FlameGraph directory not found: {fg_dir}')
-
- fg_collapse = os.path.join(fg_dir, 'stackcollapse.pl')
+ fg_collapse = os.path.join(self._fg_dir, 'stackcollapse.pl')
if not os.path.exists(fg_collapse):
raise Exception(f'FlameGraph script not found: {fg_collapse}')
-
- fg_gen_flame = os.path.join(fg_dir, 'flamegraph.pl')
- if not os.path.exists(fg_gen_flame):
- raise Exception(f'FlameGraph script not found: {fg_gen_flame}')
-
- file_collapsed = f'{dtrace.file}.collapsed'
- file_svg = os.path.join(self._run_dir, 'curl.flamegraph.svg')
- file_err = os.path.join(self._run_dir, 'curl.flamegraph.stderr')
- log.info('waiting a sec for dtrace to finish flusheing its buffers')
- time.sleep(1)
- log.info(f'collapsing stacks into {file_collapsed}')
- with open(file_collapsed, 'w') as cout, open(file_err, 'w') as cerr:
+ stacks_collapsed = f'{dtrace.file}.collapsed'
+ log.info(f'collapsing stacks into {stacks_collapsed}')
+ with open(stacks_collapsed, 'w') as cout, open(file_err, 'a') as cerr:
p = subprocess.run([
fg_collapse, dtrace.file
], stdout=cout, stderr=cerr, cwd=self._run_dir, shell=False)
rc = p.returncode
if rc != 0:
raise Exception(f'{fg_collapse} returned error {rc}')
+ return stacks_collapsed
+
+ def _generate_flame(self, curl_args: List[str],
+ dtrace: Optional[DTraceProfile] = None,
+ perf: Optional[PerfProfile] = None):
+ fg_gen_flame = os.path.join(self._fg_dir, 'flamegraph.pl')
+ file_svg = os.path.join(self._run_dir, 'curl.flamegraph.svg')
+ if not os.path.exists(fg_gen_flame):
+ raise Exception(f'FlameGraph script not found: {fg_gen_flame}')
+
+ log.info('waiting a sec for perf/dtrace to finish flushing')
+ time.sleep(2)
+ log.info('generating flame graph for this run')
+ file_err = os.path.join(self._run_dir, 'curl.flamegraph.stderr')
+ if perf:
+ stacks_collapsed = self._perf_collapse(perf, file_err)
+ elif dtrace:
+ stacks_collapsed = self._dtrace_collapse(dtrace, file_err)
+ else:
+ raise Exception('no stacks measure given')
+
log.info(f'generating graph into {file_svg}')
cmdline = ' '.join(curl_args)
if len(cmdline) > 80:
else:
title = cmdline
subtitle = ''
- with open(file_svg, 'w') as cout, open(file_err, 'w') as cerr:
+ with open(file_svg, 'w') as cout, open(file_err, 'a') as cerr:
p = subprocess.run([
fg_gen_flame, '--colors', 'green',
'--title', title, '--subtitle', subtitle,
- file_collapsed
+ stacks_collapsed
], stdout=cout, stderr=cerr, cwd=self._run_dir, shell=False)
rc = p.returncode
if rc != 0: