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