Skip to content

Instantly share code, notes, and snippets.

@jay
Last active October 31, 2021 05:09
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jay/97c8a7c20031997438dd7456e0a2b83a to your computer and use it in GitHub Desktop.
Save jay/97c8a7c20031997438dd7456e0a2b83a to your computer and use it in GitHub Desktop.
Use libcurl to test multi vs easy performance.
/* 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 <raysatiro@yahoo.com>
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, &microsec); \
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