exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

taler-exchange-benchmark.c (19185B)


      1 /*
      2   This file is part of TALER
      3   (C) 2014-2023 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it
      6   under the terms of the GNU Affero General Public License as
      7   published by the Free Software Foundation; either version 3, or
      8   (at your option) any later version.
      9 
     10   TALER is distributed in the hope that it will be useful, but
     11   WITHOUT ANY WARRANTY; without even the implied warranty of
     12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     13   General Public License for more details.
     14 
     15   You should have received a copy of the GNU General Public License
     16   along with TALER; see the file COPYING.  If not,
     17   see <http://www.gnu.org/licenses/>
     18 */
     19 /**
     20  * @file benchmark/taler-exchange-benchmark.c
     21  * @brief HTTP serving layer intended to perform crypto-work and
     22  * communication with the exchange
     23  * @author Marcello Stanisci
     24  * @author Christian Grothoff
     25  */
     26 #include "platform.h"
     27 #include <gnunet/gnunet_util_lib.h>
     28 #include <microhttpd.h>
     29 #include <sys/resource.h>
     30 #include "taler/taler_util.h"
     31 #include "taler/taler_testing_lib.h"
     32 
     33 /**
     34  * The whole benchmark is a repetition of a "unit".  Each
     35  * unit is a array containing a withdraw+deposit operation,
     36  * and _possibly_ a refresh of the deposited coin.
     37  */
     38 #define UNITY_SIZE 6
     39 
     40 
     41 /**
     42  * Credentials to use for the benchmark.
     43  */
     44 static struct TALER_TESTING_Credentials cred;
     45 
     46 /**
     47  * Array of all the commands the benchmark is running.
     48  */
     49 static struct TALER_TESTING_Command *all_commands;
     50 
     51 /**
     52  * Name of our configuration file.
     53  */
     54 static char *cfg_filename;
     55 
     56 /**
     57  * How many coins we want to create per client and reserve.
     58  */
     59 static unsigned int howmany_coins = 1;
     60 
     61 /**
     62  * How many reserves we want to create per client.
     63  */
     64 static unsigned int howmany_reserves = 1;
     65 
     66 /**
     67  * Probability (in percent) of refreshing per spent coin.
     68  */
     69 static unsigned int refresh_rate = 10;
     70 
     71 /**
     72  * How many clients we want to create.
     73  */
     74 static unsigned int howmany_clients = 1;
     75 
     76 /**
     77  * Log level used during the run.
     78  */
     79 static char *loglev;
     80 
     81 /**
     82  * Log file.
     83  */
     84 static char *logfile;
     85 
     86 /**
     87  * Configuration.
     88  */
     89 static struct GNUNET_CONFIGURATION_Handle *cfg;
     90 
     91 /**
     92  * Should we create all of the reserves at the beginning?
     93  */
     94 static int reserves_first;
     95 
     96 /**
     97  * Are we running against 'fakebank'?
     98  */
     99 static int use_fakebank;
    100 
    101 /**
    102  * Section with the configuration data for the exchange
    103  * bank account.
    104  */
    105 static char *exchange_bank_section;
    106 
    107 /**
    108  * Currency used.
    109  */
    110 static char *currency;
    111 
    112 /**
    113  * Array of command labels.
    114  */
    115 static char **labels;
    116 
    117 /**
    118  * Length of #labels.
    119  */
    120 static unsigned int label_len;
    121 
    122 /**
    123  * Offset in #labels.
    124  */
    125 static unsigned int label_off;
    126 
    127 /**
    128  * Performance counters.
    129  */
    130 static struct TALER_TESTING_Timer timings[] = {
    131   { .prefix = "createreserve" },
    132   { .prefix = "withdraw" },
    133   { .prefix = "deposit" },
    134   { .prefix = "melt" },
    135   { .prefix = "reveal" },
    136   { .prefix = "link" },
    137   { .prefix = NULL }
    138 };
    139 
    140 
    141 /**
    142  * Add label to the #labels table and return it.
    143  *
    144  * @param label string to add to the table
    145  * @return same string, now stored in the table
    146  */
    147 static const char *
    148 add_label (char *label)
    149 {
    150   if (label_off == label_len)
    151     GNUNET_array_grow (labels,
    152                        label_len,
    153                        label_len * 2 + 4);
    154   labels[label_off++] = label;
    155   return label;
    156 }
    157 
    158 
    159 static struct TALER_TESTING_Command
    160 cmd_transfer_to_exchange (const char *label,
    161                           const char *amount)
    162 {
    163   return TALER_TESTING_cmd_admin_add_incoming_retry (
    164     TALER_TESTING_cmd_admin_add_incoming (label,
    165                                           amount,
    166                                           &cred.ba_admin,
    167                                           cred.user42_payto));
    168 }
    169 
    170 
    171 /**
    172  * Throw a weighted coin with @a probability.
    173  *
    174  * @param probability weight of the coin flip
    175  * @return #GNUNET_OK with @a probability,
    176  *         #GNUNET_NO with 1 - @a probability
    177  */
    178 static unsigned int
    179 eval_probability (float probability)
    180 {
    181   uint64_t random;
    182   float random_01;
    183 
    184   random = GNUNET_CRYPTO_random_u64 (UINT64_MAX);
    185   random_01 = (double) random / (double) UINT64_MAX;
    186   return (random_01 <= probability) ? GNUNET_OK : GNUNET_NO;
    187 }
    188 
    189 
    190 /**
    191  * Actual commands construction and execution.
    192  *
    193  * @param cls unused
    194  * @param is interpreter to run commands with
    195  */
    196 static void
    197 run (void *cls,
    198      struct TALER_TESTING_Interpreter *is)
    199 {
    200   struct TALER_Amount total_reserve_amount;
    201   struct TALER_Amount withdraw_fee;
    202   char *withdraw_fee_str;
    203   char *amount_5;
    204   char *amount_4;
    205   char *amount_1;
    206 
    207   (void) cls;
    208   all_commands = GNUNET_malloc_large (
    209     (1 /* exchange CMD */
    210      + howmany_reserves
    211      * (1 /* Withdraw block */
    212         + howmany_coins) /* All units */
    213      + 1 /* stat CMD */
    214      + 1 /* End CMD */) * sizeof (struct TALER_TESTING_Command));
    215   GNUNET_assert (NULL != all_commands);
    216   all_commands[0]
    217     = TALER_TESTING_cmd_get_exchange ("get-exchange",
    218                                       cred.cfg,
    219                                       NULL,
    220                                       true,
    221                                       true);
    222   GNUNET_asprintf (&amount_5, "%s:5", currency);
    223   GNUNET_asprintf (&amount_4, "%s:4", currency);
    224   GNUNET_asprintf (&amount_1, "%s:1", currency);
    225   GNUNET_assert (GNUNET_OK ==
    226                  TALER_amount_set_zero (currency,
    227                                         &total_reserve_amount));
    228   total_reserve_amount.value = 5 * howmany_coins;
    229   GNUNET_asprintf (&withdraw_fee_str,
    230                    "%s:0.1",
    231                    currency);
    232   GNUNET_assert (GNUNET_OK ==
    233                  TALER_string_to_amount (withdraw_fee_str,
    234                                          &withdraw_fee));
    235   for (unsigned int i = 0; i < howmany_coins; i++)
    236     GNUNET_assert (0 <=
    237                    TALER_amount_add (&total_reserve_amount,
    238                                      &total_reserve_amount,
    239                                      &withdraw_fee));
    240   for (unsigned int j = 0; j < howmany_reserves; j++)
    241   {
    242     char *create_reserve_label;
    243 
    244     GNUNET_asprintf (&create_reserve_label,
    245                      "createreserve-%u",
    246                      j);
    247     {
    248       struct TALER_TESTING_Command make_reserve[] = {
    249         cmd_transfer_to_exchange (add_label (create_reserve_label),
    250                                   TALER_amount2s (&total_reserve_amount)),
    251         TALER_TESTING_cmd_end ()
    252       };
    253       char *batch_label;
    254 
    255       GNUNET_asprintf (&batch_label,
    256                        "batch-start-%u",
    257                        j);
    258       all_commands[1 + (reserves_first
    259                         ? j
    260                         : j * (howmany_coins + 1))]
    261         = TALER_TESTING_cmd_batch (add_label (batch_label),
    262                                    make_reserve);
    263     }
    264     for (unsigned int i = 0; i < howmany_coins; i++)
    265     {
    266       char *withdraw_label;
    267       char *order_enc;
    268       struct TALER_TESTING_Command unit[UNITY_SIZE];
    269       char *unit_label;
    270       const char *wl;
    271 
    272       GNUNET_asprintf (&withdraw_label,
    273                        "withdraw-%u-%u",
    274                        i,
    275                        j);
    276       wl = add_label (withdraw_label);
    277       GNUNET_asprintf (&order_enc,
    278                        "{\"nonce\": %llu}",
    279                        ((unsigned long long) i)
    280                        + (howmany_coins * (unsigned long long) j));
    281       unit[0] =
    282         TALER_TESTING_cmd_withdraw_with_retry (
    283           TALER_TESTING_cmd_withdraw_amount (wl,
    284                                              create_reserve_label,
    285                                              amount_5,
    286                                              0,  /* age restriction off */
    287                                              MHD_HTTP_OK));
    288       unit[1] =
    289         TALER_TESTING_cmd_deposit_with_retry (
    290           TALER_TESTING_cmd_deposit ("deposit",
    291                                      wl,
    292                                      0,  /* Index of the one withdrawn coin in the traits.  */
    293                                      cred.user43_payto,
    294                                      add_label (order_enc),
    295                                      GNUNET_TIME_UNIT_ZERO,
    296                                      amount_1,
    297                                      MHD_HTTP_OK));
    298       if (eval_probability (refresh_rate / 100.0d))
    299       {
    300         char *melt_label;
    301         char *reveal_label;
    302         const char *ml;
    303         const char *rl;
    304 
    305         GNUNET_asprintf (&melt_label,
    306                          "melt-%u-%u",
    307                          i,
    308                          j);
    309         ml = add_label (melt_label);
    310         GNUNET_asprintf (&reveal_label,
    311                          "reveal-%u-%u",
    312                          i,
    313                          j);
    314         rl = add_label (reveal_label);
    315         unit[2] =
    316           TALER_TESTING_cmd_melt_with_retry (
    317             TALER_TESTING_cmd_melt (ml,
    318                                     wl,
    319                                     MHD_HTTP_OK,
    320                                     NULL));
    321         unit[3] =
    322           TALER_TESTING_cmd_melt_reveal_with_retry (
    323             TALER_TESTING_cmd_melt_reveal (rl,
    324                                            ml,
    325                                            MHD_HTTP_OK));
    326         unit[4] = TALER_TESTING_cmd_end ();
    327       }
    328       else
    329         unit[2] = TALER_TESTING_cmd_end ();
    330 
    331       GNUNET_asprintf (&unit_label,
    332                        "unit-%u-%u",
    333                        i,
    334                        j);
    335       all_commands[1 + (reserves_first
    336                         ? howmany_reserves + j * howmany_coins + i
    337                         : j * (howmany_coins + 1) + (1 + i))]
    338         = TALER_TESTING_cmd_batch (add_label (unit_label),
    339                                    unit);
    340     }
    341   }
    342   all_commands[1 + howmany_reserves * (1 + howmany_coins)]
    343     = TALER_TESTING_cmd_stat (timings);
    344   all_commands[1 + howmany_reserves * (1 + howmany_coins) + 1]
    345     = TALER_TESTING_cmd_end ();
    346   TALER_TESTING_run2 (is,
    347                       all_commands,
    348                       GNUNET_TIME_UNIT_FOREVER_REL); /* no timeout */
    349   GNUNET_free (amount_1);
    350   GNUNET_free (amount_4);
    351   GNUNET_free (amount_5);
    352   GNUNET_free (withdraw_fee_str);
    353 }
    354 
    355 
    356 /**
    357  * Print performance statistics for this process.
    358  */
    359 static void
    360 print_stats (void)
    361 {
    362   for (unsigned int i = 0; NULL != timings[i].prefix; i++)
    363   {
    364     char *total;
    365     char *latency;
    366 
    367     total = GNUNET_strdup (
    368       GNUNET_STRINGS_relative_time_to_string (timings[i].total_duration,
    369                                               true));
    370     latency = GNUNET_strdup (
    371       GNUNET_STRINGS_relative_time_to_string (timings[i].success_latency,
    372                                               true));
    373     fprintf (stderr,
    374              "%s-%d took %s in total with %s for latency for %u executions (%u repeats)\n",
    375              timings[i].prefix,
    376              (int) getpid (),
    377              total,
    378              latency,
    379              timings[i].num_commands,
    380              timings[i].num_retries);
    381     GNUNET_free (total);
    382     GNUNET_free (latency);
    383   }
    384 }
    385 
    386 
    387 /**
    388  * Run the benchmark in parallel in many (client) processes
    389  * and summarize result.
    390  *
    391  * @param main_cb main function to run per process
    392  * @param main_cb_cls closure for @a main_cb
    393  * @param config_file configuration file to use
    394  * @return #GNUNET_OK on success
    395  */
    396 static enum GNUNET_GenericReturnValue
    397 parallel_benchmark (TALER_TESTING_Main main_cb,
    398                     void *main_cb_cls,
    399                     const char *config_file)
    400 {
    401   enum GNUNET_GenericReturnValue result = GNUNET_OK;
    402   pid_t cpids[howmany_clients];
    403 
    404   if (1 == howmany_clients)
    405   {
    406     result = TALER_TESTING_loop (main_cb,
    407                                  main_cb_cls);
    408     print_stats ();
    409   }
    410   else
    411   {
    412     for (unsigned int i = 0; i<howmany_clients; i++)
    413     {
    414       if (0 == (cpids[i] = fork ()))
    415       {
    416         /* I am the child, do the work! */
    417         GNUNET_log_setup ("benchmark-worker",
    418                           loglev,
    419                           logfile);
    420         result = TALER_TESTING_loop (main_cb,
    421                                      main_cb_cls);
    422         print_stats ();
    423         if (GNUNET_OK != result)
    424           GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    425                       "Failure in child process %u test suite!\n",
    426                       i);
    427         if (GNUNET_OK == result)
    428           exit (0);
    429         else
    430           exit (1);
    431       }
    432       if (-1 == cpids[i])
    433       {
    434         GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
    435                              "fork");
    436         howmany_clients = i;
    437         result = GNUNET_SYSERR;
    438         break;
    439       }
    440       /* fork() success, continue starting more processes! */
    441     }
    442     /* collect all children */
    443     for (unsigned int i = 0; i<howmany_clients; i++)
    444     {
    445       int wstatus;
    446 
    447       waitpid (cpids[i],
    448                &wstatus,
    449                0);
    450       if ( (! WIFEXITED (wstatus)) ||
    451            (0 != WEXITSTATUS (wstatus)) )
    452       {
    453         GNUNET_break (0);
    454         result = GNUNET_SYSERR;
    455       }
    456     }
    457   }
    458   return result;
    459 }
    460 
    461 
    462 /**
    463  * The main function of the serve tool
    464  *
    465  * @param argc number of arguments from the command line
    466  * @param argv command line arguments
    467  * @return 0 ok, or `enum PaymentGeneratorError` on error
    468  */
    469 int
    470 main (int argc,
    471       char *const *argv)
    472 {
    473   struct GNUNET_GETOPT_CommandLineOption options[] = {
    474     GNUNET_GETOPT_option_mandatory (
    475       GNUNET_GETOPT_option_cfgfile (
    476         &cfg_filename)),
    477     GNUNET_GETOPT_option_version (
    478       PACKAGE_VERSION),
    479     GNUNET_GETOPT_option_flag (
    480       'f',
    481       "fakebank",
    482       "use fakebank for the banking system",
    483       &use_fakebank),
    484     GNUNET_GETOPT_option_flag (
    485       'F',
    486       "reserves-first",
    487       "should all reserves be created first, before starting normal operations",
    488       &reserves_first),
    489     GNUNET_GETOPT_option_help (
    490       TALER_EXCHANGE_project_data (),
    491       "Exchange benchmark"),
    492     GNUNET_GETOPT_option_string (
    493       'l',
    494       "logfile",
    495       "LF",
    496       "will log to file LF",
    497       &logfile),
    498     GNUNET_GETOPT_option_loglevel (
    499       &loglev),
    500     GNUNET_GETOPT_option_uint (
    501       'n',
    502       "coins-number",
    503       "CN",
    504       "How many coins we should instantiate per reserve",
    505       &howmany_coins),
    506     GNUNET_GETOPT_option_uint (
    507       'p',
    508       "parallelism",
    509       "NPROCS",
    510       "How many client processes we should run",
    511       &howmany_clients),
    512     GNUNET_GETOPT_option_uint (
    513       'r',
    514       "reserves",
    515       "NRESERVES",
    516       "How many reserves per client we should create",
    517       &howmany_reserves),
    518     GNUNET_GETOPT_option_uint (
    519       'R',
    520       "refresh-rate",
    521       "RATE",
    522       "Probability of refresh per coin (0-100)",
    523       &refresh_rate),
    524     GNUNET_GETOPT_option_string (
    525       'u',
    526       "exchange-account-section",
    527       "SECTION",
    528       "use exchange bank account configuration from the given SECTION",
    529       &exchange_bank_section),
    530     GNUNET_GETOPT_OPTION_END
    531   };
    532   enum GNUNET_GenericReturnValue result;
    533   struct GNUNET_TIME_Relative duration;
    534 
    535   unsetenv ("XDG_DATA_HOME");
    536   unsetenv ("XDG_CONFIG_HOME");
    537   if (0 >=
    538       (result = GNUNET_GETOPT_run ("taler-exchange-benchmark",
    539                                    options,
    540                                    argc,
    541                                    argv)))
    542   {
    543     GNUNET_free (cfg_filename);
    544     if (GNUNET_NO == result)
    545       return 0;
    546     return EXIT_INVALIDARGUMENT;
    547   }
    548   if (NULL == exchange_bank_section)
    549     exchange_bank_section = (char *) "exchange-account-1";
    550   if (NULL == loglev)
    551     loglev = (char *) "INFO";
    552   GNUNET_log_setup ("taler-exchange-benchmark",
    553                     loglev,
    554                     logfile);
    555   if (NULL == cfg_filename)
    556     cfg_filename = GNUNET_CONFIGURATION_default_filename (
    557       TALER_EXCHANGE_project_data ());
    558   if (NULL == cfg_filename)
    559   {
    560     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    561                 "Can't find default configuration file.\n");
    562     return EXIT_NOTCONFIGURED;
    563   }
    564   cfg = GNUNET_CONFIGURATION_create (TALER_EXCHANGE_project_data ());
    565   if (GNUNET_OK !=
    566       GNUNET_CONFIGURATION_load (cfg,
    567                                  cfg_filename))
    568   {
    569     TALER_LOG_ERROR ("Could not parse configuration\n");
    570     GNUNET_free (cfg_filename);
    571     return EXIT_NOTCONFIGURED;
    572   }
    573   if (GNUNET_OK !=
    574       TALER_config_get_currency (cfg,
    575                                  "exchange",
    576                                  &currency))
    577   {
    578     GNUNET_CONFIGURATION_destroy (cfg);
    579     GNUNET_free (cfg_filename);
    580     return EXIT_NOTCONFIGURED;
    581   }
    582   if (howmany_clients > 10240)
    583   {
    584     TALER_LOG_ERROR ("-p option value given is too large\n");
    585     return EXIT_INVALIDARGUMENT;
    586   }
    587   if (0 == howmany_clients)
    588   {
    589     TALER_LOG_ERROR ("-p option value must not be zero\n");
    590     GNUNET_free (cfg_filename);
    591     return EXIT_INVALIDARGUMENT;
    592   }
    593 
    594   if (GNUNET_OK !=
    595       TALER_TESTING_get_credentials (
    596         cfg_filename,
    597         exchange_bank_section,
    598         use_fakebank
    599         ? TALER_TESTING_BS_FAKEBANK
    600         : TALER_TESTING_BS_IBAN,
    601         &cred))
    602   {
    603     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    604                 "Required bank credentials not given in configuration\n");
    605     GNUNET_free (cfg_filename);
    606     return EXIT_NOTCONFIGURED;
    607   }
    608 
    609   {
    610     struct GNUNET_TIME_Absolute start_time;
    611 
    612     start_time = GNUNET_TIME_absolute_get ();
    613     result = parallel_benchmark (&run,
    614                                  NULL,
    615                                  cfg_filename);
    616     duration = GNUNET_TIME_absolute_get_duration (start_time);
    617   }
    618 
    619   if (GNUNET_OK == result)
    620   {
    621     struct rusage usage;
    622 
    623     GNUNET_assert (0 == getrusage (RUSAGE_CHILDREN,
    624                                    &usage));
    625     fprintf (stdout,
    626              "Executed (Withdraw=%u, Deposit=%u, Refresh~=%5.2f)"
    627              " * Reserve=%u * Parallel=%u, operations in %s\n",
    628              howmany_coins,
    629              howmany_coins,
    630              howmany_coins * (refresh_rate / 100.0d),
    631              howmany_reserves,
    632              howmany_clients,
    633              GNUNET_STRINGS_relative_time_to_string (
    634                duration,
    635                false));
    636     fprintf (stdout,
    637              "(approximately %s/coin)\n",
    638              GNUNET_STRINGS_relative_time_to_string (
    639                GNUNET_TIME_relative_divide (
    640                  duration,
    641                  (unsigned long long) howmany_coins
    642                  * howmany_reserves
    643                  * howmany_clients),
    644                true));
    645     fprintf (stdout,
    646              "RAW: %04u %04u %04u %16llu\n",
    647              howmany_coins,
    648              howmany_reserves,
    649              howmany_clients,
    650              (unsigned long long) duration.rel_value_us);
    651     fprintf (stdout,
    652              "cpu time: sys %llu user %llu\n",
    653              (unsigned long long) (usage.ru_stime.tv_sec * 1000 * 1000
    654                                    + usage.ru_stime.tv_usec),
    655              (unsigned long long) (usage.ru_utime.tv_sec * 1000 * 1000
    656                                    + usage.ru_utime.tv_usec));
    657   }
    658 
    659   for (unsigned int i = 0; i<label_off; i++)
    660     GNUNET_free (labels[i]);
    661   GNUNET_array_grow (labels,
    662                      label_len,
    663                      0);
    664   GNUNET_CONFIGURATION_destroy (cfg);
    665   GNUNET_free (cfg_filename);
    666   return (GNUNET_OK == result) ? 0 : result;
    667 }