/*
  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-exchange-benchmark.c
 * @brief HTTP serving layer intended to perform crypto-work and
 * communication with the exchange
 * @author Marcello Stanisci
 * @author Christian Grothoff
 */
#include "platform.h"
#include 
#include 
#include 
#include "taler_util.h"
#include "taler_testing_lib.h"
/**
 * The whole benchmark is a repetition of a "unit".  Each
 * unit is a array containing a withdraw+deposit operation,
 * and _possibly_ a refresh of the deposited coin.
 */
#define UNITY_SIZE 6
/**
 * 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;
/**
 * How many coins we want to create per client and reserve.
 */
static unsigned int howmany_coins = 1;
/**
 * How many reserves we want to create per client.
 */
static unsigned int howmany_reserves = 1;
/**
 * Probability (in percent) of refreshing per spent coin.
 */
static unsigned int refresh_rate = 10;
/**
 * How many clients we want to create.
 */
static unsigned int howmany_clients = 1;
/**
 * Log level used during the run.
 */
static char *loglev;
/**
 * Log file.
 */
static char *logfile;
/**
 * Configuration.
 */
static struct GNUNET_CONFIGURATION_Handle *cfg;
/**
 * Should we create all of the reserves at the beginning?
 */
static int reserves_first;
/**
 * Are we running against 'fakebank'?
 */
static int use_fakebank;
/**
 * 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 = "withdraw" },
  { .prefix = "deposit" },
  { .prefix = "melt" },
  { .prefix = "reveal" },
  { .prefix = "link" },
  { .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;
}
static struct TALER_TESTING_Command
cmd_transfer_to_exchange (const char *label,
                          const char *amount)
{
  return TALER_TESTING_cmd_admin_add_incoming_retry (
    TALER_TESTING_cmd_admin_add_incoming (label,
                                          amount,
                                          &cred.ba,
                                          cred.user42_payto));
}
/**
 * Throw a weighted coin with @a probability.
 *
 * @return #GNUNET_OK with @a probability,
 *         #GNUNET_NO with 1 - @a probability
 */
static unsigned int
eval_probability (float probability)
{
  uint64_t random;
  float random_01;
  random = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK,
                                     UINT64_MAX);
  random_01 = (double) random / (double) UINT64_MAX;
  return (random_01 <= probability) ? GNUNET_OK : GNUNET_NO;
}
/**
 * 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)
{
  struct TALER_Amount total_reserve_amount;
  struct TALER_Amount withdraw_fee;
  char *withdraw_fee_str;
  char *amount_5;
  char *amount_4;
  char *amount_1;
  (void) cls;
  all_commands = GNUNET_malloc_large (
    (1 /* exchange CMD */
     + howmany_reserves
     * (1 /* Withdraw block */
        + howmany_coins) /* All units */
     + 1 /* stat CMD */
     + 1 /* End CMD */) * 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 (&amount_5, "%s:5", currency);
  GNUNET_asprintf (&amount_4, "%s:4", currency);
  GNUNET_asprintf (&amount_1, "%s:1", currency);
  GNUNET_assert (GNUNET_OK ==
                 TALER_amount_set_zero (currency,
                                        &total_reserve_amount));
  total_reserve_amount.value = 5 * howmany_coins;
  GNUNET_asprintf (&withdraw_fee_str,
                   "%s:0.1",
                   currency);
  GNUNET_assert (GNUNET_OK ==
                 TALER_string_to_amount (withdraw_fee_str,
                                         &withdraw_fee));
  for (unsigned int i = 0; i < howmany_coins; i++)
    GNUNET_assert (0 <=
                   TALER_amount_add (&total_reserve_amount,
                                     &total_reserve_amount,
                                     &withdraw_fee));
  for (unsigned int j = 0; j < howmany_reserves; j++)
  {
    char *create_reserve_label;
    GNUNET_asprintf (&create_reserve_label,
                     "createreserve-%u",
                     j);
    {
      struct TALER_TESTING_Command make_reserve[] = {
        cmd_transfer_to_exchange (add_label (create_reserve_label),
                                  TALER_amount2s (&total_reserve_amount)),
        TALER_TESTING_cmd_end ()
      };
      char *batch_label;
      GNUNET_asprintf (&batch_label,
                       "batch-start-%u",
                       j);
      all_commands[1 + (reserves_first
                        ? j
                        : j * (howmany_coins + 1))]
        = TALER_TESTING_cmd_batch (add_label (batch_label),
                                   make_reserve);
    }
    for (unsigned int i = 0; i < howmany_coins; i++)
    {
      char *withdraw_label;
      char *order_enc;
      struct TALER_TESTING_Command unit[UNITY_SIZE];
      char *unit_label;
      const char *wl;
      GNUNET_asprintf (&withdraw_label,
                       "withdraw-%u-%u",
                       i,
                       j);
      wl = add_label (withdraw_label);
      GNUNET_asprintf (&order_enc,
                       "{\"nonce\": %llu}",
                       ((unsigned long long) i)
                       + (howmany_coins * (unsigned long long) j));
      unit[0] =
        TALER_TESTING_cmd_withdraw_with_retry (
          TALER_TESTING_cmd_withdraw_amount (wl,
                                             create_reserve_label,
                                             amount_5,
                                             0,  /* age restriction off */
                                             MHD_HTTP_OK));
      unit[1] =
        TALER_TESTING_cmd_deposit_with_retry (
          TALER_TESTING_cmd_deposit ("deposit",
                                     wl,
                                     0,  /* Index of the one withdrawn coin in the traits.  */
                                     cred.user43_payto,
                                     add_label (order_enc),
                                     GNUNET_TIME_UNIT_ZERO,
                                     amount_1,
                                     MHD_HTTP_OK));
      if (eval_probability (refresh_rate / 100.0))
      {
        char *melt_label;
        char *reveal_label;
        const char *ml;
        const char *rl;
        GNUNET_asprintf (&melt_label,
                         "melt-%u-%u",
                         i,
                         j);
        ml = add_label (melt_label);
        GNUNET_asprintf (&reveal_label,
                         "reveal-%u-%u",
                         i,
                         j);
        rl = add_label (reveal_label);
        unit[2] =
          TALER_TESTING_cmd_melt_with_retry (
            TALER_TESTING_cmd_melt (ml,
                                    wl,
                                    MHD_HTTP_OK,
                                    NULL));
        unit[3] =
          TALER_TESTING_cmd_refresh_reveal_with_retry (
            TALER_TESTING_cmd_refresh_reveal (rl,
                                              ml,
                                              MHD_HTTP_OK));
        unit[4] =
          TALER_TESTING_cmd_refresh_link_with_retry (
            TALER_TESTING_cmd_refresh_link ("link",
                                            rl,
                                            MHD_HTTP_OK));
        unit[5] = TALER_TESTING_cmd_end ();
      }
      else
        unit[2] = TALER_TESTING_cmd_end ();
      GNUNET_asprintf (&unit_label,
                       "unit-%u-%u",
                       i,
                       j);
      all_commands[1 + (reserves_first
                        ? howmany_reserves + j * howmany_coins + i
                        : j * (howmany_coins + 1) + (1 + i))]
        = TALER_TESTING_cmd_batch (add_label (unit_label),
                                   unit);
    }
  }
  all_commands[1 + howmany_reserves * (1 + howmany_coins)]
    = TALER_TESTING_cmd_stat (timings);
  all_commands[1 + howmany_reserves * (1 + howmany_coins) + 1]
    = TALER_TESTING_cmd_end ();
  TALER_TESTING_run2 (is,
                      all_commands,
                      GNUNET_TIME_UNIT_FOREVER_REL); /* no timeout */
  GNUNET_free (amount_1);
  GNUNET_free (amount_4);
  GNUNET_free (amount_5);
  GNUNET_free (withdraw_fee_str);
}
/**
 * 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);
  }
}
/**
 * Run the benchmark in parallel in many (client) processes
 * and summarize result.
 *
 * @param main_cb main function to run per process
 * @param main_cb_cls closure for @a main_cb
 * @param config_file configuration file to use
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
parallel_benchmark (TALER_TESTING_Main main_cb,
                    void *main_cb_cls,
                    const char *config_file)
{
  enum GNUNET_GenericReturnValue result = GNUNET_OK;
  pid_t cpids[howmany_clients];
  if (1 == howmany_clients)
  {
    result = TALER_TESTING_loop (main_cb,
                                 main_cb_cls);
    print_stats ();
  }
  else
  {
    for (unsigned int i = 0; i=
      (result = GNUNET_GETOPT_run ("taler-exchange-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-exchange-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 (howmany_clients > 10240)
  {
    TALER_LOG_ERROR ("-p option value given is too large\n");
    return EXIT_INVALIDARGUMENT;
  }
  if (0 == howmany_clients)
  {
    TALER_LOG_ERROR ("-p option value must not be zero\n");
    GNUNET_free (cfg_filename);
    return EXIT_INVALIDARGUMENT;
  }
  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 (&run,
                                 NULL,
                                 cfg_filename);
    duration = GNUNET_TIME_absolute_get_duration (start_time);
  }
  if (GNUNET_OK == result)
  {
    struct rusage usage;
    GNUNET_assert (0 == getrusage (RUSAGE_CHILDREN,
                                   &usage));
    fprintf (stdout,
             "Executed (Withdraw=%u, Deposit=%u, Refresh~=%5.2f)"
             " * Reserve=%u * Parallel=%u, operations in %s\n",
             howmany_coins,
             howmany_coins,
             (float) howmany_coins * (refresh_rate / 100.0),
             howmany_reserves,
             howmany_clients,
             GNUNET_STRINGS_relative_time_to_string (
               duration,
               false));
    fprintf (stdout,
             "(approximately %s/coin)\n",
             GNUNET_STRINGS_relative_time_to_string (
               GNUNET_TIME_relative_divide (
                 duration,
                 (unsigned long long) howmany_coins
                 * howmany_reserves
                 * howmany_clients),
               true));
    fprintf (stdout,
             "RAW: %04u %04u %04u %16llu\n",
             howmany_coins,
             howmany_reserves,
             howmany_clients,
             (unsigned long long) duration.rel_value_us);
    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