xref: /freebsd/sys/contrib/openzfs/cmd/zilstat.in (revision 2668e76d6e764c5c361156ffa3d39eb02ce8e5d9)
1#!/usr/bin/env @PYTHON_SHEBANG@
2#
3# Print out statistics for all zil stats. This information is
4# available through the zil kstat.
5#
6# CDDL HEADER START
7#
8# The contents of this file are subject to the terms of the
9# Common Development and Distribution License, Version 1.0 only
10# (the "License").  You may not use this file except in compliance
11# with the License.
12#
13# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
14# or https://opensource.org/licenses/CDDL-1.0.
15# See the License for the specific language governing permissions
16# and limitations under the License.
17#
18# When distributing Covered Code, include this CDDL HEADER in each
19# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
20# If applicable, add the following below this CDDL HEADER, with the
21# fields enclosed by brackets "[]" replaced with your own identifying
22# information: Portions Copyright [yyyy] [name of copyright owner]
23#
24# This script must remain compatible with Python 3.6+.
25#
26
27import sys
28import subprocess
29import time
30import copy
31import os
32import re
33import signal
34from collections import defaultdict
35import argparse
36from argparse import RawTextHelpFormatter
37
38cols = {
39	# hdr:       [size,      scale,      kstat name]
40	"time":      [8,         -1,         "time"],
41	"pool":      [12,        -1,         "pool"],
42	"ds":        [12,        -1,         "dataset_name"],
43	"obj":       [12,        -1,         "objset"],
44	"cc":        [5,         1000,       "zil_commit_count"],
45	"cwc":       [5,         1000,       "zil_commit_writer_count"],
46	"cec":       [5,         1000,       "zil_commit_error_count"],
47	"csc":       [5,         1000,       "zil_commit_stall_count"],
48	"cSc":       [5,         1000,       "zil_commit_suspend_count"],
49	"ic":        [5,         1000,       "zil_itx_count"],
50	"iic":       [5,         1000,       "zil_itx_indirect_count"],
51	"iib":       [5,         1024,       "zil_itx_indirect_bytes"],
52	"icc":       [5,         1000,       "zil_itx_copied_count"],
53	"icb":       [5,         1024,       "zil_itx_copied_bytes"],
54	"inc":       [5,         1000,       "zil_itx_needcopy_count"],
55	"inb":       [5,         1024,       "zil_itx_needcopy_bytes"],
56	"idc":       [5,         1000,       "icc+inc"],
57	"idb":       [5,         1024,       "icb+inb"],
58	"iwc":       [5,         1000,       "iic+idc"],
59	"iwb":       [5,         1024,       "iib+idb"],
60	"imnc":      [6,         1000,       "zil_itx_metaslab_normal_count"],
61	"imnb":      [6,         1024,       "zil_itx_metaslab_normal_bytes"],
62	"imnw":      [6,         1024,       "zil_itx_metaslab_normal_write"],
63	"imna":      [6,         1024,       "zil_itx_metaslab_normal_alloc"],
64	"imsc":      [6,         1000,       "zil_itx_metaslab_slog_count"],
65	"imsb":      [6,         1024,       "zil_itx_metaslab_slog_bytes"],
66	"imsw":      [6,         1024,       "zil_itx_metaslab_slog_write"],
67	"imsa":      [6,         1024,       "zil_itx_metaslab_slog_alloc"],
68	"imc":       [5,         1000,       "imnc+imsc"],
69	"imb":       [5,         1024,       "imnb+imsb"],
70	"imw":       [5,         1024,       "imnw+imsw"],
71	"ima":       [5,         1024,       "imna+imsa"],
72	"se%":       [3,         100,        "imb/ima"],
73	"sen%":      [4,         100,        "imnb/imna"],
74	"ses%":      [4,         100,        "imsb/imsa"],
75	"te%":       [3,         100,        "imb/imw"],
76	"ten%":      [4,         100,        "imnb/imnw"],
77	"tes%":      [4,         100,        "imsb/imsw"],
78}
79
80hdr = ["time", "ds", "cc", "ic", "idc", "idb", "iic", "iib",
81	"imnc", "imnw", "imsc", "imsw"]
82
83ghdr = ["time", "cc", "ic", "idc", "idb", "iic", "iib",
84	"imnc", "imnw", "imsc", "imsw"]
85
86cmd = ("Usage: zilstat [-hgdv] [-i interval] [-p pool_name]")
87
88curr = {}
89diff = {}
90kstat = {}
91ds_pairs = {}
92pool_name = None
93dataset_name = None
94interval = 0
95sep = "  "
96gFlag = True
97dsFlag = False
98
99def prettynum(sz, scale, num=0):
100	suffix = [' ', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']
101	index = 0
102	save = 0
103
104	if scale == -1:
105		return "%*s" % (sz, num)
106
107	# Rounding error, return 0
108	elif 0 < num < 1:
109		num = 0
110
111	while num > scale and index < 5:
112		save = num
113		num = num / scale
114		index += 1
115
116	if index == 0:
117		return "%*d" % (sz, num)
118
119	if (save / scale) < 10:
120		return "%*.1f%s" % (sz - 1, num, suffix[index])
121	else:
122		return "%*d%s" % (sz - 1, num, suffix[index])
123
124def print_header():
125	global hdr
126	global sep
127	for col in hdr:
128		new_col = col
129		if interval > 0 and cols[col][1] > 100:
130			new_col += "/s"
131		sys.stdout.write("%*s%s" % (cols[col][0], new_col, sep))
132	sys.stdout.write("\n")
133
134def print_values(v):
135	global hdr
136	global sep
137	for col in hdr:
138		val = v[cols[col][2]]
139		if interval > 0 and cols[col][1] > 100:
140			val = v[cols[col][2]] // interval
141		sys.stdout.write("%s%s" % (
142			prettynum(cols[col][0], cols[col][1], val), sep))
143	sys.stdout.write("\n")
144
145def print_dict(d):
146	for pool in d:
147		for objset in d[pool]:
148			print_values(d[pool][objset])
149
150def detailed_usage():
151	sys.stderr.write("%s\n" % cmd)
152	sys.stderr.write("Field definitions are as follows:\n")
153	for key in cols:
154		sys.stderr.write("%11s : %s\n" % (key, cols[key][2]))
155	sys.stderr.write("\n")
156	sys.exit(0)
157
158def init():
159	global pool_name
160	global dataset_name
161	global interval
162	global hdr
163	global curr
164	global gFlag
165	global sep
166
167	curr = dict()
168
169	parser = argparse.ArgumentParser(description='Program to print zilstats',
170                                	 add_help=True,
171					 formatter_class=RawTextHelpFormatter,
172					 epilog="\nUsage Examples\n"\
173				 		"Note: Global zilstats is shown by default,"\
174						" if none of a|p|d option is not provided\n"\
175				 		"\tzilstat -a\n"\
176						'\tzilstat -v\n'\
177						'\tzilstat -p tank\n'\
178						'\tzilstat -d tank/d1,tank/d2,tank/zv1\n'\
179						'\tzilstat -i 1\n'\
180						'\tzilstat -s \"***\"\n'\
181						'\tzilstat -f zcwc,zimnb,zimsb\n')
182
183	parser.add_argument(
184		"-v", "--verbose",
185		action="store_true",
186		help="List field headers and definitions"
187	)
188
189	pool_grp = parser.add_mutually_exclusive_group()
190
191	pool_grp.add_argument(
192		"-a", "--all",
193		action="store_true",
194		dest="all",
195		help="Print all dataset stats"
196	)
197
198	pool_grp.add_argument(
199		"-p", "--pool",
200		type=str,
201		help="Print stats for all datasets of a speicfied pool"
202	)
203
204	pool_grp.add_argument(
205		"-d", "--dataset",
206		type=str,
207		help="Print given dataset(s) (Comma separated)"
208	)
209
210	parser.add_argument(
211		"-f", "--columns",
212		type=str,
213		help="Specify specific fields to print (see -v)"
214	)
215
216	parser.add_argument(
217		"-s", "--separator",
218		type=str,
219		help="Override default field separator with custom "
220			 "character or string"
221	)
222
223	parser.add_argument(
224		"-i", "--interval",
225		type=int,
226		dest="interval",
227		help="Print stats between specified interval"
228			 " (in seconds)"
229	)
230
231	parsed_args = parser.parse_args()
232
233	if parsed_args.verbose:
234		detailed_usage()
235
236	if parsed_args.all:
237		gFlag = False
238
239	if parsed_args.interval:
240		interval = parsed_args.interval
241
242	if parsed_args.pool:
243		pool_name = parsed_args.pool
244		gFlag = False
245
246	if parsed_args.dataset:
247		dataset_name = parsed_args.dataset
248		gFlag = False
249
250	if parsed_args.separator:
251		sep = parsed_args.separator
252
253	if gFlag:
254		hdr = ghdr
255
256	if parsed_args.columns:
257		hdr = parsed_args.columns.split(",")
258
259		invalid = []
260		for ele in hdr:
261			if ele not in cols:
262				invalid.append(ele)
263
264		if len(invalid) > 0:
265			sys.stderr.write("Invalid column definition! -- %s\n" % invalid)
266			sys.exit(1)
267
268	if pool_name and dataset_name:
269		print ("Error: Can not filter both dataset and pool")
270		sys.exit(1)
271
272def FileCheck(fname):
273	try:
274		return (open(fname))
275	except IOError:
276		print ("Unable to open zilstat proc file: " + fname)
277		sys.exit(1)
278
279if sys.platform.startswith('freebsd'):
280	# Requires py-sysctl on FreeBSD
281	import sysctl
282
283	def kstat_update(pool = None, objid = None):
284		global kstat
285		kstat = {}
286		if not pool:
287			file = "kstat.zfs.misc.zil"
288			k = [ctl for ctl in sysctl.filter(file) \
289				if ctl.type != sysctl.CTLTYPE_NODE]
290			kstat_process_str(k, file, "GLOBAL", len(file + "."))
291		elif objid:
292			file = "kstat.zfs." + pool + ".dataset.objset-" + objid
293			k = [ctl for ctl in sysctl.filter(file) if ctl.type \
294				!= sysctl.CTLTYPE_NODE]
295			kstat_process_str(k, file, objid, len(file + "."))
296		else:
297			file = "kstat.zfs." + pool + ".dataset"
298			zil_start = len(file + ".")
299			obj_start = len("kstat.zfs." + pool + ".")
300			k = [ctl for ctl in sysctl.filter(file)
301				if ctl.type != sysctl.CTLTYPE_NODE]
302			for s in k:
303				if not s or (s.name.find("zil") == -1 and \
304					s.name.find("dataset_name") == -1):
305					continue
306				name, value = s.name, s.value
307				objid = re.findall(r'0x[0-9A-F]+', \
308					name[obj_start:], re.I)[0]
309				if objid not in kstat:
310					kstat[objid] = dict()
311				zil_start = len(file + ".objset-" + \
312					objid + ".")
313				kstat[objid][name[zil_start:]] = value \
314					if (name.find("dataset_name")) \
315					else int(value)
316
317	def kstat_process_str(k, file, objset = "GLOBAL", zil_start = 0):
318			global kstat
319			if not k:
320				print("Unable to process kstat for: " + file)
321				sys.exit(1)
322			kstat[objset] = dict()
323			for s in k:
324				if not s or (s.name.find("zil") == -1 and \
325				    s.name.find("dataset_name") == -1):
326					continue
327				name, value = s.name, s.value
328				kstat[objset][name[zil_start:]] = value \
329				    if (name.find("dataset_name")) else int(value)
330
331elif sys.platform.startswith('linux'):
332	def kstat_update(pool = None, objid = None):
333		global kstat
334		kstat = {}
335		if not pool:
336			k = [line.strip() for line in \
337				FileCheck("/proc/spl/kstat/zfs/zil")]
338			kstat_process_str(k, "/proc/spl/kstat/zfs/zil")
339		elif objid:
340			file = "/proc/spl/kstat/zfs/" + pool + "/objset-" + objid
341			k = [line.strip() for line in FileCheck(file)]
342			kstat_process_str(k, file, objid)
343		else:
344			if not os.path.exists(f"/proc/spl/kstat/zfs/{pool}"):
345				print("Pool \"" + pool + "\" does not exist, Exitting")
346				sys.exit(1)
347			objsets = os.listdir(f'/proc/spl/kstat/zfs/{pool}')
348			for objid in objsets:
349				if objid.find("objset-") == -1:
350					continue
351				file = "/proc/spl/kstat/zfs/" + pool + "/" + objid
352				k = [line.strip() for line in FileCheck(file)]
353				kstat_process_str(k, file, objid.replace("objset-", ""))
354
355	def kstat_process_str(k, file, objset = "GLOBAL", zil_start = 0):
356			global kstat
357			if not k:
358				print("Unable to process kstat for: " + file)
359				sys.exit(1)
360
361			kstat[objset] = dict()
362			for s in k:
363				if not s or (s.find("zil") == -1 and \
364				    s.find("dataset_name") == -1):
365					continue
366				name, unused, value = s.split()
367				kstat[objset][name] = value \
368				    if (name == "dataset_name") else int(value)
369
370def zil_process_kstat():
371	global curr, pool_name, dataset_name, dsFlag, ds_pairs
372	curr.clear()
373	if gFlag == True:
374		kstat_update()
375		zil_build_dict()
376	else:
377		if pool_name:
378			kstat_update(pool_name)
379			zil_build_dict(pool_name)
380		elif dataset_name:
381			if dsFlag == False:
382				dsFlag = True
383				datasets = dataset_name.split(',')
384				ds_pairs = defaultdict(list)
385				for ds in datasets:
386					try:
387						objid = subprocess.check_output(['zfs',
388						    'list', '-Hpo', 'objsetid', ds], \
389						    stderr=subprocess.DEVNULL) \
390						    .decode('utf-8').strip()
391					except subprocess.CalledProcessError as e:
392						print("Command: \"zfs list -Hpo objset "\
393						+ str(ds) + "\" failed with error code:"\
394						+ str(e.returncode))
395						print("Please make sure that dataset \""\
396						+ str(ds) + "\" exists")
397						sys.exit(1)
398					if not objid:
399						continue
400					ds_pairs[ds.split('/')[0]]. \
401						append(hex(int(objid)))
402			for pool, objids in ds_pairs.items():
403				for objid in objids:
404					kstat_update(pool, objid)
405					zil_build_dict(pool)
406		else:
407			try:
408				pools = subprocess.check_output(['zpool', 'list', '-Hpo',\
409				    'name']).decode('utf-8').split()
410			except subprocess.CalledProcessError as e:
411				print("Command: \"zpool list -Hpo name\" failed with error"\
412				    "code: " + str(e.returncode))
413				sys.exit(1)
414			for pool in pools:
415				kstat_update(pool)
416				zil_build_dict(pool)
417
418def calculate_diff():
419	global curr, diff
420	prev = copy.deepcopy(curr)
421	zil_process_kstat()
422	diff = copy.deepcopy(curr)
423	for pool in curr:
424		for objset in curr[pool]:
425			for key in curr[pool][objset]:
426				if not isinstance(diff[pool][objset][key], int):
427					continue
428				# If prev is NULL, this is the
429				# first time we are here
430				if not prev:
431					diff[pool][objset][key] = 0
432				else:
433					diff[pool][objset][key] \
434						= curr[pool][objset][key] \
435						- prev[pool][objset][key]
436
437def zil_build_dict(pool = "GLOBAL"):
438	global kstat
439	for objset in kstat:
440		for key in kstat[objset]:
441			val = kstat[objset][key]
442			if pool not in curr:
443				curr[pool] = dict()
444			if objset not in curr[pool]:
445				curr[pool][objset] = dict()
446			curr[pool][objset][key] = val
447
448def zil_extend_dict():
449	global diff
450	for pool in diff:
451		for objset in diff[pool]:
452			diff[pool][objset]["pool"] = pool
453			diff[pool][objset]["objset"] = objset
454			diff[pool][objset]["time"] = time.strftime("%H:%M:%S", \
455				time.localtime())
456			diff[pool][objset]["icc+inc"] = \
457				diff[pool][objset]["zil_itx_copied_count"] + \
458				diff[pool][objset]["zil_itx_needcopy_count"]
459			diff[pool][objset]["icb+inb"] = \
460				diff[pool][objset]["zil_itx_copied_bytes"] + \
461				diff[pool][objset]["zil_itx_needcopy_bytes"]
462			diff[pool][objset]["iic+idc"] = \
463				diff[pool][objset]["zil_itx_indirect_count"] + \
464				diff[pool][objset]["zil_itx_copied_count"] + \
465				diff[pool][objset]["zil_itx_needcopy_count"]
466			diff[pool][objset]["iib+idb"] = \
467				diff[pool][objset]["zil_itx_indirect_bytes"] + \
468				diff[pool][objset]["zil_itx_copied_bytes"] + \
469				diff[pool][objset]["zil_itx_needcopy_bytes"]
470			diff[pool][objset]["imnc+imsc"] = \
471				diff[pool][objset]["zil_itx_metaslab_normal_count"] + \
472				diff[pool][objset]["zil_itx_metaslab_slog_count"]
473			diff[pool][objset]["imnb+imsb"] = \
474				diff[pool][objset]["zil_itx_metaslab_normal_bytes"] + \
475				diff[pool][objset]["zil_itx_metaslab_slog_bytes"]
476			diff[pool][objset]["imnw+imsw"] = \
477				diff[pool][objset]["zil_itx_metaslab_normal_write"] + \
478				diff[pool][objset]["zil_itx_metaslab_slog_write"]
479			diff[pool][objset]["imna+imsa"] = \
480				diff[pool][objset]["zil_itx_metaslab_normal_alloc"] + \
481				diff[pool][objset]["zil_itx_metaslab_slog_alloc"]
482			if diff[pool][objset]["imna+imsa"] > 0:
483				diff[pool][objset]["imb/ima"] = 100 * \
484					diff[pool][objset]["imnb+imsb"] // \
485					diff[pool][objset]["imna+imsa"]
486			else:
487				diff[pool][objset]["imb/ima"] = 100
488			if diff[pool][objset]["zil_itx_metaslab_normal_alloc"] > 0:
489				diff[pool][objset]["imnb/imna"] = 100 * \
490					diff[pool][objset]["zil_itx_metaslab_normal_bytes"] // \
491					diff[pool][objset]["zil_itx_metaslab_normal_alloc"]
492			else:
493				diff[pool][objset]["imnb/imna"] = 100
494			if diff[pool][objset]["zil_itx_metaslab_slog_alloc"] > 0:
495				diff[pool][objset]["imsb/imsa"] = 100 * \
496					diff[pool][objset]["zil_itx_metaslab_slog_bytes"] // \
497					diff[pool][objset]["zil_itx_metaslab_slog_alloc"]
498			else:
499				diff[pool][objset]["imsb/imsa"] = 100
500			if diff[pool][objset]["imnw+imsw"] > 0:
501				diff[pool][objset]["imb/imw"] = 100 * \
502					diff[pool][objset]["imnb+imsb"] // \
503					diff[pool][objset]["imnw+imsw"]
504			else:
505				diff[pool][objset]["imb/imw"] = 100
506			if diff[pool][objset]["zil_itx_metaslab_normal_alloc"] > 0:
507				diff[pool][objset]["imnb/imnw"] = 100 * \
508					diff[pool][objset]["zil_itx_metaslab_normal_bytes"] // \
509					diff[pool][objset]["zil_itx_metaslab_normal_write"]
510			else:
511				diff[pool][objset]["imnb/imnw"] = 100
512			if diff[pool][objset]["zil_itx_metaslab_slog_alloc"] > 0:
513				diff[pool][objset]["imsb/imsw"] = 100 * \
514					diff[pool][objset]["zil_itx_metaslab_slog_bytes"] // \
515					diff[pool][objset]["zil_itx_metaslab_slog_write"]
516			else:
517				diff[pool][objset]["imsb/imsw"] = 100
518
519def sign_handler_epipe(sig, frame):
520	print("Caught EPIPE signal: " + str(frame))
521	print("Exitting...")
522	sys.exit(0)
523
524def main():
525	global interval
526	global curr, diff
527	hprint = False
528	init()
529	signal.signal(signal.SIGINT, signal.SIG_DFL)
530	signal.signal(signal.SIGPIPE, sign_handler_epipe)
531
532	zil_process_kstat()
533	if not curr:
534		print ("Error: No stats to show")
535		sys.exit(0)
536	print_header()
537	if interval > 0:
538		time.sleep(interval)
539		while True:
540			calculate_diff()
541			if not diff:
542				print ("Error: No stats to show")
543				sys.exit(0)
544			zil_extend_dict()
545			print_dict(diff)
546			time.sleep(interval)
547	else:
548		diff = curr
549		zil_extend_dict()
550		print_dict(diff)
551
552if __name__ == '__main__':
553	main()
554
555