/*
  This file is part of TALER
  (C) 2014-2021 Taler Systems SA
  TALER is free software; you can redistribute it and/or modify it
  under the terms of the GNU Affero General Public License as
  published by the Free Software Foundation; either version 3, or
  (at your option) any later version.
  TALER 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 TALER; see the file COPYING.  If not,
  see 
*/
/**
 * @file benchmark/taler-bank-benchmark.c
 * @brief code to benchmark only the 'bank' and the 'taler-exchange-wirewatch' tool
 * @author Marcello Stanisci
 * @author Christian Grothoff
 */
// TODO:
// - use more than one 'client' bank account
// - also add taler-exchange-transfer to simulate outgoing payments
#include "platform.h"
#include 
#include 
#include 
#include "taler_util.h"
#include "taler_signatures.h"
#include "taler_json_lib.h"
#include "taler_bank_service.h"
#include "taler_exchangedb_lib.h"
#include "taler_fakebank_lib.h"
#include "taler_testing_lib.h"
#include "taler_error_codes.h"
/* Error codes.  */
enum BenchmarkError
{
  MISSING_BANK_URL,
  FAILED_TO_LAUNCH_BANK,
  BAD_CLI_ARG,
  BAD_CONFIG_FILE,
  NO_CONFIG_FILE_GIVEN
};
/**
 * What mode should the benchmark run in?
 */
enum BenchmarkMode
{
  /**
   * Run as client against the bank.
   */
  MODE_CLIENT = 1,
  /**
   * Run the bank.
   */
  MODE_BANK = 2,
  /**
   * Run both, for a local benchmark.
   */
  MODE_BOTH = 3,
};
/**
 * Hold information regarding which bank has the exchange account.
 */
static const struct TALER_EXCHANGEDB_AccountInfo *exchange_bank_account;
/**
 * Time snapshot taken right before executing the CMDs.
 */
static struct GNUNET_TIME_Absolute start_time;
/**
 * Benchmark duration time taken right after the CMD interpreter
 * returns.
 */
static struct GNUNET_TIME_Relative duration;
/**
 * Array of all the commands the benchmark is running.
 */
static struct TALER_TESTING_Command *all_commands;
/**
 * Dummy keepalive task.
 */
static struct GNUNET_SCHEDULER_Task *keepalive;
/**
 * Name of our configuration file.
 */
static char *cfg_filename;
/**
 * Use the fakebank instead of LibEuFin.
 * NOTE: LibEuFin not yet supported! Set
 * to 0 once we do support it!
 */
static int use_fakebank = 1;
/**
 * Number of taler-exchange-wirewatchers to launch.
 */
static unsigned int start_wirewatch;
/**
 * Verbosity level.
 */
static unsigned int verbose;
/**
 * Size of the transaction history the fakebank
 * should keep in RAM.
 */
static unsigned long long history_size = 65536;
/**
 * How many reserves we want to create per client.
 */
static unsigned int howmany_reserves = 1;
/**
 * How many clients we want to create.
 */
static unsigned int howmany_clients = 1;
/**
 * How many bank worker threads do we want to create.
 */
static unsigned int howmany_threads;
/**
 * Log level used during the run.
 */
static char *loglev;
/**
 * Log file.
 */
static char *logfile;
/**
 * Benchmarking mode (run as client, exchange, both) as string.
 */
static char *mode_str;
/**
 * Benchmarking mode (run as client, bank, both).
 */
static enum BenchmarkMode mode;
/**
 * Don't kill exchange/fakebank/wirewatch until
 * requested by the user explicitly.
 */
static int linger;
/**
 * Configuration.
 */
static struct GNUNET_CONFIGURATION_Handle *cfg;
/**
 * Currency used.
 */
static char *currency;
/**
 * Array of command labels.
 */
static char **labels;
/**
 * Length of #labels.
 */
static unsigned int label_len;
/**
 * Offset in #labels.
 */
static unsigned int label_off;
/**
 * Performance counters.
 */
static struct TALER_TESTING_Timer timings[] = {
  { .prefix = "createreserve" },
  { .prefix = NULL }
};
/**
 * Add label to the #labels table and return it.
 *
 * @param label string to add to the table
 * @return same string, now stored in the table
 */
const char *
add_label (char *label)
{
  if (label_off == label_len)
    GNUNET_array_grow (labels,
                       label_len,
                       label_len * 2 + 4);
  labels[label_off++] = label;
  return label;
}
/**
 * Print performance statistics for this process.
 */
static void
print_stats (void)
{
  for (unsigned int i = 0; NULL != timings[i].prefix; i++)
  {
    char *total;
    char *latency;
    total = GNUNET_strdup (
      GNUNET_STRINGS_relative_time_to_string (timings[i].total_duration,
                                              GNUNET_YES));
    latency = GNUNET_strdup (
      GNUNET_STRINGS_relative_time_to_string (timings[i].success_latency,
                                              GNUNET_YES));
    fprintf (stderr,
             "%s-%d took %s in total with %s for latency for %u executions (%u repeats)\n",
             timings[i].prefix,
             (int) getpid (),
             total,
             latency,
             timings[i].num_commands,
             timings[i].num_retries);
    GNUNET_free (total);
    GNUNET_free (latency);
  }
}
/**
 * Actual commands construction and execution.
 *
 * @param cls unused
 * @param is interpreter to run commands with
 */
static void
run (void *cls,
     struct TALER_TESTING_Interpreter *is)
{
  char *total_reserve_amount;
  size_t len;
  (void) cls;
  len = howmany_reserves + 2;
  all_commands = GNUNET_malloc_large (len
                                      * sizeof (struct TALER_TESTING_Command));
  GNUNET_assert (NULL != all_commands);
  GNUNET_asprintf (&total_reserve_amount,
                   "%s:5",
                   currency);
  for (unsigned int j = 0; j < howmany_reserves; j++)
  {
    char *create_reserve_label;
    char *user_payto_uri;
    // FIXME: vary user accounts more...
    GNUNET_assert (GNUNET_OK ==
                   GNUNET_CONFIGURATION_get_value_string (cfg,
                                                          "benchmark",
                                                          "USER_PAYTO_URI",
                                                          &user_payto_uri));
    GNUNET_asprintf (&create_reserve_label,
                     "createreserve-%u",
                     j);
    all_commands[j]
      = TALER_TESTING_cmd_admin_add_incoming_retry (
          TALER_TESTING_cmd_admin_add_incoming (add_label (
                                                  create_reserve_label),
                                                total_reserve_amount,
                                                exchange_bank_account->auth,
                                                add_label (user_payto_uri)));
  }
  GNUNET_free (total_reserve_amount);
  all_commands[howmany_reserves]
    = TALER_TESTING_cmd_stat (timings);
  all_commands[howmany_reserves + 1]
    = TALER_TESTING_cmd_end ();
  TALER_TESTING_run2 (is,
                      all_commands,
                      GNUNET_TIME_UNIT_FOREVER_REL); /* no timeout */
}
/**
 * Starts #howmany_clients workers to run the client logic from #run().
 */
static enum GNUNET_GenericReturnValue
launch_clients (void)
{
  enum GNUNET_GenericReturnValue result = GNUNET_OK;
  pid_t cpids[howmany_clients];
  start_time = GNUNET_TIME_absolute_get ();
  if (1 == howmany_clients)
  {
    /* do everything in this process */
    result = TALER_TESTING_setup (&run,
                                  NULL,
                                  cfg,
                                  NULL,
                                  GNUNET_NO);
    if (verbose)
      print_stats ();
    return result;
  }
  /* start work processes */
  for (unsigned int i = 0; i=
      (result = GNUNET_GETOPT_run ("taler-bank-benchmark",
                                   options,
                                   argc,
                                   argv)))
  {
    GNUNET_free (cfg_filename);
    if (GNUNET_NO == result)
      return 0;
    return BAD_CLI_ARG;
  }
  GNUNET_log_setup ("taler-bank-benchmark",
                    NULL == loglev ? "INFO" : loglev,
                    logfile);
  if (history_size < 10)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "History size too small, this can hardly work\n");
    return BAD_CLI_ARG;
  }
  if (NULL == mode_str)
    mode = MODE_BOTH;
  else if (0 == strcasecmp (mode_str,
                            "bank"))
    mode = MODE_BANK;
  else if (0 == strcasecmp (mode_str,
                            "client"))
    mode = MODE_CLIENT;
  else if (0 == strcasecmp (mode_str,
                            "both"))
    mode = MODE_BOTH;
  else
  {
    TALER_LOG_ERROR ("Unknown mode given: '%s'\n",
                     mode_str);
    GNUNET_free (cfg_filename);
    return BAD_CONFIG_FILE;
  }
  if (NULL == cfg_filename)
    cfg_filename = GNUNET_CONFIGURATION_default_filename ();
  if (NULL == cfg_filename)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Can't find default configuration file.\n");
    return EXIT_NOTCONFIGURED;
  }
  cfg = GNUNET_CONFIGURATION_create ();
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_load (cfg,
                                 cfg_filename))
  {
    TALER_LOG_ERROR ("Could not parse configuration\n");
    GNUNET_free (cfg_filename);
    return BAD_CONFIG_FILE;
  }
  if (GNUNET_OK !=
      TALER_config_get_currency (cfg,
                                 ¤cy))
  {
    GNUNET_CONFIGURATION_destroy (cfg);
    GNUNET_free (cfg_filename);
    return BAD_CONFIG_FILE;
  }
  if (MODE_BANK != mode)
  {
    if (howmany_clients > 10240)
    {
      TALER_LOG_ERROR ("-p option value given is too large\n");
      return BAD_CLI_ARG;
    }
    if (0 == howmany_clients)
    {
      TALER_LOG_ERROR ("-p option value must not be zero\n");
      GNUNET_free (cfg_filename);
      return BAD_CLI_ARG;
    }
  }
  if (GNUNET_OK !=
      TALER_EXCHANGEDB_load_accounts (cfg,
                                      TALER_EXCHANGEDB_ALO_AUTHDATA
                                      | TALER_EXCHANGEDB_ALO_CREDIT))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Configuration fails to provide exchange bank details\n");
    GNUNET_free (cfg_filename);
    return BAD_CONFIG_FILE;
  }
  exchange_bank_account
    = TALER_EXCHANGEDB_find_account_by_method ("x-taler-bank");
  if (NULL == exchange_bank_account)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "No bank account for `x-taler-bank` given in configuration\n");
    GNUNET_free (cfg_filename);
    return BAD_CONFIG_FILE;
  }
  result = parallel_benchmark ();
  TALER_EXCHANGEDB_unload_accounts ();
  GNUNET_CONFIGURATION_destroy (cfg);
  GNUNET_free (cfg_filename);
  if (MODE_BANK == mode)
  {
    /* If we're the bank, we're done now.  No need to print results. */
    return (GNUNET_OK == result) ? 0 : result;
  }
  if (GNUNET_OK == result)
  {
    struct rusage usage;
    unsigned long long tps;
    GNUNET_assert (0 == getrusage (RUSAGE_CHILDREN,
                                   &usage));
    fprintf (stdout,
             "Executed Reserve=%u * Parallel=%u, operations in %s\n",
             howmany_reserves,
             howmany_clients,
             GNUNET_STRINGS_relative_time_to_string (duration,
                                                     GNUNET_YES));
    if (! GNUNET_TIME_relative_is_zero (duration))
    {
      tps = ((unsigned long long) howmany_reserves) * howmany_clients * 1000LLU
            / (duration.rel_value_us / 1000LL);
      fprintf (stdout,
               "RAW: %04u %04u %16llu (%llu TPS)\n",
               howmany_reserves,
               howmany_clients,
               (unsigned long long) duration.rel_value_us,
               tps);
    }
    fprintf (stdout,
             "CPU time: sys %llu user %llu\n",                          \
             (unsigned long long) (usage.ru_stime.tv_sec * 1000 * 1000
                                   + usage.ru_stime.tv_usec),
             (unsigned long long) (usage.ru_utime.tv_sec * 1000 * 1000
                                   + usage.ru_utime.tv_usec));
  }
  for (unsigned int i = 0; i