Small tool to benchmark webservers
https://redmine.lighttpd.net/projects/weighttp/wiki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
355 lines
8.2 KiB
355 lines
8.2 KiB
/* |
|
* weighttp - a lightweight and simple webserver benchmarking tool |
|
* |
|
* Author: |
|
* Copyright (c) 2009 Thomas Porzelt |
|
* |
|
* License: |
|
* MIT, see COPYING file |
|
*/ |
|
|
|
#include "weighttp.h" |
|
|
|
extern int optind, optopt; /* getopt */ |
|
|
|
static void show_help(void) { |
|
printf("weighttp <options> <url>\n"); |
|
printf(" -n num number of requests (mandatory)\n"); |
|
printf(" -k keep alive (default: no)\n"); |
|
printf(" -t num threadcount (default: 1)\n"); |
|
printf(" -c num concurrent clients (default: 1)\n"); |
|
printf(" -h show help and exit\n"); |
|
printf(" -v show version and exit\n\n"); |
|
} |
|
|
|
static struct addrinfo *resolve_host(char *hostname, uint16_t port) { |
|
int err; |
|
char port_str[6]; |
|
struct addrinfo hints, *res, *res_first, *res_last; |
|
|
|
memset(&hints, 0, sizeof(hints)); |
|
hints.ai_family = PF_UNSPEC; |
|
hints.ai_socktype = SOCK_STREAM; |
|
|
|
sprintf(port_str, "%d", port); |
|
|
|
err = getaddrinfo(hostname, port_str, &hints, &res_first); |
|
|
|
if (err) { |
|
W_ERROR("could not resolve hostname: %s", hostname); |
|
return NULL; |
|
} |
|
|
|
/* search for an ipv4 address, no ipv6 yet */ |
|
res_last = NULL; |
|
for (res = res_first; res != NULL; res = res->ai_next) { |
|
if (res->ai_family == AF_INET) |
|
break; |
|
|
|
res_last = res; |
|
} |
|
|
|
if (!res) { |
|
freeaddrinfo(res_first); |
|
W_ERROR("could not resolve hostname: %s", hostname); |
|
return NULL; |
|
} |
|
|
|
if (res != res_first) { |
|
/* unlink from list and free rest */ |
|
res_last->ai_next = res->ai_next; |
|
freeaddrinfo(res_first); |
|
res->ai_next = NULL; |
|
} |
|
|
|
return res; |
|
} |
|
|
|
static char *forge_request(char *url, char keep_alive, char **host, uint16_t *port) { |
|
char *c, *end; |
|
char *req; |
|
uint32_t len; |
|
|
|
*host = NULL; |
|
*port = 0; |
|
|
|
if (strncmp(url, "http://", 7) == 0) |
|
url += 7; |
|
else if (strncmp(url, "https://", 8) == 0) { |
|
W_ERROR("%s", "no ssl support yet"); |
|
url += 8; |
|
return NULL; |
|
} |
|
|
|
len = strlen(url); |
|
|
|
if ((c = strchr(url, ':'))) { |
|
/* found ':' => host:port */ |
|
*host = W_MALLOC(char, c - url + 1); |
|
memcpy(*host, url, c - url); |
|
(*host)[c - url] = '\0'; |
|
|
|
if ((end = strchr(c+1, '/'))) { |
|
*end = '\0'; |
|
*port = atoi(c+1); |
|
*end = '/'; |
|
url = end; |
|
} else { |
|
*port = atoi(c+1); |
|
url += len; |
|
} |
|
} else { |
|
*port = 80; |
|
|
|
if ((c = strchr(url, '/'))) { |
|
*host = W_MALLOC(char, c - url + 1); |
|
memcpy(*host, url, c - url); |
|
(*host)[c - url] = '\0'; |
|
url = c; |
|
} else { |
|
*host = W_MALLOC(char, len + 1); |
|
memcpy(*host, url, len); |
|
(*host)[len] = '\0'; |
|
url += len; |
|
} |
|
} |
|
|
|
if (*port == 0) { |
|
W_ERROR("%s", "could not parse url"); |
|
free(*host); |
|
return NULL; |
|
} |
|
|
|
if (*url == '\0') |
|
url = "/"; |
|
|
|
req = W_MALLOC(char, sizeof("GET HTTP/1.1\r\nHost: :65536\r\nConnection: keep-alive\r\n\r\n") + strlen(*host) + strlen(url)); |
|
|
|
strcpy(req, "GET "); |
|
strcat(req, url); |
|
strcat(req, " HTTP/1.1\r\nHost: "); |
|
strcat(req, *host); |
|
if (*port != 80) |
|
sprintf(req + strlen(req), ":%"PRIu16, *port); |
|
if (keep_alive) |
|
strcat(req, "\r\nConnection: keep-alive\r\n\r\n"); |
|
else |
|
strcat(req, "\r\nConnection: close\r\n\r\n"); |
|
|
|
return req; |
|
} |
|
|
|
uint64_t str_to_uint64(char *str) { |
|
uint64_t i; |
|
|
|
for (i = 0; *str; str++) { |
|
if (*str < '0' || *str > '9') |
|
return UINT64_MAX; |
|
|
|
i *= 10; |
|
i += *str - '0'; |
|
} |
|
|
|
return i; |
|
} |
|
|
|
int main(int argc, char *argv[]) { |
|
Worker **workers; |
|
pthread_t *threads; |
|
int i; |
|
char c; |
|
int err; |
|
struct ev_loop *loop; |
|
ev_tstamp ts_start, ts_end; |
|
Config config; |
|
Worker *worker; |
|
char *host; |
|
uint16_t port; |
|
uint16_t rest_concur, rest_req; |
|
Stats stats; |
|
ev_tstamp duration; |
|
int sec, millisec, microsec; |
|
uint64_t rps; |
|
uint64_t kbps; |
|
|
|
|
|
printf("weighttp - a lightweight and simple webserver benchmarking tool\n\n"); |
|
|
|
/* default settings */ |
|
config.thread_count = 1; |
|
config.concur_count = 1; |
|
config.req_count = 0; |
|
config.keep_alive = 0; |
|
|
|
while ((c = getopt(argc, argv, ":hvkn:t:c:")) != -1) { |
|
switch (c) { |
|
case 'h': |
|
show_help(); |
|
return 0; |
|
case 'v': |
|
printf("version: " VERSION "\n"); |
|
printf("build-date: " __DATE__ " " __TIME__ "\n\n"); |
|
return 0; |
|
case 'k': |
|
config.keep_alive = 1; |
|
break; |
|
case 'n': |
|
config.req_count = str_to_uint64(optarg); |
|
break; |
|
case 't': |
|
config.thread_count = atoi(optarg); |
|
break; |
|
case 'c': |
|
config.concur_count = atoi(optarg); |
|
break; |
|
case '?': |
|
W_ERROR("unkown option: -%c", optopt); |
|
show_help(); |
|
return 1; |
|
} |
|
} |
|
|
|
if ((argc - optind) < 1) { |
|
W_ERROR("%s", "missing url argument\n"); |
|
show_help(); |
|
return 1; |
|
} else if ((argc - optind) > 1) { |
|
W_ERROR("%s", "too many arguments\n"); |
|
show_help(); |
|
return 1; |
|
} |
|
|
|
/* check for sane arguments */ |
|
if (!config.thread_count) { |
|
W_ERROR("%s", "thread count has to be > 0\n"); |
|
show_help(); |
|
return 1; |
|
} |
|
if (!config.concur_count) { |
|
W_ERROR("%s", "number of concurrent clients has to be > 0\n"); |
|
show_help(); |
|
return 1; |
|
} |
|
if (!config.req_count) { |
|
W_ERROR("%s", "number of requests has to be > 0\n"); |
|
show_help(); |
|
return 1; |
|
} |
|
if (config.req_count == UINT64_MAX || config.thread_count > config.req_count || config.thread_count > config.concur_count || config.concur_count > config.req_count) { |
|
W_ERROR("%s", "insane arguments\n"); |
|
show_help(); |
|
return 1; |
|
} |
|
|
|
|
|
loop = ev_default_loop(0); |
|
if (!loop) { |
|
W_ERROR("%s", "could not initialize libev\n"); |
|
return 2; |
|
} |
|
|
|
if (NULL == (config.request = forge_request(argv[optind], config.keep_alive, &host, &port))) { |
|
return 1; |
|
} |
|
|
|
config.request_size = strlen(config.request); |
|
//printf("Request (%d):\n==========\n%s==========\n", config.request_size, config.request); |
|
//printf("host: '%s', port: %d\n", host, port); |
|
|
|
/* resolve hostname */ |
|
if(!(config.saddr = resolve_host(host, port))) { |
|
return 1; |
|
} |
|
|
|
/* spawn threads */ |
|
threads = W_MALLOC(pthread_t, config.thread_count); |
|
workers = W_MALLOC(Worker*, config.thread_count); |
|
|
|
rest_concur = config.concur_count % config.thread_count; |
|
rest_req = config.req_count % config.thread_count; |
|
|
|
printf("starting benchmark...\n"); |
|
|
|
memset(&stats, 0, sizeof(stats)); |
|
ts_start = ev_time(); |
|
|
|
for (i = 0; i < config.thread_count; i++) { |
|
uint64_t reqs = config.req_count / config.thread_count; |
|
uint16_t concur = config.concur_count / config.thread_count; |
|
uint64_t diff; |
|
|
|
if (rest_concur) { |
|
diff = (i == config.thread_count) ? rest_concur : (rest_concur / config.thread_count); |
|
diff = diff ? diff : 1; |
|
concur += diff; |
|
rest_concur -= diff; |
|
} |
|
|
|
if (rest_req) { |
|
diff = (i == config.thread_count) ? rest_req : (rest_req / config.thread_count); |
|
diff = diff ? diff : 1; |
|
reqs += diff; |
|
rest_req -= diff; |
|
} |
|
printf("spawning thread #%d: %"PRIu16" concurrent requests, %"PRIu64" total requests\n", i+1, concur, reqs); |
|
workers[i] = worker_new(i+1, &config, concur, reqs); |
|
|
|
if (!(workers[i])) { |
|
W_ERROR("%s", "failed to allocate worker or client"); |
|
return 1; |
|
} |
|
|
|
err = pthread_create(&threads[i], NULL, worker_thread, (void*)workers[i]); |
|
|
|
if (err != 0) { |
|
W_ERROR("failed spawning thread (%d)", err); |
|
return 2; |
|
} |
|
} |
|
|
|
for (i = 0; i < config.thread_count; i++) { |
|
err = pthread_join(threads[i], NULL); |
|
worker = workers[i]; |
|
|
|
if (err != 0) { |
|
W_ERROR("failed joining thread (%d)", err); |
|
return 3; |
|
} |
|
|
|
stats.req_started += worker->stats.req_started; |
|
stats.req_done += worker->stats.req_done; |
|
stats.req_success += worker->stats.req_success; |
|
stats.req_failed += worker->stats.req_failed; |
|
stats.bytes_total += worker->stats.bytes_total; |
|
stats.bytes_body += worker->stats.bytes_body; |
|
|
|
worker_free(worker); |
|
} |
|
|
|
ts_end = ev_time(); |
|
duration = ts_end - ts_start; |
|
sec = duration; |
|
duration -= sec; |
|
duration = duration * 1000; |
|
millisec = duration; |
|
duration -= millisec; |
|
microsec = duration * 1000; |
|
rps = stats.req_done / (ts_end - ts_start); |
|
kbps = stats.bytes_total / (ts_end - ts_start) / 1024; |
|
printf("\nfinished in %d sec, %d millisec and %d microsec, %"PRIu64" req/s, %"PRIu64" kbyte/s\n", sec, millisec, microsec, rps, kbps); |
|
printf("requests: %"PRIu64" total, %"PRIu64" started, %"PRIu64" done, %"PRIu64" succeeded, %"PRIu64" failed, %"PRIu64" errored\n", |
|
config.req_count, stats.req_started, stats.req_done, stats.req_success, stats.req_failed, stats.req_error |
|
); |
|
printf("traffic: %"PRIu64" bytes total, %"PRIu64" bytes http, %"PRIu64" bytes data\n", |
|
stats.bytes_total, stats.bytes_total - stats.bytes_body, stats.bytes_body |
|
); |
|
|
|
ev_default_destroy(); |
|
|
|
free(threads); |
|
free(workers); |
|
free(config.request); |
|
freeaddrinfo(config.saddr); |
|
|
|
return 0; |
|
}
|
|
|