git: 60c7313074b9 - main - Introduce nfsdtop(8)

From: Devin Teske <dteske_at_FreeBSD.org>
Date: Sun, 06 Sep 2026 22:36:51 UTC
The branch main has been updated by dteske:

URL: https://cgit.FreeBSD.org/src/commit/?id=60c7313074b93adf4ea70ff44b78b3b4fc15c29f

commit 60c7313074b93adf4ea70ff44b78b3b4fc15c29f
Author:     Devin Teske <dteske@FreeBSD.org>
AuthorDate: 2026-09-06 22:34:34 +0000
Commit:     Devin Teske <dteske@FreeBSD.org>
CommitDate: 2026-09-06 22:34:34 +0000

    Introduce nfsdtop(8)
    
    Display top-like NFS server I/O using dtrace(1).
    Also supports JSON output for time-series.
    
    Relnotes:       yes
    
    Reviewed by:    ziaee, bcr, adrian
    Differential Revision:  https://reviews.freebsd.org/D59438
---
 usr.sbin/Makefile          |    1 +
 usr.sbin/nfsdtop/Makefile  |    6 +
 usr.sbin/nfsdtop/nfsdtop   | 1565 ++++++++++++++++++++++++++++++++++++++++++++
 usr.sbin/nfsdtop/nfsdtop.8 |  310 +++++++++
 4 files changed, 1882 insertions(+)

diff --git a/usr.sbin/Makefile b/usr.sbin/Makefile
index 9e885d5d45c5..8336dbde4085 100644
--- a/usr.sbin/Makefile
+++ b/usr.sbin/Makefile
@@ -51,6 +51,7 @@ SUBDIR=	adduser \
 	nfscbd \
 	nfsd \
 	nfsdumpstate \
+	nfsdtop \
 	nfsrevoke \
 	nfsuserd \
 	nmtree \
diff --git a/usr.sbin/nfsdtop/Makefile b/usr.sbin/nfsdtop/Makefile
new file mode 100644
index 000000000000..b269ef4ea6d9
--- /dev/null
+++ b/usr.sbin/nfsdtop/Makefile
@@ -0,0 +1,6 @@
+PACKAGE=	nfs
+
+SCRIPTS=	nfsdtop
+MAN=		nfsdtop.8
+
+.include <bsd.prog.mk>
diff --git a/usr.sbin/nfsdtop/nfsdtop b/usr.sbin/nfsdtop/nfsdtop
new file mode 100755
index 000000000000..1bd03ea6d017
--- /dev/null
+++ b/usr.sbin/nfsdtop/nfsdtop
@@ -0,0 +1,1565 @@
+#!/bin/sh
+############################################################ LICENSE
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2020 Devin Teske
+#
+############################################################ IDENT(1)
+#
+# $Title: Script to generate top-like statistics for nfsd I/O $
+#
+############################################################ INFORMATION
+#
+# In nfsdtop, a ``view'' is the user's choice between -c, -g, -s, or -u.
+# For example, `-u' asks nfsdtop to display the ``user view'' where statistics
+# displayed are on a per-user basis.
+#
+# The code is broken down into:
+# 	- View selection (user choice)
+# 	- Filter selection (user choice)
+# 	- dtrace execution
+# 	- awk to process dtrace output
+#
+# Code navigating Search Terms/ST:
+# 	DTRACE		Start of dtrace
+# 	FILTERS		Data filters
+# 	JSONDATA	JSON data generation
+# 	OPS		Data operations (read vs write)
+# 	POST		dtrace post-processor (awk)
+# 	PRE		Start of pre-processor (sh)
+# 	SORTING		Sort routines
+# 	TRACEDATA	dtrace data generation and processing
+# 	VIEWDATA	View data generation
+# 	VIEWS		View processing
+#
+############################################################ DEFAULTS
+
+DEFAULT_INTERVAL=2.0 # seconds
+
+#
+# User/Group map files
+#
+DEFAULT_PASSWD_MAP=.nfsd.passwd
+DEFAULT_GROUP_MAP=.nfsd.group
+
+#
+# Network IP map file
+#
+DEFAULT_IP_MAP=.nfsd.hosts
+
+############################################################ GLOBALS
+
+VERSION='$Version: 6.1.3 $'
+
+pgm="${0##*/}" # Program basename
+
+#
+# Global exit status
+#
+SUCCESS=0
+FAILURE=1
+
+#
+# Command-line options
+#
+COLOR=1					# -a
+DEBUG=					# -d
+DEBUGGER=				# -D
+FILTER_CLIENT=				# -C ip
+FILTER_GROUP=				# -G group
+FILTER_USER=				# -U user
+GROUP_MAP="$DEFAULT_GROUP_MAP"		# -P file
+INTERVAL=$DEFAULT_INTERVAL		# -i sec
+IP_MAP="$DEFAULT_IP_MAP"		# -I file
+NO_NAMES=				# -n
+NSAMPLES=				# -N num
+OUTPUT_JSON=				# -j
+PASSWD_MAP="$DEFAULT_PASSWD_MAP"	# -p file
+QUIET=					# -q
+RAW_VIEW=				# -r
+REDACT=${NFSDTOP_REDACT:+1}		# -R
+SHOW_BYTES=				# -b
+VIEW_CLIENT=				# -c
+VIEW_GROUP=				# -g
+VIEW_USER=				# -u (default)
+WIDE_VIEW=				# -w
+
+#
+# Miscellaneous
+#
+CONS=1
+[ -t 1 ] || CONS= COLOR= # stdout is not a tty
+_FILTER_CLIENT=
+_FILTER_GROUP=
+_FILTER_USER=
+INTERVAL_PROBE= # Calculated
+INTERVAL_SECONDS= # Raw value for awk
+VIEW=
+
+############################################################ FUNCTIONS
+
+die()
+{
+	local fmt="$1"
+	if [ "$fmt" ]; then
+		shift 1 # fmt
+		printf "%s: $fmt\n" "$pgm" "$@" >&2
+	fi
+	exit $FAILURE
+}
+
+usage()
+{
+	local fmt="$1"
+	local optfmt="\t%-11s %s\n"
+
+	exec >&2
+	if [ "$fmt" ]; then
+		shift 1 # fmt
+		printf "%s: $fmt\n" "$pgm" "$@"
+	fi
+
+	printf "Usage: %s [OPTIONS]\n" "$pgm"
+	printf "Options:\n"
+	printf "$optfmt" "-a" "Always enable color."
+	printf "$optfmt" "-b" "Show bytes instead of bandwidth."
+	printf "$optfmt" "-C ip" "Client filter (IPv4 only)."
+	printf "$optfmt" "-c" "View read/write activity by client."
+	printf "$optfmt" "-D" "Enable debugger."
+	printf "$optfmt" "-d" "Debug. Print dtrace script and exit."
+	printf "$optfmt" "-G group" "Group filter (name or id)."
+	printf "$optfmt" "-g" "View read/write activity by group."
+	printf "$optfmt" "-h" "Print usage statement and exit."
+	printf "$optfmt" "-I file" "IP map file. Default \`$DEFAULT_IP_MAP'."
+	printf "$optfmt" "-i sec" \
+		"Set interval seconds. Default \`$DEFAULT_INTERVAL'."
+	printf "$optfmt" "-J" "Output most JSON data. Same as \`-jcgu'."
+	printf "$optfmt" "-j" "Output JSON formatted data."
+	printf "$optfmt" "-N num" "Perform num samples and exit."
+	printf "$optfmt" "-n" "Do not attempt to map uid/gid/ip to names."
+	printf "$optfmt" "-o" "Force non-console output."
+	printf "$optfmt" "-P file" \
+		"Group map file. Default \`$DEFAULT_GROUP_MAP'."
+	printf "$optfmt" "-p file" \
+		"User map file. Default \`$DEFAULT_PASSWD_MAP'."
+	printf "$optfmt" "-q" "Quiet. Hide informational messages."
+	printf "$optfmt" "-R" "Redact potentially sensitive information."
+	printf "$optfmt" "-r" "Raw view. Do not format output of dtrace."
+	printf "$optfmt" "-U user" "User filter (name or id)."
+	printf "$optfmt" "-u" "View read/write activity by user (default)."
+	printf "$optfmt" "-v" "Print version and exit."
+	printf "$optfmt" "-w" "Wide view. Maximize width of first column."
+
+	die
+}
+
+run_dtrace()
+{
+	if [ "$DEBUG" ]; then
+		cat
+		return
+	fi
+
+	dtrace -s /dev/stdin "$@"
+}
+
+isint()
+{
+	local arg="${1#-}"
+	[ "${arg:-x}" = "${arg%[!0-9]*}" ]
+}
+
+isip()
+{
+        local IFS=. noctets=0 octet
+        for octet in $1; do
+                [ "$octet" ] || return 2
+                isint "$octet" || return 3
+                [ $octet -ge 0 ] || return 4
+                [ $octet -gt 255 ] && return 5
+                noctets=$(( $noctets + 1 ))
+        done
+        [ $noctets -eq 4 ]
+}
+
+checkip()
+{
+	isip "$1"
+	case $? in
+	1) die "-C argument \`%s' too many dots" "$1" ;;
+	2) die "-C argument \`%s' missing octet" "$1" ;;
+	3) die "-C argument \`%s' bad octet" "$1" ;;
+	4) die "-C argument \`%s' negative octet" "$1" ;;
+	5) die "-C argument \`%s' too big octet" "$1" ;;
+	esac
+}
+
+#
+# ST: CALLS
+#
+
+send_user()
+{
+	local type="$1"
+	shift 1 # type
+	printf "%s|%s\n" "$type" "$*"
+}
+
+info() { send_user info "$*"; }
+
+resize()
+{
+	local size
+	if [ -e /dev/tty ]; then
+		size=$( { stty size < /dev/tty; } 2> /dev/null )
+	else
+		size=$( stty size 2> /dev/null )
+	fi
+	send_user resize "${size:-24 80}"
+}
+
+############################################################ MAIN
+
+#
+# Process command-line options
+#
+while getopts abC:cDdG:ghI:i:JjN:noP:p:qRrU:uvw flag; do
+	case "$flag" in
+	a) COLOR=1 ;;
+	b) SHOW_BYTES=1 ;;
+	C) FILTER_CLIENT="$OPTARG" ;;
+	c) VIEW=CLIENT VIEW_CLIENT=1 ;;
+	D) DEBUGGER=1 RAW_VIEW=1 ;;
+	d) DEBUG=1 RAW_VIEW=1 ;;
+	G) FILTER_GROUP="$OPTARG" ;;
+	g) VIEW=GROUP VIEW_GROUP=1 ;;
+	I) IP_MAP="$OPTARG" ;;
+	i) INTERVAL="$OPTARG" ;;
+	J) VIEW=JSON OUTPUT_JSON=1 \
+		VIEW_CLIENT=1 VIEW_GROUP=1 VIEW_USER=1 ;;
+	j) OUTPUT_JSON=1 ;;
+	N) [ "$OPTARG" ] || usage "-N option requires an argument" # NOTREACHED
+		NSAMPLES="$OPTARG" ;;
+	n) NO_NAMES=1 ;;
+	o) CONS= COLOR= ;;
+	P) GROUP_MAP="$OPTARG" ;;
+	p) PASSWD_MAP="$OPTARG" ;;
+	q) QUIET=1 ;;
+	R) REDACT=1 ;;
+	r) RAW_VIEW=1 ;;
+	U) FILTER_USER="$OPTARG" ;;
+	u) VIEW=USER VIEW_USER=1 ;;
+	v) VERSION="${VERSION#*: }"
+		echo "${VERSION% $}"
+		exit $SUCCESS ;;
+	w) WIDE_VIEW=1 ;;
+	*) usage # NOTREACHED
+	esac
+done
+shift $(( $OPTIND - 1 ))
+
+#
+# Process command-line arguments
+#
+[ $# -eq 0 ] || usage "Too many arguments" # NOTREACHED
+
+#
+# Prevent non-functional option combinations
+#
+if [ "$SHOW_BYTES" ]; then
+	[ "$VIEW" != "JSON" ] || die "-b cannot be combined with -J"
+	[ ! "$OUTPUT_JSON" ] || die "-b cannot be combined with -j"
+fi
+
+#
+# Silently ignore previous view options unless JSON output
+#
+[ "$VIEW" ] || VIEW=USER VIEW_USER=1
+if [ ! "$OUTPUT_JSON" ]; then
+	case "$VIEW" in # ST: VIEWS
+	CLIENT) VIEW_GROUP= VIEW_USER= ;;
+	GROUP) VIEW_CLIENT= VIEW_USER= ;;
+	USER) VIEW_CLIENT= VIEW_GROUP= ;;
+	esac
+fi
+
+#
+# Validate `-i sec' option
+#
+case "$INTERVAL" in
+"") usage "missing -i argument" ;; # NOTREACHED
+0) die "-i sec must be non-zero" ;;
+*[!0-9.]*|*.*.*|.) die "-i sec must be a number" ;;
+*.*)
+	INTERVAL_SECONDS=$INTERVAL
+	ms=$( echo "$INTERVAL * 1000" | bc )
+	ms="${ms%%.*}"
+
+	#
+	# If, after multiplying by 1000 to convert sec to msec,
+	# the leading [non-decimal] digit is either missing or zero,
+	# the input was too small to produce timing of at least 1 msec
+	#
+	case "$ms" in
+	""|0) die "-i sec must be at least 0.001" ;;
+	esac
+
+	INTERVAL_PROBE=profile:::tick-${ms}ms
+	;;
+*)
+	INTERVAL_SECONDS=$INTERVAL
+	INTERVAL_PROBE=profile:::tick-${INTERVAL_SECONDS}s
+esac
+
+#
+# Validate `-N num' option
+#
+case "$NSAMPLES" in
+0) die "-N num must be non-zero" ;;
+*[!0-9]*) die "-N num must be a positive integer" ;;
+esac
+
+#
+# Process `-G group'/`-U user' option
+#
+case "$FILTER_GROUP" in
+"") : leave-empty ;;
+*[!0-9]*) # Translate from name to GID
+	_FILTER_GROUP=$( awk \
+		-v sq="'" \
+		-v group_map="$GROUP_MAP" \
+		-v name="$FILTER_GROUP" \
+	'BEGIN {
+		delete name2gid
+		while (getline < group_map > 0) {
+			n = split($0, fields, /:/)
+			name2gid[fields[1]] = fields[3]
+		}
+		close(group_map)
+		if (name in name2gid) {
+			print name2gid[name]
+			exit 0
+		}
+		gsub(sq, "&\\\\&&", name)
+		cmd = sprintf("getent group %s%s%s", sq, name, sq)
+		cmd | getline group
+		close(cmd)
+		if (split(group, fields, /:/) >= 3) {
+			print fields[3]
+			exit 0
+		}
+		exit 1
+	}' 2> /dev/null ) || die "Unknown group %s" "$FILTER_GROUP"
+	FILTER_GROUP="$_FILTER_GROUP"
+	;;
+esac
+case "$FILTER_USER" in
+"") : leave-empty ;;
+*[!0-9]*) # Translate from name to UID
+	_FILTER_USER=$( awk \
+		-v sq="'" \
+		-v user_map="$PASSWD_MAP" \
+		-v name="$FILTER_USER" \
+	'BEGIN {
+		delete uid2name
+		while (getline < user_map > 0) {
+			n = split($0, fields, /:/)
+			name2uid[fields[1]] = fields[3]
+		}
+		close(user_map)
+		if (name in name2uid) {
+			print name2uid[name]
+			exit 0
+		}
+		gsub(sq, "&\\\\&&", name)
+		cmd = sprintf("getent passwd %s%s%s", sq, name, sq)
+		cmd | getline passwd
+		close(cmd)
+		if (split(passwd, fields, /:/) >= 3) {
+			print fields[3]
+			exit 0
+		}
+		exit 1
+	}' ) || die "Unknown user %s" "$FILTER_USER"
+	FILTER_USER="$_FILTER_USER"
+	;;
+esac
+
+#
+# Process `-C ip' option
+#
+case "$FILTER_CLIENT" in
+"") : ok ;;
+*[a-zA-Z]*)
+	_FILTER_CLIENT=$( awk \
+		-v sq="'" \
+		-v ip_map="$IP_MAP" \
+		-v host="$FILTER_CLIENT" \
+	'BEGIN {
+		delete host2ip
+		while (getline < ip_map > 0) {
+			if (/^[[:space:]]*(#|$)/) continue
+			n = split($0, fields, /[[:space:]]+/)
+			host2ip[fields[2]] = fields[1]
+		}
+		close(ip_map)
+		if (host in host2ip) {
+			print host2ip[host]
+			exit 0
+		} else if (host !~ /\.$/ && (host ".") in host2ip) {
+			print host2ip[host "."]
+			exit 0
+		}
+		gsub(sq, "&\\\\&&", host)
+		cmd = sprintf("host -t A %s%s%s", sq, host, sq)
+		cmd | getline
+		close(cmd)
+		if ($NF !~ /NXDOMAIN/) {
+			print $NF
+			exit 0
+		}
+		exit 1
+	}' 2> /dev/null ) || die "Unknown host %s" "$FILTER_CLIENT"
+	FILTER_CLIENT="$_FILTER_CLIENT"
+	checkip "$FILTER_CLIENT" # NOTREACHED if non-ip
+	;;
+*)
+	checkip "$FILTER_CLIENT" # NOTREACHED if non-ip
+	;;
+esac
+
+#
+# Get terminal size
+#
+size=$( resize )
+size="${size#*|}"
+if [ "$size" ]; then
+	cols="${size#*[$IFS]}"
+	rows="${size%%[$IFS]*}"
+fi
+case "$rows$cols" in
+""|*[!0-9]*)
+	cols=80
+	rows=24
+	;;
+esac
+
+#
+# Run script
+# ST: PRE
+#
+{
+	trap resize WINCH # ST: SIGWINCH
+
+	#
+	# Start background dtrace
+	# NB: M-x package-install [RET] dtrace-script-mode [RET]
+	# ST: DTRACE
+	#
+	run_dtrace <<EOF &
+	${DEBUG:+#!/usr/sbin/dtrace -s}
+	/* -*- mode: dtrace-script; tab-width: 4 -*- ;; Emacs
+	 * vi: set ft=dtrace noet ts=4 sw=4 :: Vi/ViM
+	 */
+	/**************************** PRAGMAS ****************************/
+
+	#pragma D option quiet
+
+	/***************************** TYPES *****************************/
+
+	typedef struct sainfo {
+		string addr;
+	} sainfo_t;
+
+	/**************************** GLOBALS ****************************/
+
+	inline int sa_data_size = 14;
+	inline char *sa_dummy_data = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
+
+	this sainfo_t	sainfo;
+
+	/************************* MAP FUNCTIONS *************************/
+
+	inline string sa_data_addr[sa_family_t af, char data[sa_data_size]] =
+		af == AF_INET ? strjoin(
+			strjoin(strjoin(lltostr(data[2] & 0xFF), "."),
+				strjoin(lltostr(data[3] & 0xFF), ".")
+			),
+			strjoin(strjoin(lltostr(data[4] & 0xFF), "."),
+				lltostr(data[5] & 0xFF))
+		) : "";
+
+	/* ST: OPS */
+
+	inline string opfunc[string func] =
+		func == "nfsrvd_read" ?		"read" :
+		func == "nfsrvd_write" ?	"write" :
+		"";
+
+	/************************** TRANSLATORS **************************/
+
+	translator sainfo_t < struct sockaddr *SA > {
+		addr = SA == NULL ?
+			sa_data_addr[0, sa_dummy_data] :
+			sa_data_addr[SA->sa_family, SA->sa_data];
+	};
+
+	/***************************** BEGIN *****************************/
+
+	BEGIN
+	{
+		printf("time|%d\n", walltimestamp / 1000000000);
+		printf("init|"); /* Initialize post-processor */
+		printf("===\n"); /* Clear screen and draw header */
+		printf("info|Sampling data for ${INTERVAL}s (\`-i sec')...\n");
+	}
+
+	/************************** READ PROBES **************************/
+
+	fbt:kernel:nfsrvd_read:entry
+	{
+		this->nfsd_read_nd = (struct nfsrv_descript *)args[0];
+		this->nfsd_read_cred = this->nfsd_read_nd == NULL ? NULL :
+			this->nfsd_read_nd->nd_cred;
+		this->nfsd_read_nam = this->nfsd_read_nd == NULL ? NULL :
+			this->nfsd_read_nd->nd_nam;
+		this->nfsd_read_info = xlate <sainfo_t>
+			((struct sockaddr *)this->nfsd_read_nam);
+	}
+
+	fbt:kernel:nfsvno_read:entry
+	/this->nfsd_read_nd != NULL/
+	{
+		this->uid = this->nfsd_read_cred->cr_ruid;
+		this->gid = this->nfsd_read_cred->cr_rgid;
+		this->bytes = args[2];
+
+		this->client_ip4 = this->nfsd_read_info.addr;
+
+		/* ST: FILTERS */
+
+		this->nfsd_read_unfiltered = ${FILTER_CLIENT:+
+			this->client_ip4 == "$FILTER_CLIENT" ?
+			}${FILTER_GROUP:+
+			this->gid == $FILTER_GROUP ?
+			}${FILTER_USER:+
+			this->uid == $FILTER_USER ?
+			}
+			1${FILTER_CLIENT:+
+			: 0}${FILTER_GROUP:+
+			: 0}${FILTER_USER:+
+			: 0};
+	}
+
+	fbt:kernel:nfsvno_read:entry
+	/this->nfsd_read_nd != NULL && this->nfsd_read_unfiltered/
+	{
+		/* ST: VIEWS TRACEDATA */
+${VIEW_CLIENT:+
+		@rd_client["@rd_client", this->client_ip4] =
+			sum(this->bytes);
+}
+
+${VIEW_GROUP:+
+		@rd_group["@rd_group", this->gid] = sum(this->bytes);
+}
+
+${VIEW_USER:+
+		@rd_user["@rd_user", this->uid] = sum(this->bytes);
+}
+	}
+
+	/************************* WRITE  PROBES *************************/
+
+	fbt:kernel:nfsrvd_write:entry
+	{
+		this->nfsd_write_nd = (struct nfsrv_descript *)args[0];
+		this->nfsd_write_cred = this->nfsd_write_nd == NULL ? NULL :
+			this->nfsd_write_nd->nd_cred;
+		this->nfsd_write_nam = this->nfsd_write_nd == NULL ? NULL :
+			this->nfsd_write_nd->nd_nam;
+		this->nfsd_write_info = xlate <sainfo_t>
+			((struct sockaddr *)this->nfsd_write_nam);
+	}
+
+	fbt:kernel:nfsvno_write:entry
+	/this->nfsd_write_nd != NULL/
+	{
+		this->uid = this->nfsd_write_cred->cr_ruid;
+		this->gid = this->nfsd_write_cred->cr_rgid;
+		this->bytes = args[2];
+
+		this->client_ip4 = this->nfsd_write_info.addr;
+
+		this->filtered = 0;
+
+		/* ST: FILTERS */
+
+		this->nfsd_write_unfiltered = ${FILTER_CLIENT:+
+			this->client_ip4 == "$FILTER_CLIENT" ?
+			}${FILTER_GROUP:+
+			this->gid == $FILTER_GROUP ?
+			}${FILTER_USER:+
+			this->uid == $FILTER_USER ?
+			}
+			1${FILTER_CLIENT:+
+			: 0}${FILTER_GROUP:+
+			: 0}${FILTER_USER:+
+			: 0};
+	}
+
+	fbt:kernel:nfsvno_write:entry
+	/this->nfsd_write_nd != NULL && this->nfsd_write_unfiltered/
+	{
+		/* ST: VIEWS TRACEDATA */
+${VIEW_CLIENT:+
+		@wr_client["@wr_client", this->client_ip4] =
+			sum(this->bytes);
+}
+
+${VIEW_GROUP:+
+		@wr_group["@wr_group", this->gid] = sum(this->bytes);
+}
+
+${VIEW_USER:+
+		@wr_user["@wr_user", this->uid] = sum(this->bytes);
+}
+	}
+
+	/************************ INTERVAL  PROBE ************************/
+
+	$INTERVAL_PROBE
+	{
+		printf("time|%d\n", walltimestamp / 1000000000);
+		printf("===\n");
+
+${VIEW_CLIENT:+
+		printa(@rd_client);
+		printa(@wr_client);
+		trunc(@rd_client);
+		trunc(@wr_client);
+}
+${VIEW_GROUP:+
+		printa(@rd_group);
+		printa(@wr_group);
+		trunc(@rd_group);
+		trunc(@wr_group);
+}
+${VIEW_USER:+
+		printa(@rd_user);
+		printa(@wr_user);
+		trunc(@rd_user);
+		trunc(@wr_user);
+}
+
+		printf("---\n");
+	}
+
+	/****************************** END ******************************/
+
+	END
+	{
+${VIEW_CLIENT:+
+		trunc(@rd_client);
+		trunc(@wr_client);
+}
+${VIEW_GROUP:+
+		trunc(@rd_group);
+		trunc(@wr_group);
+}
+${VIEW_USER:+
+		trunc(@rd_user);
+		trunc(@wr_user);
+}
+	}
+
+	/********************************************************************\
+	 * END
+	 ********************************************************************/
+EOF
+	pid=$!
+
+	#
+	# Identify child dtrace
+	#
+	if [ ! "$DEBUG" ]; then
+		info "Waiting for dtrace to initialize..."
+		while kill -0 $pid 2> /dev/null; do
+			bpid=$( pgrep -P $pid dtrace ) && break
+			sleep 1
+		done
+		if ! kill -0 $pid 2> /dev/null; then
+			wait $pid > /dev/null 2>&1 # Collect exit status
+			echo EXIT:$? # Send status to post-processor
+			exit
+		fi
+	fi
+
+	#
+	# Wait on background (dtrace) child
+	#
+	status_collected=
+	while kill -0 $bpid > /dev/null 2>&1; do
+		wait > /dev/null 2>&1 # Collect exit status
+		[ "$status_collected" ] || status_collected=$?
+	done
+	echo EXIT:$status_collected # Send status to post-processor
+
+} | awk -v color=${COLOR:-0} \
+	-v cols=$cols \
+	-v cons=${CONS:-0} \
+	-v debug=${DEBUG:-0} \
+	-v debugger=${DEBUGGER:-0} \
+	-v group_map="$GROUP_MAP" \
+	-v interval=$INTERVAL_SECONDS \
+	-v ip_map="$IP_MAP" \
+	-v no_names=${NO_NAMES:-0} \
+	-v nsamples=${NSAMPLES:-0} \
+	-v output_json=${OUTPUT_JSON:-0} \
+	-v quiet=${QUIET:-0} \
+	-v passwd_map="$PASSWD_MAP" \
+	-v raw_view=${RAW_VIEW:-0} \
+	-v redact=${REDACT:-0} \
+	-v rows=$rows \
+	-v show_bytes=${SHOW_BYTES:-0} \
+	-v stderr=/dev/stderr \
+	-v tm=$( date +%s ) \
+	-v view="$VIEW" \
+	-v wide_view=${WIDE_VIEW:-0} \
+	'####################################### BEGIN
+
+	# ST: POST
+
+	BEGIN {
+		debug2("Terminal size (rows, cols) = (%d, %d)", rows, cols)
+
+		exit_status = 0 # SUCCESS
+		time_delta = 0 # Calculated
+		samples_left = nsamples
+
+		inv	= "\033[7m"
+		noinv	= "\033[27m"
+		bold    = "\033[1m"
+		nobold  = "\033[22m"
+		red     = "\033[31m"
+		green   = "\033[32m"
+		yellow  = "\033[33m"
+		magenta = "\033[35m"
+		cyan    = "\033[36m"
+		fgreset = "\033[39m"
+
+		# Obtain current process (awk) pid
+		(cmd = "echo $PPID") | getline apid
+		close(cmd)
+
+		# Obtain parent process (sh) pid
+		getline stat < (file = sprintf("/proc/%d/stat", apid))
+		close(file)
+		split(stat, st)
+		spid = st[4]
+
+		# Obtain parent process (sh) name
+		getline stat < (file = sprintf("/proc/%d/stat", spid))
+		close(file)
+		split(stat, st)
+		comm = st[2]
+		if (match(comm, /^\(.*\)$/))
+			comm = substr(comm, 2, length(comm) - 2)
+
+		# Obtain child (sh) pid
+		(cmd = sprintf("pgrep -P %d %s", spid, comm)) | getline cpid
+		close(cmd)
+
+		if (!raw_view) {
+			clear_data()
+			resize()
+			if (!no_names) load_files()
+		}
+
+		# Declare arrays
+		delete times
+
+		if (redact) {
+			m = "^(USER|total|"
+			if ((u = ENVIRON["USER"]) != "")
+				m = m u "|"
+			if ((s = ENVIRON["SUDO_USER"]) != "" && s != u)
+				m = m s "|"
+			cmd = "getent passwd 2> /dev/null"
+			while (cmd | getline > 0) {
+				if (split($0, f, /:/) < 3) continue
+				if (f[3] > 1024 && f[3] !~ /^6553[456]$/)
+					continue
+				m = m f[1] "|"
+			}
+			close(cmd)
+			m = m "\\*)$"
+			unredacted_users = m
+
+			m = "^(GROUP|total|"
+			cmd = "getent group 2> /dev/null"
+			while (cmd | getline > 0) {
+				if (split($0, f, /:/) < 3) continue
+				if (f[3] > 1024 && f[3] !~ /^6553[456]$/)
+					continue
+				m = m f[1] "|"
+			}
+			close(cmd)
+			m = m "\\*)$"
+			unredacted_groups = m
+
+			unredacted_clients = "^(CLIENT|total|\\*)$"
+		}
+	}
+
+	######################################## FUNCTIONS
+
+	function dtrace_init()
+	{
+		# Obtain handler (sh) pid
+		(cmd = sprintf("pgrep -P %d %s", cpid, comm)) | getline wpid
+
+		# Obtain dtrace pid
+		cmd = sprintf("pgrep -P %d dtrace", wpid)
+		cmd | getline bpid
+		close(cmd)
+	}
+
+	function debug1(fmt,a1) { if (debugger) printf fmt "\n", a1 }
+	function debug2(fmt,a1,a2) { if (debugger) printf fmt "\n", a1, a2 }
+	function debug3(fmt,a1,a2,a3) {
+		if (debugger) printf fmt "\n", a1, a2, a3
+	}
+	function buffer_add(text) { BUFFER = BUFFER text }
+
+	function print_buffer()
+	{
+		if (!cons && !output_json) buffer_add("\n")
+		printf "%s", BUFFER
+		fflush()
+	}
+
+	function info(str)
+	{
+		if (quiet) return
+		printf "%sINFO%s %s\n", color ? magenta : "",
+			color ? fgreset : "", str > stderr
+		fflush(stderr)
+	}
+
+	function get_random(len,        c, n, r, rdata, rfile, rlen)
+	{
+		if (len < 1) return ""
+		rlen = 0
+		rdata = ""
+		rfile = "/dev/urandom"
+		while (length(rdata) < len && getline r < rfile > 0) {
+			for (n = split(r, c, ""); n >= 1; n--) {
+				if (c[n] !~ /[\x41-\x5a]/) continue
+				rdata = rdata c[n]
+				if (++rlen == len) break
+			}
+		}
+		close(rfile)
+		return rdata
+	}
+
+	function resize(        dsz, vsz, vsz_fixed, bar_size_fixed,
+		bar_size_fixed_max, bar_size_fixed_min, bar_min1, bar_min2,
+		vsz_cols1, vsz_cols2, vsz_max, vsz_min, wv)
+	{
+		if (output_json) return
+
+		#
+		# Calculate columns and column widths
+		# ST: VIEWS
+		#
+		# NB: bar_size = size of bar column (if shown)
+		# NB: dsz = size of TOTAL, READ(OUT), WRITE(IN) data columns
+		# NB: vsz = size of VIEW column ("view size")
+		#
+		# If given -w (wide view) make bar_size fixed-width and
+		# vsz variable-width.
+		#
+		# Without -w, make vsz fixed-width and bar_size variable.
+		#
+
+		wv = wide_view
+
+		show_bar_column = 1
+		show_rw_columns = 1
+
+		vsz_min = length(view)
+		vsz_max = 15
+
+		dsz = show_bytes ? 10 : 12
+		bar_size_fixed_max = 21
+		bar_size_fixed_min = 11
+
+		#
+		# Calculate minimum terminal width required (bar_min1)
+		# to display small bar (bar_size_fixed_min) and also
+		# minimum terminal width required (bar_min2) to display
+		# larger bar (bar_size_fixed_max).
+		#
+		bar_min1 = 0
+		bar_min1 += vsz_min + 1 # VIEW + space
+		bar_min1 += dsz + 1 # TOTAL + space
+		vsz_cols2 = bar_min1
+		bar_min1 += dsz + 1 # WRITE(IN) + space
+		vsz_cols1 = bar_min2 = bar_min1
+		bar_min1 += bar_size_fixed_min + 1 # small bar + space
+		bar_min2 += bar_size_fixed_max + 1 # bigger bar + space
+		bar_min1 += dsz # READ(OUT)
+		bar_min2 += dsz # READ(OUT)
+		vsz_cols1 += dsz # READ(OUT)
+
+		#
+		# Calculate fixed bar width based on terminal width
+		# NB: Only used in wide-view (-w)
+		# NB: If terminal is too narrow, disable bar/columns
+		#
+		if (cols >= bar_min2) {
+			bar_size_fixed = bar_size_fixed_max
+		} else if (cols >= bar_min1) {
+			bar_size_fixed = bar_size_fixed_min
+		} else {
+			show_bar_column = 0
+			bar_size_fixed = 0
+		}
+
*** 950 LINES SKIPPED ***