Skip to content

Commit 02e982c

Browse files
AstroProfundisethercflow
authored andcommitted
Add ability to collect perf data of process by specifying its listening port (#14)
* perf: add arguments to collect perf data by process's listening port * logfiles & config: adjust output path to proper sub-directory * perf: add argument to run `perf archive` after collecting data * perf: fix compatibility with Python 2 * insight: disable collector and pdctl by default, if calling a specific task * process: minor cleanup of code * process: unify argument names
1 parent 9decce9 commit 02e982c

File tree

7 files changed

+124
-40
lines changed

7 files changed

+124
-40
lines changed

insight.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,21 @@ def run_perf(self, args):
107107
perf_proc = self.format_proc_info("name")
108108
self.insight_perf = perf.InsightPerf(perf_proc, args)
109109
# parse pid list
110-
elif len(args.pid) > 0:
110+
elif args.pid:
111111
perf_proc = {}
112112
for _pid in args.pid:
113113
perf_proc[_pid] = None
114114
self.insight_perf = perf.InsightPerf(perf_proc, args)
115+
# find process by port
116+
elif args.proc_listen_port:
117+
perf_proc = {}
118+
pid_list = proc_meta.find_process_by_port(
119+
args.proc_listen_port, args.proc_listen_proto)
120+
if not pid_list or len(pid_list) < 1:
121+
return
122+
for _pid in pid_list:
123+
perf_proc[_pid] = None
124+
self.insight_perf = perf.InsightPerf(perf_proc, args)
115125
else:
116126
self.insight_perf = perf.InsightPerf(options=args)
117127
self.insight_perf.run(self.full_outdir)
@@ -170,10 +180,11 @@ def save_logfiles(self, args):
170180
proc_cmdline = self.format_proc_info("cmd") # cmdline of process
171181
if args.log_auto:
172182
self.insight_logfiles.save_logfiles_auto(
173-
proc_cmdline=proc_cmdline, outputdir=self.outdir)
183+
proc_cmdline=proc_cmdline, outputdir=self.full_outdir)
174184
else:
175-
self.insight_logfiles.save_tidb_logfiles(outputdir=self.outdir)
176-
self.insight_logfiles.save_system_log(outputdir=self.outdir)
185+
self.insight_logfiles.save_tidb_logfiles(
186+
outputdir=self.full_outdir)
187+
self.insight_logfiles.save_system_log(outputdir=self.full_outdir)
177188

178189
def save_configs(self, args):
179190
if not args.config_file:
@@ -182,14 +193,15 @@ def save_configs(self, args):
182193

183194
self.insight_configfiles = configfiles.InsightConfigFiles(options=args)
184195
if args.config_sysctl:
185-
self.insight_configfiles.save_sysconf(outputdir=self.outdir)
196+
self.insight_configfiles.save_sysconf(outputdir=self.full_outdir)
186197
# collect TiDB configs
187198
if args.config_auto:
188199
proc_cmdline = self.format_proc_info("cmd") # cmdline of process
189200
self.insight_configfiles.save_configs_auto(
190-
proc_cmdline=proc_cmdline, outputdir=self.outdir)
201+
proc_cmdline=proc_cmdline, outputdir=self.full_outdir)
191202
else:
192-
self.insight_configfiles.save_tidb_configs(outputdir=self.outdir)
203+
self.insight_configfiles.save_tidb_configs(
204+
outputdir=self.full_outdir)
193205

194206
def read_pdctl(self, args):
195207
self.insight_pdctl = pdctl.PDCtl(host=args.pd_host, port=args.pd_port)
@@ -211,19 +223,24 @@ def read_pdctl(self, args):
211223

212224
insight = Insight(args)
213225

214-
insight.collector()
226+
if (not args.pid and not args.proc_listen_port
227+
and not args.log_auto and not args.config_auto
228+
):
229+
insight.collector()
230+
# check size of data folder of TiDB processes
231+
insight.get_datadir_size()
232+
# list files opened by TiDB processes
233+
insight.get_lsof_tidb()
215234
# WIP: call scripts that collect metrics of the node
216235
insight.run_perf(args)
217-
# check size of data folder of TiDB processes
218-
insight.get_datadir_size()
219-
# list files opened by TiDB processes
220-
insight.get_lsof_tidb()
221236
# save log files
222237
insight.save_logfiles(args)
223238
# save config files
224239
insight.save_configs(args)
225-
# read and save `pd-ctl` info
226-
insight.read_pdctl(args)
240+
241+
if args.pdctl:
242+
# read and save `pd-ctl` info
243+
insight.read_pdctl(args)
227244

228245
# compress all output to tarball
229246
if args.compress:

measurement/files/configfiles.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ def list_config_files(base_dir, prefix):
7272
return file_list
7373

7474
source_dir = self.config_options.config_dir
75-
if not os.path.isdir(source_dir):
76-
logging.fatal("Source config path is not a directory.")
75+
if not source_dir or not os.path.isdir(source_dir):
76+
logging.fatal(
77+
"Source config path is not a directory. Did you set correct `--config-dir`?")
7778
return
7879
output_base = outputdir
7980
if not output_base:

measurement/files/fileutils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33

44
import logging
55
import os
6+
import sys
67

78
from measurement import util
89

10+
# get a numeric Python version
11+
python_version = sys.version_info[0] + sys.version_info[1] / 10
12+
913

1014
# read data from file
1115
def read_file(filename):
@@ -45,6 +49,28 @@ def create_dir(path):
4549
return None
4650

4751

52+
# os.scandir() is added in Python 3.5 and has better performance than os.listdir()
53+
# so we try to use it if available, and fall back to os.listdir() for older versions
54+
def list_dir(path):
55+
file_list = []
56+
try:
57+
if python_version >= 3.5:
58+
for entry in os.scandir(path):
59+
file_list.append("%s/%s" % (path, entry.name))
60+
else:
61+
for file in os.listdir(path):
62+
file_list.append("%s/%s" % (path, file))
63+
except OSError as e:
64+
# There is PermissionError in Python 3.3+, but only OSError in Python 2
65+
import errno
66+
if e.errno == errno.EACCES or e.errno == errno.EPERM:
67+
logging.warn("Permission Denied reading %s" % path)
68+
elif e.errno == errno.ENOENT:
69+
# when a process just exited
70+
pass
71+
return file_list
72+
73+
4874
def build_full_output_dir(basedir=None, subdir=None):
4975
if basedir is None and subdir is None:
5076
# default to current working directory

measurement/files/logfiles.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,9 @@ def save_system_log(self, outputdir=None):
105105
def save_tidb_logfiles(self, outputdir=None):
106106
# init values of args
107107
source_dir = self.log_options.log_dir
108-
if not os.path.isdir(source_dir):
109-
logging.fatal("Source log path is not a directory.")
108+
if not source_dir or not os.path.isdir(source_dir):
109+
logging.fatal(
110+
"Source log path is not a directory. Did you set correct `--log-dir`?")
110111
return
111112
output_base = outputdir
112113
if not output_base:

measurement/perf.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def __init__(self, process={}, options={}):
2626
self.perf_options = options
2727

2828
# set params of perf
29-
def build_cmd(self, pid=None, outfile=None, outdir=None):
29+
def build_record_cmd(self, pid=None, outfile=None, outdir=None):
3030
cmd = ["perf", # default executable name
3131
"record", # default action of perf
3232
"-g",
@@ -44,17 +44,17 @@ def build_cmd(self, pid=None, outfile=None, outdir=None):
4444
except (KeyError, TypeError):
4545
cmd.append("120") # default to 120Hz
4646

47-
if pid is not None:
47+
if pid:
4848
cmd.append("-p")
4949
cmd.append("%d" % pid)
5050
else:
5151
cmd.append("-a") # default to whole system
5252

5353
# default will be perf.data if nothing specified
54-
if outfile is not None:
54+
if outfile:
5555
cmd.append("-o")
5656
cmd.append("%s/%s.data" % (outdir, outfile))
57-
elif outfile is None and pid is not None:
57+
elif not outfile and pid:
5858
cmd.append("-o")
5959
cmd.append("%s/%d.data" % (outdir, pid))
6060

@@ -66,6 +66,20 @@ def build_cmd(self, pid=None, outfile=None, outdir=None):
6666

6767
return cmd
6868

69+
def build_archive_cmd(self, pid=None, outfile=None, outdir=None):
70+
cmd = ["perf",
71+
"archive"]
72+
73+
# default will be perf.data if nothing specified
74+
if outfile:
75+
cmd.append("%s/%s.data" % (outdir, outfile))
76+
elif not outfile and pid:
77+
cmd.append("%s/%d.data" % (outdir, pid))
78+
else:
79+
cmd.append("%s/perf.data" % outdir)
80+
81+
return cmd
82+
6983
def run(self, outputdir=None):
7084
# set output path of perf data
7185
full_outputdir = fileutils.build_full_output_dir(
@@ -79,7 +93,7 @@ def run(self, outputdir=None):
7993
if len(self.process_info) > 0:
8094
# perf on given process(es)
8195
for pid, pname in self.process_info.items():
82-
cmd = self.build_cmd(pid, pname, full_outputdir)
96+
cmd = self.build_record_cmd(pid, pname, full_outputdir)
8397
# TODO: unified output: "Now perf recording %s(%d)..." % (pname, pid)
8498
stdout, stderr = util.run_cmd(cmd)
8599
if stdout:
@@ -88,13 +102,25 @@ def run(self, outputdir=None):
88102
if stderr:
89103
fileutils.write_file(
90104
path.join(full_outputdir, "%s.stderr" % pname), stderr)
105+
if self.perf_options.perf_archive:
106+
cmd = self.build_archive_cmd(pid, pname, full_outputdir)
107+
stdout, stderr = util.run_cmd(cmd)
108+
if stderr:
109+
fileutils.write_file(
110+
path.join(full_outputdir, "%s.archive.stderr" % pname), stderr)
91111
else:
92112
# perf the entire system
93-
cmd = self.build_cmd()
113+
cmd = self.build_record_cmd()
94114
stdout, stderr = util.run_cmd(cmd)
95115
if stdout:
96116
fileutils.write_file(
97117
path.join(full_outputdir, "perf.stdout"), stdout)
98118
if stderr:
99119
fileutils.write_file(
100120
path.join(full_outputdir, "perf.stderr"), stderr)
121+
if self.perf_options.perf_archive:
122+
cmd = self.build_archive_cmd()
123+
stdout, stderr = util.run_cmd(cmd)
124+
if stderr:
125+
fileutils.write_file(
126+
path.join(full_outputdir, "perf.archive.stderr"), stderr)

measurement/process/meta.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from measurement.files import fileutils
66

77

8-
def find_process_by_port(port=None, protocol="tcp"):
8+
def find_process_by_port(port=None, protocol=None):
9+
if not protocol:
10+
protocol = "tcp"
911
process_list = []
1012
if not port:
1113
logging.fatal("No process listening port specified.")
@@ -14,22 +16,25 @@ def find_process_by_port(port=None, protocol="tcp"):
1416
# iterate over all file descriptors and build a socket address -> pid map
1517
def build_inode_to_pid_map():
1618
result = {}
17-
for entry in os.scandir("/proc"):
19+
for entry in fileutils.list_dir("/proc"):
1820
# find all PIDs
19-
if str.isdigit(entry.name):
20-
try:
21-
for _fd in os.scandir("/proc/%s/fd" % entry.name):
22-
_fd_target = os.readlink(_fd.path)
23-
if not str.startswith(_fd_target, "socket"):
24-
continue
25-
_socket = _fd_target.split(":[")[-1][:-1]
26-
try:
27-
result[_socket].append(entry.name)
28-
except KeyError:
29-
result[_socket] = [entry.name]
30-
except PermissionError:
31-
logging.warn(
32-
"Permission Denied reading /proc/%s/fd" % entry.name)
21+
fname = entry.split('/')[-1]
22+
if str.isdigit(fname):
23+
for _fd in fileutils.list_dir("/proc/%s/fd" % fname):
24+
try:
25+
_fd_target = os.readlink(_fd)
26+
except OSError as e:
27+
import errno
28+
if e.errno == errno.ENOENT:
29+
pass
30+
raise e
31+
if not str.startswith(_fd_target, "socket"):
32+
continue
33+
_socket = _fd_target.split(":[")[-1][:-1]
34+
try:
35+
result[_socket].append(int(fname))
36+
except KeyError:
37+
result[_socket] = [int(fname)]
3338
return result
3439

3540
def find_inode_by_port(port, protocol):

measurement/util.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ def parse_insight_opts():
6666
help="Collect trace info using perf. Disabled by default.")
6767
parser.add_argument("--pid", type=int, action="append", default=None,
6868
help="""PID of process to run perf on. If `-p`/`--perf` is not set, this value will not take effect. Multiple PIDs can be set by using more than one `--pid` argument. `None` by default which means the whole system.""")
69+
parser.add_argument("--proc-listen-port", action="store", type=int, default=None,
70+
help="Collect perf data of process that listen on given port. This value will be ignored if `--pid` is set.")
71+
parser.add_argument("--proc-listen-proto", action="store", default=None,
72+
help="Protocol type of listen port, available values are: tcp/udp. If not set, only TCP listening ports are checked.")
6973
parser.add_argument("--tidb-proc", action="store_true", default=False,
7074
help="Collect perf data for PD/TiDB/TiKV processes instead of the whole system.")
7175
parser.add_argument("--perf-exec", type=int, action="store", default=None,
@@ -74,6 +78,8 @@ def parse_insight_opts():
7478
help="Event sampling frequency of perf-record, in Hz.")
7579
parser.add_argument("--perf-time", type=int, action="store", default=None,
7680
help="Time period of perf recording, in seconds.")
81+
parser.add_argument("--perf-archive", action="store_true", default=False,
82+
help="Run `perf archive` after collecting data, useful when reading data on another machine. Disabled by default.")
7783

7884
parser.add_argument("-l", "--log", action="store_true", default=False,
7985
help="Collect log files in output. PD/TiDB/TiKV logs are included by default.")
@@ -99,6 +105,8 @@ def parse_insight_opts():
99105
parser.add_argument("--config-prefix", action="store", default=None,
100106
help="The prefix of config files, will be directory name of all config files, will be in the name of output tarball. If `--config-auto` is set, the value will be ignored.")
101107

108+
parser.add_argument("--pdctl", action="store_true", default=False,
109+
help="Enable collecting data from PD API. Disabled by default.")
102110
parser.add_argument("--pd-host", action="store", default=None,
103111
help="The host of the PD server. `localhost` by default.")
104112
parser.add_argument("--pd-port", type=int, action="store", default=None,

0 commit comments

Comments
 (0)