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