#!/usr/bin/env python3
#
# Print out ZFS ARC Statistics exported via kstat(1)
# For a definition of fields, or usage, use arctstat.pl -v
#
# This script is a fork of the original arcstat.pl (0.1) by
# Neelakanth Nadgir, originally published on his Sun blog on
# 09/18/2007
#     http://blogs.sun.com/realneel/entry/zfs_arc_statistics
#
# This version aims to improve upon the original by adding features
# and fixing bugs as needed.  This version is maintained by
# Mike Harsch and is hosted in a public open source repository:
#    http://github.com/mharsch/arcstat
#
# Comments, Questions, or Suggestions are always welcome.
# Contact the maintainer at ( mike at harschsystems dot com )
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License, Version 1.0 only
# (the "License").  You may not use this file except in compliance
# with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
#
# Fields have a fixed width. Every interval, we fill the "v"
# hash with its corresponding value (v[field]=value) using calculate().
# @hdr is the array of fields that needs to be printed, so we
# just iterate over this array and print the values using our pretty printer.
#
# This script must remain compatible with Python 2.6+ and Python 3.4+.
#

import sys
import time
import getopt
import re
import copy

from decimal import Decimal
from signal import signal, SIGINT, SIGWINCH, SIG_DFL


cols = {
    # HDR:        [Size, Scale, Description]
    "time":       [8, -1, "Time"],
    "hits":       [4, 1000, "ARC reads per second"],
    "miss":       [4, 1000, "ARC misses per second"],
    "read":       [4, 1000, "Total ARC accesses per second"],
    "hit%":       [4, 100, "ARC Hit percentage"],
    "miss%":      [5, 100, "ARC miss percentage"],
    "dhit":       [4, 1000, "Demand hits per second"],
    "dmis":       [4, 1000, "Demand misses per second"],
    "dh%":        [3, 100, "Demand hit percentage"],
    "dm%":        [3, 100, "Demand miss percentage"],
    "phit":       [4, 1000, "Prefetch hits per second"],
    "pmis":       [4, 1000, "Prefetch misses per second"],
    "ph%":        [3, 100, "Prefetch hits percentage"],
    "pm%":        [3, 100, "Prefetch miss percentage"],
    "mhit":       [4, 1000, "Metadata hits per second"],
    "mmis":       [4, 1000, "Metadata misses per second"],
    "mread":      [5, 1000, "Metadata accesses per second"],
    "mh%":        [3, 100, "Metadata hit percentage"],
    "mm%":        [3, 100, "Metadata miss percentage"],
    "arcsz":      [5, 1024, "ARC Size"],
    "c":          [4, 1024, "ARC Target Size"],
    "mfu":        [4, 1000, "MFU List hits per second"],
    "mru":        [4, 1000, "MRU List hits per second"],
    "mfug":       [4, 1000, "MFU Ghost List hits per second"],
    "mrug":       [4, 1000, "MRU Ghost List hits per second"],
    "eskip":      [5, 1000, "evict_skip per second"],
    "mtxmis":     [6, 1000, "mutex_miss per second"],
    "dread":      [5, 1000, "Demand accesses per second"],
    "pread":      [5, 1000, "Prefetch accesses per second"],
    "l2hits":     [6, 1000, "L2ARC hits per second"],
    "l2miss":     [6, 1000, "L2ARC misses per second"],
    "l2read":     [6, 1000, "Total L2ARC accesses per second"],
    "l2hit%":     [6, 100, "L2ARC access hit percentage"],
    "l2miss%":    [7, 100, "L2ARC access miss percentage"],
    "l2asize":    [7, 1024, "Actual (compressed) size of the L2ARC"],
    "l2size":     [6, 1024, "Size of the L2ARC"],
    "l2bytes":    [7, 1024, "bytes read per second from the L2ARC"],
    "grow":       [4, 1000, "ARC Grow disabled"],
    "need":       [4, 1024, "ARC Reclaim need"],
    "free":       [4, 1024, "ARC Free memory"],
}

v = {}
hdr = ["time", "read", "miss", "miss%", "dmis", "dm%", "pmis", "pm%", "mmis",
       "mm%", "arcsz", "c"]
xhdr = ["time", "mfu", "mru", "mfug", "mrug", "eskip", "mtxmis", "dread",
        "pread", "read"]
sint = 1               # Default interval is 1 second
count = 1              # Default count is 1
hdr_intr = 20          # Print header every 20 lines of output
opfile = None
sep = "  "              # Default separator is 2 spaces
version = "0.4"
l2exist = False
cmd = ("Usage: arcstat [-hvx] [-f fields] [-o file] [-s string] [interval "
       "[count]]\n")
cur = {}
d = {}
out = None
kstat = None


if sys.platform.startswith('freebsd'):
    # Requires py27-sysctl on FreeBSD
    import sysctl

    def kstat_update():
        global kstat

        k = sysctl.filter('kstat.zfs.misc.arcstats')

        if not k:
            sys.exit(1)

        kstat = {}

        for s in k:
            if not s:
                continue

            name, value = s.name, s.value
            # Trims 'kstat.zfs.misc.arcstats' from the name
            kstat[name[24:]] = Decimal(value)

elif sys.platform.startswith('linux'):
    def kstat_update():
        global kstat

        k = [line.strip() for line in open('/proc/spl/kstat/zfs/arcstats')]

        if not k:
            sys.exit(1)

        del k[0:2]
        kstat = {}

        for s in k:
            if not s:
                continue

            name, unused, value = s.split()
            kstat[name] = Decimal(value)


def detailed_usage():
    sys.stderr.write("%s\n" % cmd)
    sys.stderr.write("Field definitions are as follows:\n")
    for key in cols:
        sys.stderr.write("%11s : %s\n" % (key, cols[key][2]))
    sys.stderr.write("\n")

    sys.exit(0)


def usage():
    sys.stderr.write("%s\n" % cmd)
    sys.stderr.write("\t -h : Print this help message\n")
    sys.stderr.write("\t -v : List all possible field headers and definitions"
                     "\n")
    sys.stderr.write("\t -x : Print extended stats\n")
    sys.stderr.write("\t -f : Specify specific fields to print (see -v)\n")
    sys.stderr.write("\t -o : Redirect output to the specified file\n")
    sys.stderr.write("\t -s : Override default field separator with custom "
                     "character or string\n")
    sys.stderr.write("\nExamples:\n")
    sys.stderr.write("\tarcstat -o /tmp/a.log 2 10\n")
    sys.stderr.write("\tarcstat -s \",\" -o /tmp/a.log 2 10\n")
    sys.stderr.write("\tarcstat -v\n")
    sys.stderr.write("\tarcstat -f time,hit%,dh%,ph%,mh% 1\n")
    sys.stderr.write("\n")

    sys.exit(1)


def snap_stats():
    global cur
    global kstat

    prev = copy.deepcopy(cur)
    kstat_update()

    cur = kstat
    for key in cur:
        if re.match(key, "class"):
            continue
        if key in prev:
            d[key] = cur[key] - prev[key]
        else:
            d[key] = cur[key]


def prettynum(sz, scale, num=0):
    suffix = [' ', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']
    index = 0
    save = 0

    # Special case for date field
    if scale == -1:
        return "%s" % num

    # Rounding error, return 0
    elif 0 < num < 1:
        num = 0

    while num > scale and index < 5:
        save = num
        num = num / scale
        index += 1

    if index == 0:
        return "%*d" % (sz, num)

    if (save / scale) < 10:
        return "%*.1f%s" % (sz - 1, num, suffix[index])
    else:
        return "%*d%s" % (sz - 1, num, suffix[index])


def print_values():
    global hdr
    global sep
    global v

    for col in hdr:
        sys.stdout.write("%s%s" % (
            prettynum(cols[col][0], cols[col][1], v[col]),
            sep
        ))
    sys.stdout.write("\n")
    sys.stdout.flush()


def print_header():
    global hdr
    global sep

    for col in hdr:
        sys.stdout.write("%*s%s" % (cols[col][0], col, sep))
    sys.stdout.write("\n")


def get_terminal_lines():
    try:
        import fcntl
        import termios
        import struct
        data = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, '1234')
        sz = struct.unpack('hh', data)
        return sz[0]
    except Exception:
        pass


def update_hdr_intr():
    global hdr_intr

    lines = get_terminal_lines()
    if lines and lines > 3:
        hdr_intr = lines - 3


def resize_handler(signum, frame):
    update_hdr_intr()


def init():
    global sint
    global count
    global hdr
    global xhdr
    global opfile
    global sep
    global out
    global l2exist

    desired_cols = None
    xflag = False
    hflag = False
    vflag = False
    i = 1

    try:
        opts, args = getopt.getopt(
            sys.argv[1:],
            "xo:hvs:f:",
            [
                "extended",
                "outfile",
                "help",
                "verbose",
                "separator",
                "columns"
            ]
        )
    except getopt.error as msg:
        sys.stderr.write("Error: %s\n" % str(msg))
        usage()
        opts = None

    for opt, arg in opts:
        if opt in ('-x', '--extended'):
            xflag = True
        if opt in ('-o', '--outfile'):
            opfile = arg
            i += 1
        if opt in ('-h', '--help'):
            hflag = True
        if opt in ('-v', '--verbose'):
            vflag = True
        if opt in ('-s', '--separator'):
            sep = arg
            i += 1
        if opt in ('-f', '--columns'):
            desired_cols = arg
            i += 1
        i += 1

    argv = sys.argv[i:]
    sint = Decimal(argv[0]) if argv else sint
    count = int(argv[1]) if len(argv) > 1 else count

    if len(argv) > 1:
        sint = Decimal(argv[0])
        count = int(argv[1])

    elif len(argv) > 0:
        sint = Decimal(argv[0])
        count = 0

    if hflag or (xflag and desired_cols):
        usage()

    if vflag:
        detailed_usage()

    if xflag:
        hdr = xhdr

    update_hdr_intr()

    # check if L2ARC exists
    snap_stats()
    l2_size = cur.get("l2_size")
    if l2_size:
        l2exist = True

    if desired_cols:
        hdr = desired_cols.split(",")

        invalid = []
        incompat = []
        for ele in hdr:
            if ele not in cols:
                invalid.append(ele)
            elif not l2exist and ele.startswith("l2"):
                sys.stdout.write("No L2ARC Here\n%s\n" % ele)
                incompat.append(ele)

        if len(invalid) > 0:
            sys.stderr.write("Invalid column definition! -- %s\n" % invalid)
            usage()

        if len(incompat) > 0:
            sys.stderr.write("Incompatible field specified! -- %s\n" %
                             incompat)
            usage()

    if opfile:
        try:
            out = open(opfile, "w")
            sys.stdout = out

        except IOError:
            sys.stderr.write("Cannot open %s for writing\n" % opfile)
            sys.exit(1)


def calculate():
    global d
    global v
    global l2exist

    v = dict()
    v["time"] = time.strftime("%H:%M:%S", time.localtime())
    v["hits"] = d["hits"] / sint
    v["miss"] = d["misses"] / sint
    v["read"] = v["hits"] + v["miss"]
    v["hit%"] = 100 * v["hits"] / v["read"] if v["read"] > 0 else 0
    v["miss%"] = 100 - v["hit%"] if v["read"] > 0 else 0

    v["dhit"] = (d["demand_data_hits"] + d["demand_metadata_hits"]) / sint
    v["dmis"] = (d["demand_data_misses"] + d["demand_metadata_misses"]) / sint

    v["dread"] = v["dhit"] + v["dmis"]
    v["dh%"] = 100 * v["dhit"] / v["dread"] if v["dread"] > 0 else 0
    v["dm%"] = 100 - v["dh%"] if v["dread"] > 0 else 0

    v["phit"] = (d["prefetch_data_hits"] + d["prefetch_metadata_hits"]) / sint
    v["pmis"] = (d["prefetch_data_misses"] +
                 d["prefetch_metadata_misses"]) / sint

    v["pread"] = v["phit"] + v["pmis"]
    v["ph%"] = 100 * v["phit"] / v["pread"] if v["pread"] > 0 else 0
    v["pm%"] = 100 - v["ph%"] if v["pread"] > 0 else 0

    v["mhit"] = (d["prefetch_metadata_hits"] +
                 d["demand_metadata_hits"]) / sint
    v["mmis"] = (d["prefetch_metadata_misses"] +
                 d["demand_metadata_misses"]) / sint

    v["mread"] = v["mhit"] + v["mmis"]
    v["mh%"] = 100 * v["mhit"] / v["mread"] if v["mread"] > 0 else 0
    v["mm%"] = 100 - v["mh%"] if v["mread"] > 0 else 0

    v["arcsz"] = cur["size"]
    v["c"] = cur["c"]
    v["mfu"] = d["mfu_hits"] / sint
    v["mru"] = d["mru_hits"] / sint
    v["mrug"] = d["mru_ghost_hits"] / sint
    v["mfug"] = d["mfu_ghost_hits"] / sint
    v["eskip"] = d["evict_skip"] / sint
    v["mtxmis"] = d["mutex_miss"] / sint

    if l2exist:
        v["l2hits"] = d["l2_hits"] / sint
        v["l2miss"] = d["l2_misses"] / sint
        v["l2read"] = v["l2hits"] + v["l2miss"]
        v["l2hit%"] = 100 * v["l2hits"] / v["l2read"] if v["l2read"] > 0 else 0

        v["l2miss%"] = 100 - v["l2hit%"] if v["l2read"] > 0 else 0
        v["l2asize"] = cur["l2_asize"]
        v["l2size"] = cur["l2_size"]
        v["l2bytes"] = d["l2_read_bytes"] / sint

    v["grow"] = 0 if cur["arc_no_grow"] else 1
    v["need"] = cur["arc_need_free"]
    v["free"] = cur["arc_sys_free"]


def main():
    global sint
    global count
    global hdr_intr

    i = 0
    count_flag = 0

    init()
    if count > 0:
        count_flag = 1

    signal(SIGINT, SIG_DFL)
    signal(SIGWINCH, resize_handler)
    while True:
        if i == 0:
            print_header()

        snap_stats()
        calculate()
        print_values()

        if count_flag == 1:
            if count <= 1:
                break
            count -= 1

        i = 0 if i >= hdr_intr else i + 1
        time.sleep(sint)

    if out:
        out.close()


if __name__ == '__main__':
    main()
