Last active
October 31, 2021 05:09
-
-
Save jay/97c8a7c20031997438dd7456e0a2b83a to your computer and use it in GitHub Desktop.
Use libcurl to test multi vs easy performance.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Use libcurl to test multi vs easy performance. | |
Usage: multi_vs_easy | |
This program compares the different methods that can be used to make transfers | |
to the same host. The code was written only for the purpose of comparison of | |
the different methods. | |
g++ -o multi_vs_easy multi_vs_easy.cpp `curl-config --cflags --libs` | |
Copyright (C) 2020 Jay Satiro <[email protected]> | |
http://curl.haxx.se/docs/copyright.html | |
https://gist.github.com/jay/97c8a7c20031997438dd7456e0a2b83a | |
*/ | |
/* | |
Results from Visual Studio debug build (debugger not attached): | |
------------------------------------------------------------------------------- | |
libcurl/7.69.0-DEV OpenSSL/1.0.2t nghttp2/1.40.0 | |
Asynchronous DNS (threaded) | |
... | |
Iteration 5 of 5: | |
Testing 24 consecutive easy transfers to same host WITHOUT reusing handle. | |
It took 3.572 seconds to complete (easy) (reuse handle: NO) | |
(Multiple runs of this test averaged 3.544 seconds.) | |
Testing 24 consecutive easy transfers to same host WITH reusing handle. | |
It took 1.919 seconds to complete (easy) (reuse handle: YES) | |
(Multiple runs of this test averaged 1.899 seconds.) | |
Testing 24 concurrent easy transfers to same host from multi WITHOUT pipewait. | |
It took 0.655 seconds to complete (multi) (pipewait: NO) | |
(Multiple runs of this test averaged 0.583 seconds.) | |
Testing 24 concurrent easy transfers to same host from multi WITH pipewait. | |
It took 0.156 seconds to complete (multi) (pipewait: YES) | |
(Multiple runs of this test averaged 0.233 seconds.) | |
------------------------------------------------------------------------------- | |
Each test should be faster than the one before it. | |
In the case of multi without pipewait and threaded DNS resolver the completion | |
time may be an outlier if there are any outstanding DNS requests, as that could | |
cause libcurl to hang for several seconds or more until getaddrinfo returns. | |
Refer to issue https://github.com/curl/curl/issues/4852 | |
*/ | |
/* !checksrc! disable CPPCOMMENTS all */ | |
#define _CRT_SECURE_NO_WARNINGS | |
#ifdef _WIN32 | |
#include <windows.h> | |
#endif | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <time.h> | |
#include <assert.h> | |
#include <curl/curl.h> | |
#ifndef _WIN32 | |
#include <unistd.h> | |
#endif | |
#include <iostream> | |
#include <string> | |
using namespace std; | |
/* For each test make this many transfers */ | |
#define MAX_TRANSFERS 24 | |
/* Show statistics after each transfer */ | |
//#define SHOW_STATS | |
#ifdef _WIN32 | |
#define WAITMS(x) Sleep(x) | |
#else | |
#define WAITMS(x) usleep((x) * 1000) | |
#endif | |
unsigned long long get_tick_count_ms() | |
{ | |
#ifdef _WIN32 | |
return (unsigned long long)GetTickCount64(); | |
#else | |
struct timespec ts; | |
unsigned long long ticks; | |
#ifdef CLOCK_MONOTONIC_RAW | |
clock_gettime(CLOCK_MONOTONIC_RAW, &ts); | |
#else | |
clock_gettime(CLOCK_MONOTONIC, &ts); | |
#endif | |
ticks = (unsigned long long)(ts.tv_sec * 1000) + | |
(unsigned long long)(ts.tv_nsec / 1000000); | |
return ticks; | |
#endif | |
} | |
size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) | |
{ | |
(void)ptr; | |
(void)userdata; | |
//printf("write_callback received %Iu bytes\n", size * nmemb); | |
return (size * nmemb); | |
} | |
struct user | |
{ | |
int handle_num; | |
char *errbuf; | |
curl_slist *hosts; | |
/* minfo is data saved when the easy handle is in a multi */ | |
struct minfo { | |
bool done; | |
CURLcode result; /* valid if done == true */ | |
} minfo; | |
}; | |
CURL *create_easy(int handle_num) | |
{ | |
CURL *easy = curl_easy_init(); | |
struct user *user = (struct user *)calloc(1, sizeof(*user)); | |
user->handle_num = handle_num; | |
user->errbuf = (char *)calloc(1, CURL_ERROR_SIZE); | |
curl_easy_setopt(easy, CURLOPT_ERRORBUFFER, user->errbuf); | |
user->hosts = curl_slist_append(NULL, "www.example.com:443:127.0.0.1"); | |
user->hosts = curl_slist_append(user->hosts, "www.example.com:80:127.0.0.1"); | |
//curl_easy_setopt(easy, CURLOPT_RESOLVE, user->hosts); | |
curl_easy_setopt(easy, CURLOPT_URL, "https://www.example.com"); | |
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback); | |
curl_easy_setopt(easy, CURLOPT_PRIVATE, user); | |
//curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L); | |
/* CURLOPT_CAINFO | |
To verify SSL sites you may need to load a bundle of certificates. | |
You can download the default bundle here: | |
https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt | |
However your SSL backend might use a database in addition to or instead of | |
the bundle. | |
https://curl.haxx.se/docs/ssl-compared.html | |
*/ | |
//curl_easy_setopt(easy, CURLOPT_CAINFO, "curl-ca-bundle.crt"); | |
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0L); | |
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 0L); | |
/* POST the transfer number to the server, for debug purposes */ | |
char data[20 + 1]; | |
sprintf(data, "%0*d", (int)(sizeof(data) - 1), handle_num); | |
curl_easy_setopt(easy, CURLOPT_COPYPOSTFIELDS, data); | |
return easy; | |
} | |
void free_easy(CURL *easy) | |
{ | |
struct user *user = NULL; | |
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &user); | |
curl_slist_free_all(user->hosts); | |
free(user->errbuf); | |
free(user); | |
curl_easy_cleanup(easy); | |
} | |
void show_stats(CURL *easy) | |
{ | |
struct user *user = NULL; | |
char *url = NULL; | |
long code = 0; | |
double average_speed = 0; | |
double bytes_downloaded = 0; | |
double total_download_time = 0; | |
curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &url); | |
curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &code); | |
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &user); | |
printf("Handle c[%u]: [HTTP %lu] %s\n", user->handle_num, code, url); | |
curl_easy_getinfo(easy, CURLINFO_SPEED_DOWNLOAD, &average_speed); | |
curl_easy_getinfo(easy, CURLINFO_SIZE_DOWNLOAD, &bytes_downloaded); | |
curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &total_download_time); | |
printf("Transfer rate: %.0f KB/sec (%.0f bytes in %.0f seconds)\n", | |
average_speed / 1024, bytes_downloaded, total_download_time); | |
#if 0 | |
#define SHOWTIME(x) do { \ | |
curl_off_t microsec = 0; \ | |
curl_easy_getinfo(easy, x, µsec); \ | |
printf("%s: %d ms\n", #x, (int)(microsec / 1000)); \ | |
} while(0) | |
SHOWTIME(CURLINFO_NAMELOOKUP_TIME_T); | |
SHOWTIME(CURLINFO_CONNECT_TIME_T); | |
SHOWTIME(CURLINFO_APPCONNECT_TIME_T); | |
SHOWTIME(CURLINFO_PRETRANSFER_TIME_T); | |
SHOWTIME(CURLINFO_STARTTRANSFER_TIME_T); | |
SHOWTIME(CURLINFO_TOTAL_TIME_T); | |
SHOWTIME(CURLINFO_REDIRECT_TIME_T); | |
#endif | |
} | |
unsigned test_easy(bool reuse) | |
{ | |
CURL *easy = NULL; | |
CURL *c[MAX_TRANSFERS] = { 0, }; | |
printf("Testing %u consecutive easy transfers " | |
"to same host %s reusing handle.\n", | |
MAX_TRANSFERS, (reuse ? "WITH" : "WITHOUT")); | |
if(reuse) | |
easy = create_easy(0); | |
else { | |
for(int i = 0; i < MAX_TRANSFERS; ++i) | |
c[i] = create_easy(i); | |
} | |
unsigned long long start = get_tick_count_ms(); | |
for(int i = 0; i < MAX_TRANSFERS; ++i) { | |
struct user *user; | |
CURL *hnd = reuse ? easy : c[i]; | |
CURLcode result = curl_easy_perform(hnd); | |
curl_easy_getinfo(hnd, CURLINFO_PRIVATE, &user); | |
/* When an easy handle is reused, the statistics, result code and error | |
buffer data will change. Though statistics could be shown here before | |
the handle is reused it would affect the elapsed time calculation. In | |
the case of an error the elapsed time is somewhat irrelevant (ie the | |
test has failed) and the error is shown here so all errors are shown. */ | |
if(result != CURLE_OK) { | |
fprintf(stderr, "Error: Transfer %d, Handle c[%u]: libcurl: (%d) %s\n", | |
i, user->handle_num, result, | |
(user->errbuf[0] ? user->errbuf : | |
curl_easy_strerror(result))); | |
} | |
/* Typically you'd also check the HTTP code here however this example POSTs | |
arbitrary data to example.com so we don't expect a specific code. */ | |
} | |
unsigned elapsed = (unsigned)(get_tick_count_ms() - start); | |
#ifdef SHOW_STATS | |
printf("\n"); | |
#endif | |
if(reuse) { | |
#ifdef SHOW_STATS | |
printf("No stats available for easy handle reuse.\n\n"); | |
#endif | |
free_easy(easy); | |
} | |
else { | |
for(int i = 0; i < MAX_TRANSFERS; ++i) { | |
#ifdef SHOW_STATS | |
show_stats(c[i]); | |
printf("\n"); | |
#endif | |
free_easy(c[i]); | |
} | |
} | |
printf("It took %u.%03u seconds to complete (easy) (reuse handle: %s)\n", | |
elapsed/1000, elapsed%1000, (reuse ? "YES" : "NO")); | |
return elapsed; | |
} | |
unsigned test_multi(bool pipewait) | |
{ | |
CURL *c[MAX_TRANSFERS] = { 0, }; | |
CURLM *multi = curl_multi_init(); | |
printf("Testing %u concurrent easy transfers " | |
"to same host from multi %s pipewait.\n", | |
MAX_TRANSFERS, (pipewait ? "WITH" : "WITHOUT")); | |
for(int i = 0; i < MAX_TRANSFERS; ++i) { | |
c[i] = create_easy(i); | |
curl_easy_setopt(c[i], CURLOPT_PIPEWAIT, pipewait ? 1L : 0L); | |
curl_multi_add_handle(multi, c[i]); | |
} | |
unsigned long long start = get_tick_count_ms(); | |
#if 0 | |
int still_running = 0; /* keep number of running handles */ | |
int repeats = 0; | |
while(!curl_multi_perform(multi, &still_running) && still_running) { | |
CURLMcode mc; /* curl_multi_wait() return code */ | |
int numfds = 0; | |
mc = curl_multi_wait(multi, NULL, 0, 1000, &numfds); | |
if(mc != CURLM_OK) { | |
fprintf(stderr, "curl_multi_wait() failed, code %d.\n", mc); | |
break; | |
} | |
/* 'numfds' being zero means either a timeout or no file descriptors to | |
wait for. Try timeout on first occurrence, then assume no file | |
descriptors and no file descriptors to wait for means wait for 100 | |
milliseconds. */ | |
if(!numfds) { | |
repeats++; /* count number of repeated zero numfds */ | |
if(repeats > 1) { | |
WAITMS(100); /* sleep 100 milliseconds */ | |
} | |
} | |
else | |
repeats = 0; | |
} | |
#else | |
for(;;) { | |
int still_running; | |
if(curl_multi_poll(multi, NULL, 0, 1000, NULL) != CURLM_OK || | |
curl_multi_perform(multi, &still_running) != CURLM_OK || | |
!still_running) | |
break; | |
} | |
#endif | |
unsigned elapsed = (unsigned)(get_tick_count_ms() - start); | |
for(;;) { | |
int qc; | |
struct CURLMsg *m = curl_multi_info_read(multi, &qc); | |
if(!m) | |
break; | |
if(m->msg == CURLMSG_DONE) { | |
struct user *user; | |
curl_easy_getinfo(m->easy_handle, CURLINFO_PRIVATE, &user); | |
user->minfo.done = true; | |
user->minfo.result = m->data.result; | |
} | |
} | |
for(int i = 0; i < MAX_TRANSFERS; ++i) { | |
struct user *user; | |
curl_easy_getinfo(c[i], CURLINFO_PRIVATE, &user); | |
if(user->minfo.done) { | |
if(user->minfo.result) { | |
fprintf(stderr, "Error: Handle c[%u]: libcurl: (%d) %s\n", | |
user->handle_num, user->minfo.result, | |
(user->errbuf[0] ? user->errbuf : | |
curl_easy_strerror(user->minfo.result))); | |
} | |
} | |
else { | |
fprintf(stderr, "Error: Handle c[%u]: Transfer incomplete.\n", | |
user->handle_num); | |
} | |
} | |
#ifdef SHOW_STATS | |
printf("\n"); | |
#endif | |
for(int i = 0; i < MAX_TRANSFERS; ++i) { | |
#ifdef SHOW_STATS | |
show_stats(c[i]); | |
printf("\n"); | |
#endif | |
curl_multi_remove_handle(multi, c[i]); | |
free_easy(c[i]); | |
} | |
printf("It took %u.%03u seconds to complete (multi) (pipewait: %s)\n", | |
elapsed/1000, elapsed%1000, (pipewait ? "YES" : "NO")); | |
curl_multi_cleanup(multi); | |
return elapsed; | |
} | |
int main(void) | |
{ | |
if(curl_global_init(CURL_GLOBAL_ALL)) { | |
fprintf(stderr, "Fatal: The initialization of libcurl has failed.\n"); | |
return EXIT_FAILURE; | |
} | |
if(atexit(curl_global_cleanup)) { | |
fprintf(stderr, "Fatal: atexit failed to register curl_global_cleanup.\n"); | |
curl_global_cleanup(); | |
return EXIT_FAILURE; | |
} | |
printf("\n\n\n%s\n", curl_version()); | |
curl_version_info_data *ver = curl_version_info(CURLVERSION_NOW); | |
if(!(ver->features & CURL_VERSION_ASYNCHDNS)) | |
printf("Synchronous DNS\n"); | |
else if(!ver->age || ver->ares_num) | |
printf("Asynchronous DNS (c-ares)\n"); | |
else | |
printf("Asynchronous DNS (threaded)\n"); | |
printf("\n\n\n"); | |
unsigned waitms = 3000; | |
printf("There will be a brief delay of %u ms after each test.\n\n", waitms); | |
struct { | |
unsigned (*func)(bool); | |
bool param; | |
unsigned accumulated_elapsed; | |
} test[] = { | |
{ test_easy, false }, | |
{ test_easy, true }, | |
{ test_multi, false }, | |
{ test_multi, true } | |
}; | |
int max = 5; | |
for(int i = 0; i < max; ++i) { | |
printf("\n\nIteration %d of %d:\n\n", i + 1, max); | |
for(int k = 0; k < (int)(sizeof(test) / sizeof(test[0])); ++k) { | |
test[k].accumulated_elapsed += test[k].func(test[k].param); | |
if(i + 1 == max) { | |
unsigned average = test[k].accumulated_elapsed / (unsigned)max; | |
printf("(Multiple runs of this test averaged %u.%03u seconds.)\n", | |
average/1000, average%1000); | |
} | |
printf("\n"); | |
WAITMS(waitms); | |
} | |
} | |
return EXIT_SUCCESS; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment