/*
 * Copyright (c) 2020 Red Hat, Inc.
 *
 * 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.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA. 
 *
 * $Id: //eng/vdo-releases/aluminum/src/c++/vdo/kernel/workItemStats.h#2 $
 */

#ifndef WORK_ITEM_STATS_H
#define WORK_ITEM_STATS_H

#include "timeUtils.h"

#include "workQueue.h"

enum {
  // Whether to enable tracking of per-work-function run-time stats.
  ENABLE_PER_FUNCTION_TIMING_STATS = 0,
  // How many work function/priority pairs to track call stats for
  NUM_WORK_QUEUE_ITEM_STATS        = 18,
};

typedef struct simpleStats {
  uint64_t count;
  uint64_t sum;
  uint64_t min;
  uint64_t max;
} SimpleStats;

/*
 * We track numbers of work items handled (and optionally the
 * wall-clock time to run the work functions), broken down by
 * individual work functions (or alternate functions that the caller
 * wants recorded, like the VIO completion callback function if we're
 * just enqueueing a work function that invokes that indirectly) and
 * priority.
 *
 * The first part of this structure manages the function/priority
 * pairs, and is read frequently but updated rarely (once for each
 * pair, plus possibly spin lock contention).
 *
 * The second part holds counters, and is updated often; different
 * parts are updated by various threads as described below. The last
 * element of each array, index NUM_WORK_QUEUE_ITEM_STATS, is updated
 * only if we have filled the arrays and can't add the current work
 * function/priority. See how the statTableIndex field is set in
 * workItemStats.c.
 *
 * All fields may additionally be read when reporting statistics
 * (including optionally reporting stats when the worker thread shuts
 * down), but that's rare and shouldn't significantly affect cache
 * contention issues.
 *
 * There is no "pending" count per work function here. For reporting
 * statistics, it can be approximated by looking at the other fields.
 * Do not rely on them being precise and synchronized, though.
 */
typedef struct kvdoWorkItemStatsFunctionTable {
  /*
   * The spin lock is used to protect .functions and .priorities
   * during updates. All three are modified by producers (enqueueing
   * threads) but only rarely. The .functions and .priorities arrays
   * are read by producers very frequently.
   */
  spinlock_t       lock;
  KvdoWorkFunction functions[NUM_WORK_QUEUE_ITEM_STATS];
  uint8_t          priorities[NUM_WORK_QUEUE_ITEM_STATS];
} KvdoWorkFunctionTable;

typedef struct kvdoWorkItemStats {
  /*
   * Table of functions and priorities, for determining the index to
   * use into the counter arrays below.
   *
   * This table is read by producers (usually multiple entries) for
   * every work item enqueued, and when reporting stats. It is updated
   * by producers, and only the first time a new (work-function,
   * priority) combination is seen.
   */
  KvdoWorkFunctionTable  functionTable;
  // Skip to (somewhere on) the next cache line
  char                   pad[CACHE_LINE_BYTES - sizeof(atomic64_t)];
  /*
   * The .enqueued field is updated by producers only, once per work
   * item processed; __sync operations are used to update these
   * values.
   */
  atomic64_t             enqueued[NUM_WORK_QUEUE_ITEM_STATS + 1];
  // Skip to (somewhere on) the next cache line
  char                   pad2[CACHE_LINE_BYTES - sizeof(atomic64_t)];
  /*
   * These values are updated only by the consumer (worker thread). We
   * overload the .times[].count field as a count of items processed,
   * so if we're not doing the optional processing-time tracking
   * (controlled via an option in workQueue.c), we need to explicitly
   * update the count.
   *
   * Since only one thread can ever update these values, no
   * synchronization is used.
   */
  SimpleStats            times[NUM_WORK_QUEUE_ITEM_STATS + 1];
} KvdoWorkItemStats;

/**
 * Initialize a statistics structure for tracking sample
 * values. Assumes the storage was already zeroed out at allocation
 * time.
 *
 * @param stats    The statistics structure
 **/
static inline void initSimpleStats(SimpleStats *stats)
{
  // Assume other fields are initialized to zero at allocation.
  stats->min = UINT64_MAX;
}

/**
 * Update the statistics being tracked for a new sample value.
 *
 * @param stats    The statistics structure
 * @param value    The new value to be folded in
 **/
static inline void addSample(SimpleStats *stats, uint64_t value)
{
  stats->count++;
  stats->sum += value;
  if (stats->min > value) {
    stats->min = value;
  }
  if (stats->max < value) {
    stats->max = value;
  }
}

/**
 * Return the average of the samples collected.
 *
 * @param stats    The statistics structure
 *
 * @return         The average sample value
 **/
static inline uint64_t getSampleAverage(const SimpleStats *stats)
{
  uint64_t slop = stats->count / 2;
  return (stats->sum + slop) / stats->count;
}

/**
 * Update all work queue statistics (work-item and otherwise) after
 * enqueueing a work item.
 *
 * @param  stats     The statistics structure
 * @param  item      The work item enqueued
 * @param  priority  The work item's priority
 **/
void updateWorkItemStatsForEnqueue(KvdoWorkItemStats *stats,
                                   KvdoWorkItem      *item,
                                   int                priority);

/**
 * Update all work queue statistics (work-item and otherwise) after enqueueing
 * a work item.
 *
 * This is a very lightweight function (after optimizing away conditionals and
 * no-ops) and is called for every work item processed, hence the inline
 * definition.
 *
 * This function requires that recordStartTime and
 * updateWorkItemStatsForWorkTime below both get called as well; in some cases
 * counters may be updated in updateWorkItemStatsForWorkTime rather than here.
 *
 * @param  stats  The statistics structure
 * @param  item   The work item enqueued
 **/
static inline void updateWorkItemStatsForDequeue(KvdoWorkItemStats *stats,
                                                 KvdoWorkItem      *item)
{
  // The times[].count field is overloaded as a count of items
  // processed.
  if (!ENABLE_PER_FUNCTION_TIMING_STATS) {
    stats->times[item->statTableIndex].count++;
  } else {
    // In this case, updateWorkItemStatsForWorkTime will bump the counter.
  }
}

/**
 * Record the starting time for processing a work item, if timing
 * stats are enabled and if we haven't run out of room for recording
 * stats in the table.
 *
 * @param  index  The work item's index into the internal array
 *
 * @return    The current time, or zero
 **/
static inline uint64_t recordStartTime(unsigned int index)
{
  return (ENABLE_PER_FUNCTION_TIMING_STATS ? currentTime(CLOCK_MONOTONIC) : 0);
}

/**
 * Update the work queue statistics with the wall-clock time for
 * processing a work item, if timing stats are enabled and if we
 * haven't run out of room for recording stats in the table.
 *
 * @param  stats      The statistics structure
 * @param  index      The work item's index into the internal array
 * @param  startTime  The start time as reported by recordStartTime
 **/
static inline void updateWorkItemStatsForWorkTime(KvdoWorkItemStats *stats,
                                                  unsigned int       index,
                                                  uint64_t           startTime)
{
  if (ENABLE_PER_FUNCTION_TIMING_STATS) {
    uint64_t endTime = currentTime(CLOCK_MONOTONIC);
    addSample(&stats->times[index], endTime - startTime);
  }
}

/**
 * Convert the pointer into a string representation, using a function
 * name if available.
 *
 * @param pointer       The pointer to be converted
 * @param buffer        The output buffer
 * @param bufferLength  The size of the output buffer
 **/
char *getFunctionName(void *pointer, char *buffer, size_t bufferLength);

/**
 * Dump statistics broken down by work function and priority into the
 * kernel log.
 *
 * @param  stats  The statistics structure
 **/
void logWorkItemStats(const KvdoWorkItemStats *stats);

/**
 * Format counters for per-work-function stats for reporting via /sys.
 *
 * @param [in]  stats   The statistics structure
 * @param [out] buffer  The output buffer
 * @param [in]  length  The size of the output buffer
 *
 * @return  The size of the string actually written
 **/
size_t formatWorkItemStats(const KvdoWorkItemStats *stats,
                           char                    *buffer,
                           size_t                   length);

#endif // WORK_ITEM_STATS_H
