taler-exchange-benchmark.c (19272B)
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 "taler/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 (GNUNET_CRYPTO_QUALITY_WEAK, 185 UINT64_MAX); 186 random_01 = (double) random / (double) UINT64_MAX; 187 return (random_01 <= probability) ? GNUNET_OK : GNUNET_NO; 188 } 189 190 191 /** 192 * Actual commands construction and execution. 193 * 194 * @param cls unused 195 * @param is interpreter to run commands with 196 */ 197 static void 198 run (void *cls, 199 struct TALER_TESTING_Interpreter *is) 200 { 201 struct TALER_Amount total_reserve_amount; 202 struct TALER_Amount withdraw_fee; 203 char *withdraw_fee_str; 204 char *amount_5; 205 char *amount_4; 206 char *amount_1; 207 208 (void) cls; 209 all_commands = GNUNET_malloc_large ( 210 (1 /* exchange CMD */ 211 + howmany_reserves 212 * (1 /* Withdraw block */ 213 + howmany_coins) /* All units */ 214 + 1 /* stat CMD */ 215 + 1 /* End CMD */) * sizeof (struct TALER_TESTING_Command)); 216 GNUNET_assert (NULL != all_commands); 217 all_commands[0] 218 = TALER_TESTING_cmd_get_exchange ("get-exchange", 219 cred.cfg, 220 NULL, 221 true, 222 true); 223 GNUNET_asprintf (&amount_5, "%s:5", currency); 224 GNUNET_asprintf (&amount_4, "%s:4", currency); 225 GNUNET_asprintf (&amount_1, "%s:1", currency); 226 GNUNET_assert (GNUNET_OK == 227 TALER_amount_set_zero (currency, 228 &total_reserve_amount)); 229 total_reserve_amount.value = 5 * howmany_coins; 230 GNUNET_asprintf (&withdraw_fee_str, 231 "%s:0.1", 232 currency); 233 GNUNET_assert (GNUNET_OK == 234 TALER_string_to_amount (withdraw_fee_str, 235 &withdraw_fee)); 236 for (unsigned int i = 0; i < howmany_coins; i++) 237 GNUNET_assert (0 <= 238 TALER_amount_add (&total_reserve_amount, 239 &total_reserve_amount, 240 &withdraw_fee)); 241 for (unsigned int j = 0; j < howmany_reserves; j++) 242 { 243 char *create_reserve_label; 244 245 GNUNET_asprintf (&create_reserve_label, 246 "createreserve-%u", 247 j); 248 { 249 struct TALER_TESTING_Command make_reserve[] = { 250 cmd_transfer_to_exchange (add_label (create_reserve_label), 251 TALER_amount2s (&total_reserve_amount)), 252 TALER_TESTING_cmd_end () 253 }; 254 char *batch_label; 255 256 GNUNET_asprintf (&batch_label, 257 "batch-start-%u", 258 j); 259 all_commands[1 + (reserves_first 260 ? j 261 : j * (howmany_coins + 1))] 262 = TALER_TESTING_cmd_batch (add_label (batch_label), 263 make_reserve); 264 } 265 for (unsigned int i = 0; i < howmany_coins; i++) 266 { 267 char *withdraw_label; 268 char *order_enc; 269 struct TALER_TESTING_Command unit[UNITY_SIZE]; 270 char *unit_label; 271 const char *wl; 272 273 GNUNET_asprintf (&withdraw_label, 274 "withdraw-%u-%u", 275 i, 276 j); 277 wl = add_label (withdraw_label); 278 GNUNET_asprintf (&order_enc, 279 "{\"nonce\": %llu}", 280 ((unsigned long long) i) 281 + (howmany_coins * (unsigned long long) j)); 282 unit[0] = 283 TALER_TESTING_cmd_withdraw_with_retry ( 284 TALER_TESTING_cmd_withdraw_amount (wl, 285 create_reserve_label, 286 amount_5, 287 0, /* age restriction off */ 288 MHD_HTTP_OK)); 289 unit[1] = 290 TALER_TESTING_cmd_deposit_with_retry ( 291 TALER_TESTING_cmd_deposit ("deposit", 292 wl, 293 0, /* Index of the one withdrawn coin in the traits. */ 294 cred.user43_payto, 295 add_label (order_enc), 296 GNUNET_TIME_UNIT_ZERO, 297 amount_1, 298 MHD_HTTP_OK)); 299 if (eval_probability (refresh_rate / 100.0d)) 300 { 301 char *melt_label; 302 char *reveal_label; 303 const char *ml; 304 const char *rl; 305 306 GNUNET_asprintf (&melt_label, 307 "melt-%u-%u", 308 i, 309 j); 310 ml = add_label (melt_label); 311 GNUNET_asprintf (&reveal_label, 312 "reveal-%u-%u", 313 i, 314 j); 315 rl = add_label (reveal_label); 316 unit[2] = 317 TALER_TESTING_cmd_melt_with_retry ( 318 TALER_TESTING_cmd_melt (ml, 319 wl, 320 MHD_HTTP_OK, 321 NULL)); 322 unit[3] = 323 TALER_TESTING_cmd_melt_reveal_with_retry ( 324 TALER_TESTING_cmd_melt_reveal (rl, 325 ml, 326 MHD_HTTP_OK)); 327 unit[4] = TALER_TESTING_cmd_end (); 328 } 329 else 330 unit[2] = TALER_TESTING_cmd_end (); 331 332 GNUNET_asprintf (&unit_label, 333 "unit-%u-%u", 334 i, 335 j); 336 all_commands[1 + (reserves_first 337 ? howmany_reserves + j * howmany_coins + i 338 : j * (howmany_coins + 1) + (1 + i))] 339 = TALER_TESTING_cmd_batch (add_label (unit_label), 340 unit); 341 } 342 } 343 all_commands[1 + howmany_reserves * (1 + howmany_coins)] 344 = TALER_TESTING_cmd_stat (timings); 345 all_commands[1 + howmany_reserves * (1 + howmany_coins) + 1] 346 = TALER_TESTING_cmd_end (); 347 TALER_TESTING_run2 (is, 348 all_commands, 349 GNUNET_TIME_UNIT_FOREVER_REL); /* no timeout */ 350 GNUNET_free (amount_1); 351 GNUNET_free (amount_4); 352 GNUNET_free (amount_5); 353 GNUNET_free (withdraw_fee_str); 354 } 355 356 357 /** 358 * Print performance statistics for this process. 359 */ 360 static void 361 print_stats (void) 362 { 363 for (unsigned int i = 0; NULL != timings[i].prefix; i++) 364 { 365 char *total; 366 char *latency; 367 368 total = GNUNET_strdup ( 369 GNUNET_STRINGS_relative_time_to_string (timings[i].total_duration, 370 true)); 371 latency = GNUNET_strdup ( 372 GNUNET_STRINGS_relative_time_to_string (timings[i].success_latency, 373 true)); 374 fprintf (stderr, 375 "%s-%d took %s in total with %s for latency for %u executions (%u repeats)\n", 376 timings[i].prefix, 377 (int) getpid (), 378 total, 379 latency, 380 timings[i].num_commands, 381 timings[i].num_retries); 382 GNUNET_free (total); 383 GNUNET_free (latency); 384 } 385 } 386 387 388 /** 389 * Run the benchmark in parallel in many (client) processes 390 * and summarize result. 391 * 392 * @param main_cb main function to run per process 393 * @param main_cb_cls closure for @a main_cb 394 * @param config_file configuration file to use 395 * @return #GNUNET_OK on success 396 */ 397 static enum GNUNET_GenericReturnValue 398 parallel_benchmark (TALER_TESTING_Main main_cb, 399 void *main_cb_cls, 400 const char *config_file) 401 { 402 enum GNUNET_GenericReturnValue result = GNUNET_OK; 403 pid_t cpids[howmany_clients]; 404 405 if (1 == howmany_clients) 406 { 407 result = TALER_TESTING_loop (main_cb, 408 main_cb_cls); 409 print_stats (); 410 } 411 else 412 { 413 for (unsigned int i = 0; i<howmany_clients; i++) 414 { 415 if (0 == (cpids[i] = fork ())) 416 { 417 /* I am the child, do the work! */ 418 GNUNET_log_setup ("benchmark-worker", 419 loglev, 420 logfile); 421 result = TALER_TESTING_loop (main_cb, 422 main_cb_cls); 423 print_stats (); 424 if (GNUNET_OK != result) 425 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 426 "Failure in child process %u test suite!\n", 427 i); 428 if (GNUNET_OK == result) 429 exit (0); 430 else 431 exit (1); 432 } 433 if (-1 == cpids[i]) 434 { 435 GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, 436 "fork"); 437 howmany_clients = i; 438 result = GNUNET_SYSERR; 439 break; 440 } 441 /* fork() success, continue starting more processes! */ 442 } 443 /* collect all children */ 444 for (unsigned int i = 0; i<howmany_clients; i++) 445 { 446 int wstatus; 447 448 waitpid (cpids[i], 449 &wstatus, 450 0); 451 if ( (! WIFEXITED (wstatus)) || 452 (0 != WEXITSTATUS (wstatus)) ) 453 { 454 GNUNET_break (0); 455 result = GNUNET_SYSERR; 456 } 457 } 458 } 459 return result; 460 } 461 462 463 /** 464 * The main function of the serve tool 465 * 466 * @param argc number of arguments from the command line 467 * @param argv command line arguments 468 * @return 0 ok, or `enum PaymentGeneratorError` on error 469 */ 470 int 471 main (int argc, 472 char *const *argv) 473 { 474 struct GNUNET_GETOPT_CommandLineOption options[] = { 475 GNUNET_GETOPT_option_mandatory ( 476 GNUNET_GETOPT_option_cfgfile ( 477 &cfg_filename)), 478 GNUNET_GETOPT_option_version ( 479 PACKAGE_VERSION " " VCS_VERSION), 480 GNUNET_GETOPT_option_flag ( 481 'f', 482 "fakebank", 483 "use fakebank for the banking system", 484 &use_fakebank), 485 GNUNET_GETOPT_option_flag ( 486 'F', 487 "reserves-first", 488 "should all reserves be created first, before starting normal operations", 489 &reserves_first), 490 GNUNET_GETOPT_option_help ( 491 TALER_EXCHANGE_project_data (), 492 "Exchange benchmark"), 493 GNUNET_GETOPT_option_string ( 494 'l', 495 "logfile", 496 "LF", 497 "will log to file LF", 498 &logfile), 499 GNUNET_GETOPT_option_loglevel ( 500 &loglev), 501 GNUNET_GETOPT_option_uint ( 502 'n', 503 "coins-number", 504 "CN", 505 "How many coins we should instantiate per reserve", 506 &howmany_coins), 507 GNUNET_GETOPT_option_uint ( 508 'p', 509 "parallelism", 510 "NPROCS", 511 "How many client processes we should run", 512 &howmany_clients), 513 GNUNET_GETOPT_option_uint ( 514 'r', 515 "reserves", 516 "NRESERVES", 517 "How many reserves per client we should create", 518 &howmany_reserves), 519 GNUNET_GETOPT_option_uint ( 520 'R', 521 "refresh-rate", 522 "RATE", 523 "Probability of refresh per coin (0-100)", 524 &refresh_rate), 525 GNUNET_GETOPT_option_string ( 526 'u', 527 "exchange-account-section", 528 "SECTION", 529 "use exchange bank account configuration from the given SECTION", 530 &exchange_bank_section), 531 GNUNET_GETOPT_OPTION_END 532 }; 533 enum GNUNET_GenericReturnValue result; 534 struct GNUNET_TIME_Relative duration; 535 536 unsetenv ("XDG_DATA_HOME"); 537 unsetenv ("XDG_CONFIG_HOME"); 538 if (0 >= 539 (result = GNUNET_GETOPT_run ("taler-exchange-benchmark", 540 options, 541 argc, 542 argv))) 543 { 544 GNUNET_free (cfg_filename); 545 if (GNUNET_NO == result) 546 return 0; 547 return EXIT_INVALIDARGUMENT; 548 } 549 if (NULL == exchange_bank_section) 550 exchange_bank_section = (char *) "exchange-account-1"; 551 if (NULL == loglev) 552 loglev = (char *) "INFO"; 553 GNUNET_log_setup ("taler-exchange-benchmark", 554 loglev, 555 logfile); 556 if (NULL == cfg_filename) 557 cfg_filename = GNUNET_CONFIGURATION_default_filename ( 558 TALER_EXCHANGE_project_data ()); 559 if (NULL == cfg_filename) 560 { 561 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 562 "Can't find default configuration file.\n"); 563 return EXIT_NOTCONFIGURED; 564 } 565 cfg = GNUNET_CONFIGURATION_create (TALER_EXCHANGE_project_data ()); 566 if (GNUNET_OK != 567 GNUNET_CONFIGURATION_load (cfg, 568 cfg_filename)) 569 { 570 TALER_LOG_ERROR ("Could not parse configuration\n"); 571 GNUNET_free (cfg_filename); 572 return EXIT_NOTCONFIGURED; 573 } 574 if (GNUNET_OK != 575 TALER_config_get_currency (cfg, 576 "exchange", 577 ¤cy)) 578 { 579 GNUNET_CONFIGURATION_destroy (cfg); 580 GNUNET_free (cfg_filename); 581 return EXIT_NOTCONFIGURED; 582 } 583 if (howmany_clients > 10240) 584 { 585 TALER_LOG_ERROR ("-p option value given is too large\n"); 586 return EXIT_INVALIDARGUMENT; 587 } 588 if (0 == howmany_clients) 589 { 590 TALER_LOG_ERROR ("-p option value must not be zero\n"); 591 GNUNET_free (cfg_filename); 592 return EXIT_INVALIDARGUMENT; 593 } 594 595 if (GNUNET_OK != 596 TALER_TESTING_get_credentials ( 597 cfg_filename, 598 exchange_bank_section, 599 use_fakebank 600 ? TALER_TESTING_BS_FAKEBANK 601 : TALER_TESTING_BS_IBAN, 602 &cred)) 603 { 604 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 605 "Required bank credentials not given in configuration\n"); 606 GNUNET_free (cfg_filename); 607 return EXIT_NOTCONFIGURED; 608 } 609 610 { 611 struct GNUNET_TIME_Absolute start_time; 612 613 start_time = GNUNET_TIME_absolute_get (); 614 result = parallel_benchmark (&run, 615 NULL, 616 cfg_filename); 617 duration = GNUNET_TIME_absolute_get_duration (start_time); 618 } 619 620 if (GNUNET_OK == result) 621 { 622 struct rusage usage; 623 624 GNUNET_assert (0 == getrusage (RUSAGE_CHILDREN, 625 &usage)); 626 fprintf (stdout, 627 "Executed (Withdraw=%u, Deposit=%u, Refresh~=%5.2f)" 628 " * Reserve=%u * Parallel=%u, operations in %s\n", 629 howmany_coins, 630 howmany_coins, 631 howmany_coins * (refresh_rate / 100.0d), 632 howmany_reserves, 633 howmany_clients, 634 GNUNET_STRINGS_relative_time_to_string ( 635 duration, 636 false)); 637 fprintf (stdout, 638 "(approximately %s/coin)\n", 639 GNUNET_STRINGS_relative_time_to_string ( 640 GNUNET_TIME_relative_divide ( 641 duration, 642 (unsigned long long) howmany_coins 643 * howmany_reserves 644 * howmany_clients), 645 true)); 646 fprintf (stdout, 647 "RAW: %04u %04u %04u %16llu\n", 648 howmany_coins, 649 howmany_reserves, 650 howmany_clients, 651 (unsigned long long) duration.rel_value_us); 652 fprintf (stdout, 653 "cpu time: sys %llu user %llu\n", 654 (unsigned long long) (usage.ru_stime.tv_sec * 1000 * 1000 655 + usage.ru_stime.tv_usec), 656 (unsigned long long) (usage.ru_utime.tv_sec * 1000 * 1000 657 + usage.ru_utime.tv_usec)); 658 } 659 660 for (unsigned int i = 0; i<label_off; i++) 661 GNUNET_free (labels[i]); 662 GNUNET_array_grow (labels, 663 label_len, 664 0); 665 GNUNET_CONFIGURATION_destroy (cfg); 666 GNUNET_free (cfg_filename); 667 return (GNUNET_OK == result) ? 0 : result; 668 }