#!/usr/bin/env pmpython
'''
Performance Metrics Domain Agent exporting Linux compressed swap metrics.
'''
#
# Copyright (c) 2014-2015,2021 Red Hat.
#
# 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.
#
# pylint: disable=bad-continuation,line-too-long,too-many-lines,bare-except
#

import cpmapi as c_api
from pcp.pmapi import pmUnits
from pcp.pmda import PMDA, pmdaMetric
from cpmda import PMDA_FETCH_NOVALUES
from resource import getpagesize
from os import getenv, listdir
import sys
if sys.version >= '3':
    long = int	# python2 to python3 portability (no long() in python3)

ZSWAP_PAGESIZE = getpagesize()
ZSWAP_STATS_PATH = '/sys/kernel/debug/zswap'

class ZswapPMDA(PMDA):
    '''
    Performance Metrics Domain Agent exporting compressed swap metrics.
    Install it and make basic use of it if you use zswap, as follows:

    # $PCP_PMDAS_DIR/zswap/Install
    $ pminfo -fmdtT zswap
    '''

    def zswap_fetch_single(self, name):
        ''' Extract one value from a single zswap statistics file. '''
        fullpath = self.path + '/' + name
        try:
            with open(fullpath) as file:
                value = file.readline()
                self.values[name] = long(value)
        except:
            # use a simple approach to avoiding errors on every fetch
            if self.fileerrors < self.nmetrics:
                self.err("fetch_single: failed to read %s" % fullpath)
            self.fileerrors += 1

    def zswap_fetch(self):
        '''
        Called once per PCP "fetch" PDU from pmcd(1)
        Iterates over the (sysfs debug) path holding zswap statistics,
        building a hash of values (one value extracted from each file).
        '''
        # self.log("fetch: %s" % self.path)
        try:
            self.patherrors = 0
            files = listdir(self.path)
            for name in files:
                self.zswap_fetch_single(name)
        except:
            # the kernel module may simply not be loaded currently
            self.patherrors = 1


    def zswap_fetch_callback(self, cluster, item, inst):
        '''
        Main fetch callback - looks up value associated with requested PMID
        (from zswap_fetch), converts units if needed, then returns a list of
        value,status (single pair) for the requested pmid
        '''
        # self.log("fetch callback for %d.%d[%d]" % (cluster, item, inst))
        if inst != c_api.PM_IN_NULL:
            return [c_api.PM_ERR_INST, 0]
        elif cluster != 0:
            return [c_api.PM_ERR_PMID, 0]
        if 0 <= item < self.nmetrics and self.patherrors == 1:
            return [PMDA_FETCH_NOVALUES, 0]

        if item == 0:
            return [self.values['pool_limit_hit'], 1]
        elif item == 1:
            return [self.values['reject_reclaim_fail'], 1]
        elif item == 2:
            return [self.values['reject_alloc_fail'], 1]
        elif item == 3:
            return [self.values['reject_kmemcache_fail'], 1]
        elif item == 4:
            return [self.values['reject_compress_poor'], 1]
        elif item == 5:
            return [self.values['written_back_pages'] * self.pagesize, 1]
        elif item == 6:
            return [self.values['duplicate_entry'], 1]
        elif item == 7:
            return [self.values['pool_pages'] * self.pagesize, 1]
        elif item == 8:
            return [self.values['stored_pages'] * self.pagesize, 1]
        elif item == 9:
            return [self.values['pool_total_size'] * self.pagesize, 1]
        elif item == 10:
            return [self.values['same_filled_pages'] * self.pagesize, 1]
        return [c_api.PM_ERR_PMID, 0]


    def setup_zswap_metrics(self, name):
        '''
        Setup the metric table - ensure a values hash entry is setup for
        each metric; that way we don't need to worry about KeyErrors in
        the fetch callback routine (e.g. if the kernel interface changes).
        '''
        self.values['pool_limit_hit'] = 0
        self.add_metric(name + '.pool_limit_hit', pmdaMetric(
                self.pmid(0, 0),
                c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER,
                pmUnits(0, 0, 1, 0, 0, c_api.PM_COUNT_ONE)),
        'Pool limit was hit (see zswap_max_pool_percent module parameter)')

        self.values['reject_reclaim_fail'] = 0
        self.add_metric(name + '.reject_reclaim_fail', pmdaMetric(
                self.pmid(0, 1),
                c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER,
                pmUnits(0, 0, 1, 0, 0, c_api.PM_COUNT_ONE)),
        'Store failed due to a reclaim failure after pool limit was reached')

        self.values['reject_alloc_fail'] = 0
        self.add_metric(name + '.reject_alloc_fail', pmdaMetric(
                self.pmid(0, 2),
                c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER,
                pmUnits(0, 0, 1, 0, 0, c_api.PM_COUNT_ONE)),
        'Store failed because underlying allocator could not get memory')

        self.values['reject_kmemcache_fail'] = 0
        self.add_metric(name + '.reject_kmemcache_fail', pmdaMetric(
                self.pmid(0, 3),
                c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER,
                pmUnits(0, 0, 1, 0, 0, c_api.PM_COUNT_ONE)),
        'Store failed because the entry metadata could not be allocated (rare)')

        self.values['reject_compress_poor'] = 0
        self.add_metric(name + '.reject_compress_poor', pmdaMetric(
                self.pmid(0, 4),
                c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER,
                pmUnits(0, 0, 1, 0, 0, c_api.PM_COUNT_ONE)),
        'Compressed page was too big for the allocator to (optimally) store')

        self.values['written_back_pages'] = 0
        self.add_metric(name + '.written_back_pages', pmdaMetric(
                self.pmid(0, 5),
                c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER,
                pmUnits(1, 0, 0, c_api.PM_SPACE_KBYTE, 0, 0)),
        'Pages written back when pool limit was reached')

        self.values['duplicate_entry'] = 0
        self.add_metric(name + '.duplicate_entry', pmdaMetric(
                self.pmid(0, 6),
                c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER,
                pmUnits(0, 0, 1, 0, 0, c_api.PM_COUNT_ONE)),
        'Duplicate store was encountered (rare)')

        self.values['pool_pages'] = 0
        self.add_metric(name + '.pool_pages', pmdaMetric(
                self.pmid(0, 7),
                c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER,
                pmUnits(1, 0, 0, c_api.PM_SPACE_KBYTE, 0, 0)),
        'Memory pages used by the compressed pool')

        self.values['stored_pages'] = 0
        self.add_metric(name + '.stored_pages', pmdaMetric(
                self.pmid(0, 8),
                c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER,
                pmUnits(1, 0, 0, c_api.PM_SPACE_KBYTE, 0, 0)),
        'Compressed pages currently stored in zswap')

        self.values['pool_total_size'] = 0
        self.add_metric(name + '.pool_total_size', pmdaMetric(
                self.pmid(0, 9),
                c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_INSTANT,
                pmUnits(1, 0, 0, c_api.PM_SPACE_KBYTE, 0, 0)),
        'Total memory used by the compressed pool')

        self.values['same_filled_pages'] = 0
        self.add_metric(name + '.same_filled_pages', pmdaMetric(
                self.pmid(0, 10),
                c_api.PM_TYPE_U64, c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER,
                pmUnits(1, 0, 0, c_api.PM_SPACE_KBYTE, 0, 0)),
        'Same-value filled pages stored in zswap')

    def __init__(self, name, domain):
        PMDA.__init__(self, name, domain)
        self.patherrors = 0
        self.fileerrors = 0
        self.pagesize = int(getenv('ZSWAP_PAGESIZE', ZSWAP_PAGESIZE))
        self.path = str(getenv('ZSWAP_STATS_PATH', ZSWAP_STATS_PATH))

        self.values = {}
        self.setup_zswap_metrics(name)
        self.nmetrics = len(self.values)

        self.set_fetch(self.zswap_fetch)
        self.set_fetch_callback(self.zswap_fetch_callback)


if __name__ == '__main__':
    ZswapPMDA('zswap', 125).run()
