/*
  This file is part of TALER
  (C) 2014-2023 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"
/**
 * Credentials to use for the benchmark.
 */
static struct TALER_TESTING_Credentials cred;
/**
 * Array of all the commands the benchmark is running.
 */
static struct TALER_TESTING_Command *all_commands;
/**
 * Name of our configuration file.
 */
static char *cfg_filename;
/**
 * Use the fakebank instead of LibEuFin.
 */
static int use_fakebank;
/**
 * Verbosity level.
 */
static unsigned int verbose;
/**
 * 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;
/**
 * How many wirewatch processes do we want to create.
 */
static unsigned int start_wirewatch;
/**
 * Log level used during the run.
 */
static char *loglev;
/**
 * Log file.
 */
static char *logfile;
/**
 * Configuration.
 */
static struct GNUNET_CONFIGURATION_Handle *cfg;
/**
 * Section with the configuration data for the exchange
 * bank account.
 */
static char *exchange_bank_section;
/**
 * 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,
                                              true));
    latency = GNUNET_strdup (
      GNUNET_STRINGS_relative_time_to_string (timings[i].success_latency,
                                              true));
    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 ((1 + len)
                                      * sizeof (struct TALER_TESTING_Command));
  GNUNET_assert (NULL != all_commands);
  all_commands[0]
    = TALER_TESTING_cmd_get_exchange ("get-exchange",
                                      cred.cfg,
                                      NULL,
                                      true,
                                      true);
  GNUNET_asprintf (&total_reserve_amount,
                   "%s:5",
                   currency);
  for (unsigned int j = 0; j < howmany_reserves; j++)
  {
    char *create_reserve_label;
    GNUNET_asprintf (&create_reserve_label,
                     "createreserve-%u",
                     j);
    // TODO: vary user accounts more...
    all_commands[1 + j]
      = TALER_TESTING_cmd_admin_add_incoming_retry (
          TALER_TESTING_cmd_admin_add_incoming (add_label (
                                                  create_reserve_label),
                                                total_reserve_amount,
                                                &cred.ba,
                                                cred.user42_payto));
  }
  GNUNET_free (total_reserve_amount);
  all_commands[1 + howmany_reserves]
    = TALER_TESTING_cmd_stat (timings);
  all_commands[1 + 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];
  if (1 == howmany_clients)
  {
    /* do everything in this process */
    result = TALER_TESTING_loop (&run,
                                 NULL);
    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 EXIT_INVALIDARGUMENT;
  }
  if (NULL == exchange_bank_section)
    exchange_bank_section = "exchange-account-1";
  if (NULL == loglev)
    loglev = "INFO";
  GNUNET_log_setup ("taler-bank-benchmark",
                    loglev,
                    logfile);
  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 EXIT_NOTCONFIGURED;
  }
  if (GNUNET_OK !=
      TALER_config_get_currency (cfg,
                                 ¤cy))
  {
    GNUNET_CONFIGURATION_destroy (cfg);
    GNUNET_free (cfg_filename);
    return EXIT_NOTCONFIGURED;
  }
  if (GNUNET_OK !=
      TALER_TESTING_get_credentials (
        cfg_filename,
        exchange_bank_section,
        use_fakebank
        ? TALER_TESTING_BS_FAKEBANK
        : TALER_TESTING_BS_IBAN,
        &cred))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Required bank credentials not given in configuration\n");
    GNUNET_free (cfg_filename);
    return EXIT_NOTCONFIGURED;
  }
  {
    struct GNUNET_TIME_Absolute start_time;
    start_time = GNUNET_TIME_absolute_get ();
    result = parallel_benchmark ();
    duration = GNUNET_TIME_absolute_get_duration (start_time);
  }
  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