#
# Copyright (C) 2017-2019 Marko Myllynen <myllynen@redhat.com>
# Based on the biolatency BCC tool by Brendan Gregg:
# https://github.com/iovisor/bcc/blob/master/tools/biolatency.py
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
""" PCP BCC PMDA biolatency module """

# pylint: disable=invalid-name, line-too-long

from bcc import BPF

from pcp.pmapi import pmUnits
from cpmapi import PM_TYPE_U64, PM_SEM_COUNTER, PM_COUNT_ONE
from cpmda import PMDA_FETCH_NOVALUES

from modules.pcpbcc import PCPBCCBase

#
# BPF program
#
bpf_src = "modules/biolatency.bpf"

#
# PCP BCC PMDA constants
#
MODULE = 'biolatency'
METRIC = 'disk.all.latency'
units_count = pmUnits(0, 0, 1, 0, 0, PM_COUNT_ONE)

#
# PCP BCC Module
#
class PCPBCCModule(PCPBCCBase):
    """ PCP BCC biolatency module """
    def __init__(self, config, log, err, _):
        """ Constructor """
        PCPBCCBase.__init__(self, MODULE, config, log, err)

        self.cache = {}
        self.queued = False

        for opt in self.config.options(MODULE):
            if opt == 'queued':
                self.queued = self.config.getboolean(MODULE, opt)

        if self.queued:
            self.log("Including OS queued time in I/O time.")
        else:
            self.log("Excluding OS queued time from I/O time.")

        self.log("Initialized.")

    def metrics(self):
        """ Get metric definitions """
        name = METRIC
        self.items.append(
            # Name - reserved - type - semantics - units - help
            (name, None, PM_TYPE_U64, PM_SEM_COUNTER, units_count, 'block io latency distribution'),
        )
        return True, self.items

    def compile(self):
        """ Compile BPF """
        try:
            self.bpf = BPF(src_file=bpf_src)
            if self.queued:
                # Compat: bcc < 0.6.0 (first check)
                if 'get_kprobe_functions' not in dir(self.bpf) or \
                   self.get_kprobe_functions(b"blk_start_request"):
                    self.bpf.attach_kprobe(event="blk_start_request", fn_name="trace_req_start")
                self.bpf.attach_kprobe(event="blk_mq_start_request", fn_name="trace_req_start")
            else:
                self.bpf.attach_kprobe(event="blk_account_io_start", fn_name="trace_req_start")
            self.bpf.attach_kprobe(event="blk_account_io_done", fn_name="trace_req_done")
            self.log("Compiled.")
        except Exception as error: # pylint: disable=broad-except
            self.bpf = None
            self.err(str(error))
            self.err("Module NOT active!")
            raise

    def refresh(self):
        """ Refresh BPF data """
        if self.bpf is None:
            return None

        dist = self.bpf["dist"]
        self.insts = self.read_log2_histogram(dist, self.cache)
        dist.clear()

        return self.insts

    def bpfdata(self, item, inst):
        """ Return BPF data as PCP metric value """
        try:
            key = self.pmdaIndom.inst_name_lookup(inst)
            return [self.cache[key], 1]
        except Exception: # pylint: disable=broad-except
            return [PMDA_FETCH_NOVALUES, 0]

    def label_indom(self):
        """ Instance domain labels """
        return '{"statistic":"histogram"}'

    def label_instance(self, inst):
        """ Instance labels """
        key = self.pmdaIndom.inst_name_lookup(inst)
        bounds = key.split("-")
        return '{"lower_bound":%s,"upper_bound":%s}' % (bounds[0], bounds[1])
