/*
* Copyright (C) 2000, 2001 Nominum, Inc.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM
* DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
* INTERNET SOFTWARE CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
* FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
* NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
* WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* Copyright (C) 2004 - 2006 Nominum, Inc.
*
* Permission to use, copy, modify, and distribute this software and its
* documentation for any purpose with or without fee is hereby granted,
* provided that the above copyright notice and this permission notice
* appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
* OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/***
*** DNS Performance Testing Tool
***
*** Version $Id: dnsperf.c 73079 2006-08-15 23:02:37Z bwelling $
***/
#include "common.h"
/*
* Configuration defaults
*/
#define DEF_MAX_QUERIES_OUTSTANDING 20
#define DEF_QUERY_TIMEOUT 5 /* in seconds */
/*
* Other constants / definitions
*/
#define RESPONSE_BLOCKING_WAIT_TIME_US 100000 /* microseconds */
#define THROTTLE_DELAY 25000 /* microseconds */
#define UPDATE_TIME_FREQUENCY 100
/* Categories into which responses get categorized in the histogram. */
#define CATEGORY_SUCCESS 0
#define CATEGORY_FAILURE 1
#define N_CATEGORIES 2
struct histogram_bucket {
unsigned int categories[2];
};
static char category_markers[N_CATEGORIES] = { '#', '-' };
/*
* Configuration options (global)
*/
static isc_uint32_t max_queries_outstanding = DEF_MAX_QUERIES_OUTSTANDING;
static double query_timeout = DEF_QUERY_TIMEOUT;
static isc_uint32_t max_qps_limit; /* init 0 */
static isc_boolean_t run_only_once = ISC_FALSE;
static isc_boolean_t use_timelimit = ISC_FALSE;
static unsigned int run_timelimit; /* init 0 */
static isc_boolean_t countrcodes = ISC_FALSE;
static int rcodecounts[16] = {0};
static isc_uint32_t histogram_nbuckets = 0;
static isc_uint32_t histogram_time = 1; /* in seconds */
static struct histogram_bucket *histogram_buckets;
static struct histogram_bucket histogram_overflow;
static double histogram_bps; /* buckets per second */
static double latency_sum = 0;
static double latency_sum_squares = 0;
static double latency_min = 5000000;
static double latency_max = 0;
/*
* Other global stuff
*/
static isc_boolean_t setup_phase = ISC_TRUE;
static unsigned int runs_through_file; /* init 0 */
static unsigned int num_queries_timed_out; /* init 0 */
static unsigned int num_queries_interrupted; /* init 0 */
static int num_responses_since_update_time; /* init 0 */
static isc_boolean_t interrupted = ISC_FALSE;
static isc_boolean_t want_stop = ISC_FALSE;
/*
* Handle a SIGINT.
*/
static void
handle_interrupt(int sig)
{
(void)sig;
interrupted = ISC_TRUE;
}
/*
* Install interrupt handler. If this fails, we don't care.
*/
static void
install_interrupt_handler(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = handle_interrupt;
if (sigfillset(&sa.sa_mask) != 0)
return;
if (sigaction(SIGINT, &sa, NULL) < 0)
return;
}
/*
* show_startup_info:
* Show name/version
*/
static void
show_startup_info(void) {
printf("\n"
"DNS Performance Testing Tool\n\n"
"Nominum Version " VERSION "\n\n");
}
/* Print program-specific usage */
void
show_usage_prog(void) {
fprintf(stderr,
" [-q num_queries] [-t timeout] [-Q max_qps] [-1] [-l limit]\n"
" [-c] [-H histogram_buckets] [-T histogram_seconds] [-u]\n");
}
/* Print program-specific option descriptions */
void
show_options_prog(void) {
fprintf(stderr,
" -q specifies the maximum number of queries outstanding (default: %d)\n"
" -t specifies the timeout for query completion in seconds (default: %d)\n"
" -Q limit the number of queries per second (no default)\n"
" -1 run through input only once (default: multiple iff limit given)\n"
" -l specifies how a limit for how long to run tests in seconds (no default)\n"
" -c print the number of packets with each rcode\n"
" -H print a response latency histogram with this many buckets\n"
" -T include latencies up to this many seconds in histogram (use with -H)\n"
" -u send dynamic updates instead of queries\n"
" -v verbose: report the RCODE of each response on stdout\n",
DEF_MAX_QUERIES_OUTSTANDING,
DEF_QUERY_TIMEOUT);
}
/* Handle program-specific options */
isc_result_t
handle_prog_opt(int c) {
isc_result_t result;
switch (c) {
case 'q':
result = isc_parse_uint32(&max_queries_outstanding,
optarg, 10);
if (result != ISC_R_SUCCESS) {
fprintf(stderr, "Invalid number of "
"outstanding queries: %s\n",
optarg);
return (ISC_R_FAILURE);
}
break;
case 't':
result = parse_udouble(&query_timeout, optarg);
if (result != ISC_R_SUCCESS) {
fprintf(stderr, "Invalid query timeout "
"(positive floating point value "
"expected): %s\n", optarg);
return (ISC_R_FAILURE);
}
break;
case 'Q':
result = isc_parse_uint32(&max_qps_limit, optarg, 10);
if (result != ISC_R_SUCCESS) {
fprintf(stderr, "Invalid max queries per "
"second: %s\n", optarg);
return (ISC_R_FAILURE);
}
break;
case '1':
run_only_once = ISC_TRUE;
break;
case 'l':
result = isc_parse_uint32(&run_timelimit, optarg, 10);
if (result != ISC_R_SUCCESS) {
fprintf(stderr, "Invalid time limit: %s\n",
optarg);
return (ISC_R_FAILURE);
}
use_timelimit = ISC_TRUE;
break;
case 'c':
countrcodes = ISC_TRUE;
break;
case 'H':
result = isc_parse_uint32(&histogram_nbuckets,
optarg, 10);
if (result != ISC_R_SUCCESS) {
fprintf(stderr, "Invalid number of histogram "
"buckets: %s\n",
optarg);
return (ISC_R_FAILURE);
}
break;
case 'T':
result = isc_parse_uint32(&histogram_time,
optarg, 10);
if (result != ISC_R_SUCCESS) {
fprintf(stderr, "Invalid histogram time: %s\n",
optarg);
return (ISC_R_FAILURE);
}
break;
case 'u':
update_mode = ISC_TRUE;
break;
case 'v':
verbose = ISC_TRUE;
break;
default:
return (ISC_R_NOTFOUND);
}
return (ISC_R_SUCCESS);
}
static void
init_histogram(void) {
if (histogram_nbuckets == 0)
return;
histogram_buckets = calloc(histogram_nbuckets,
sizeof(struct histogram_bucket));
if (histogram_buckets == NULL) {
fprintf(stderr, "Out of memory\n");
exit(1);
}
histogram_bps = (double) histogram_nbuckets / histogram_time;
}
/* Print a single histogram bucket. */
static void
print_bucket(struct histogram_bucket *bucket, int maxval, const char *op,
double time)
{
int hashes;
int j, k;
printf("%s%8.3fs", op, time);
for (k = 0; k < N_CATEGORIES; k++) {
printf("%6d", bucket->categories[k]);
}
printf(" |");
for (k = 0; k < N_CATEGORIES; k++) {
hashes = (int) floor(60.0 * bucket->categories[k] / maxval);
for (j = 0; j < hashes; j++)
putchar(category_markers[k]);
}
printf("\n");
}
static void
print_histogram(void) {
int total = 0;
int maxval = 1;
unsigned int i, j;
int bucket_total;
int count;
if (histogram_nbuckets == 0)
return;
for (i = 0; i < histogram_nbuckets; i++) {
bucket_total = 0;
for (j = 0; j < N_CATEGORIES; j++) {
count = histogram_buckets[i].categories[j];
total += count;
bucket_total += count;
}
if (bucket_total > maxval)
maxval = bucket_total;
}
bucket_total = 0;
for (j = 0; j < N_CATEGORIES; j++) {
count = histogram_overflow.categories[j];
total += count;
bucket_total += count;
}
if (bucket_total > maxval)
maxval = bucket_total;
printf("Latency: Min: %f s; Max: %f s; Avg: %f s; StdDev: %f\n",
latency_min, latency_max, latency_sum / total,
sqrt((latency_sum_squares - ((latency_sum * latency_sum)/total))
/(total-1)));
printf("\nResponse latency distribution (total %d responses):\n\n",
total);
printf("Latency Success Fail |\n");
for (i = 0; i < histogram_nbuckets; i++) {
print_bucket(&histogram_buckets[i], maxval, "< ",
(i + 1) / histogram_bps);
}
print_bucket(&histogram_overflow, maxval, ">=",
histogram_nbuckets / histogram_bps);
printf("\nLegend:\n\n");
printf("##### = success responses (RCODE was NOERROR or NXDOMAIN)\n");
printf("----- = failure responses (any other RCODE)\n");
}
/*
* timelimit_reached:
* Have we reached the time limit (if any)?
*
* Returns ISC_FALSE if there is no time limit or if we have not reached it
* Returns ISC_TRUE otherwise
*/
static isc_boolean_t
timelimit_reached(void) {
if (! use_timelimit || setup_phase)
return (ISC_FALSE);
return ISC_TF(difftv(&time_now, &time_of_program_start) >
(double)run_timelimit);
}
/*
* keep_sending:
* Should we keep sending queries or stop here?
*
* Return ISC_TRUE if we should keep on sending queries
* Return ISC_FALSE if we should stop
*
* Side effects:
* Rewinds the input and clears reached_end_input if we have reached the
* end of the input, but we are meant to run through it multiple times
* and have not hit the time limit yet (if any is set).
*/
static isc_boolean_t
keep_sending(isc_boolean_t *reached_end_input) {
int at_timelimit;
if (want_stop)
return (ISC_FALSE);
at_timelimit = timelimit_reached();
if (!(*reached_end_input) && !at_timelimit)
return (ISC_TRUE);
else if (*reached_end_input && !run_only_once && !at_timelimit) {
rewind(datafile);
*reached_end_input = ISC_FALSE;
runs_through_file++;
return (ISC_TRUE);
} else {
if (*reached_end_input)
runs_through_file++;
want_stop = ISC_TRUE;
return (ISC_FALSE);
}
}
/*
* register_response:
* Register receipt of a query
*
* Removes the record for the given query id in status[] if any exists.
*
* Returns ISC_TRUE if the response was for an outstanding query
* Returns ISC_FALSE otherwise.
*/
isc_boolean_t
register_response(unsigned int id, unsigned int rcode) {
struct query_status *s = &status[id];
if (s->list != &outstanding_list) {
fprintf(stderr, "Warning: Received a response with an "
"unexpected (maybe timed out) id: %u\n", id);
return (ISC_FALSE);
}
ISC_LIST_UNLINK(outstanding_list, s, link);
ISC_LIST_APPEND(instanding_list, s, link);
s->list = &instanding_list;
num_queries_outstanding--;
if (s->desc != NULL) {
printf("> %s %s\n", rcode_strings[rcode], s->desc);
free(s->desc);
s->desc = NULL;
}
if (countrcodes)
rcodecounts[rcode]++;
if (histogram_nbuckets) {
double latency;
int buckno;
struct histogram_bucket *bucket;
int category;
latency = difftv(&time_now, &s->sent_timestamp);
latency_sum += latency;
latency_sum_squares += (latency * latency);
if (latency < latency_min)
latency_min = latency;
if (latency > latency_max)
latency_max = latency;
buckno = (int) floor(latency * histogram_bps);
if (buckno < 0)
buckno = 0;
if ((unsigned int) buckno < histogram_nbuckets)
bucket = &histogram_buckets[buckno];
else
bucket = &histogram_overflow;
if (rcode == dns_rcode_noerror || rcode == dns_rcode_nxdomain)
category = CATEGORY_SUCCESS;
else
category = CATEGORY_FAILURE;
bucket->categories[category]++;
}
return (ISC_TRUE);
}
/*
* Wait for a response from the server.
*
* Returns whether we have a response to read.
*/
static isc_boolean_t
wait_for_response(isc_boolean_t throttling) {
fd_set read_fds;
struct timeval tv;
int retval;
FD_ZERO(&read_fds);
FD_SET(query_socket, &read_fds);
tv.tv_sec = 0;
if (throttling)
tv.tv_usec = THROTTLE_DELAY;
else
tv.tv_usec = RESPONSE_BLOCKING_WAIT_TIME_US;
retval = select(query_socket + 1, &read_fds, NULL, NULL, &tv);
if (retval < 0) {
if (errno == EINTR)
return (ISC_FALSE);
fprintf(stderr, "select() failed\n");
exit(1);
}
update_current_time();
return ISC_TF(FD_ISSET(query_socket, &read_fds));
}
/*
* need_throttle:
* Determine if we've exceeded the maximum queries per second limit.
*/
static isc_boolean_t
need_throttle(void) {
double runtime;
runtime = difftv(&time_now, &time_of_program_start);
return ISC_TF((runtime > 0.1 &&
num_queries_sent > max_qps_limit * runtime));
}
/*
* retire_old_queries:
* Go through the list of open queries and remove any queries which are
* older than the timeout, decrementing the number of queries
* outstanding for each one removed.
*/
static void
retire_old_queries(void) {
struct query_status *s;
while (ISC_TRUE) {
s = ISC_LIST_HEAD(outstanding_list);
if (s == NULL ||
(difftv(&time_now, &s->sent_timestamp) < query_timeout))
break;
ISC_LIST_UNLINK(outstanding_list, s, link);
ISC_LIST_APPEND(instanding_list, s, link);
s->list = &instanding_list;
num_queries_outstanding--;
num_queries_timed_out++;
if (s->desc != NULL) {
printf("> T %s\n", s->desc);
free(s->desc);
s->desc = NULL;
} else {
printf("[Timeout] %s timed out: msg id %u\n",
update_mode ? "Update" : "Query",
(unsigned int)(s - status));
}
}
}
/*
* cancel_queries:
* Go through the list of open queries and remove any queries which are
* outstanding, decrementing the number of queries outstanding for each
* one removed.
*/
static void
cancel_queries(void) {
struct query_status *s;
while (ISC_TRUE) {
s = ISC_LIST_HEAD(outstanding_list);
if (s == NULL)
break;
ISC_LIST_UNLINK(outstanding_list, s, link);
ISC_LIST_APPEND(instanding_list, s, link);
s->list = &instanding_list;
num_queries_outstanding--;
num_queries_interrupted++;
if (s->desc != NULL) {
printf("> I %s\n", s->desc);
free(s->desc);
s->desc = NULL;
} else {
printf("[Interrupt] %s interrupted: msg id %u\n",
update_mode ? "Update" : "Query",
(unsigned int)(s - status));
}
}
}
/*
* print_statistics:
* Print out statistics based on the results of the test
*/
static void
print_statistics(void) {
unsigned int num_queries_completed;
double per_lost, per_completed;
double run_time, queries_per_sec;
time_t start_secs, end_secs;
isc_uint64_t avg_request_size, avg_response_size;
const char *units_cap, *units;
unsigned int i;
units = update_mode ? "updates" : "queries";
units_cap = update_mode ? "Updates" : "Queries";
num_queries_completed = num_queries_sent - num_queries_timed_out -
num_queries_interrupted;
if (num_queries_completed == 0) {
per_lost = 0.0;
per_completed = 0.0;
avg_response_size = 0;
} else {
per_lost = 100.0 * (double)num_queries_timed_out
/ (double)num_queries_sent;
per_completed = 100.0 - per_lost;
avg_response_size = total_response_size /
num_queries_completed;
}
run_time = difftv(&time_of_end_of_run, &time_of_program_start);
if (num_queries_sent == 0) {
queries_per_sec = 0.0;
avg_request_size = 0;
} else {
queries_per_sec = (double)num_queries_completed / run_time;
avg_request_size = total_request_size / num_queries_sent;
}
printf("\n");
printf("Statistics:\n\n");
printf(" Parse input file: %s\n",
(run_only_once ? "once" : "multiple times"));
if (use_timelimit)
printf(" Run time limit: %u seconds\n", run_timelimit);
if (!run_only_once)
printf(" Ran through file: %u times\n",
runs_through_file);
else {
const char *reason;
if (interrupted)
reason = "interruption";
else if (runs_through_file == 0)
reason = "reaching time limit";
else
reason = "reaching end of file";
printf(" Ended due to: %s\n", reason);
}
printf("\n");
printf(" %s sent: %u %s\n",
units_cap, num_queries_sent, units);
printf(" %s completed: %u %s\n",
units_cap, num_queries_completed, units);
printf(" %s lost: %u %s\n",
units_cap, num_queries_timed_out, units);
if (num_queries_interrupted > 0)
printf(" %s interrupted: %u %s\n",
units_cap, num_queries_interrupted, units);
printf("\n");
if (countrcodes) {
for (i = 0; i < 16; i++) {
if (rcodecounts[i] == 0)
continue;
printf(" Returned %8s: %u %s\n",
rcode_strings[i], rcodecounts[i], units);
}
printf("\n");
}
printf(" Avg request size: %u bytes\n",
(unsigned int)avg_request_size);
printf(" Avg response size: %u bytes\n",
(unsigned int)avg_response_size);
printf("\n");
printf(" Percentage completed: %6.2lf%%\n", per_completed);
printf(" Percentage lost: %6.2lf%%\n", per_lost);
printf("\n");
start_secs = time_of_program_start.tv_sec;
end_secs = time_of_end_of_run.tv_sec;
printf(" Started at: %s", ctime(&start_secs));
printf(" Finished at: %s", ctime(&end_secs));
printf(" Ran for: %.6lf seconds\n", run_time);
printf("\n");
printf(" %s per second: %.6lf %cps\n",
units_cap, queries_per_sec, units[0]);
printf("\n");
}
static void
maybe_update_current_time() {
/*
* We don't want to call gettimeofday() every time a packet is sent
* or received, as it's expensive, so we keep a cached copy of the
* time when we last checked.
*
* We check the current time after blocking, and also keep a count
* of the number of packets we've read since we last blocked so that
* if we never block, we can still update the cached time periodically.
*/
num_responses_since_update_time++;
if (num_responses_since_update_time > UPDATE_TIME_FREQUENCY) {
num_responses_since_update_time = 0;
update_current_time();
}
}
/*
* Main program.
*/
int
main(int argc, char **argv) {
isc_boolean_t got_eof = ISC_FALSE, keepsending;
char line[MAX_INPUT_LEN + 1];
isc_result_t result;
show_startup_info();
if (setup(argc, argv, "q:t:Q:1l:cH:T:uv") != ISC_R_SUCCESS)
exit(1);
if (!run_only_once && !use_timelimit)
run_only_once = ISC_TRUE;
init_histogram();
install_interrupt_handler();
printf("[Status] Processing input data\n");
update_current_time();
time_of_program_start = time_now;
while (!interrupted) {
int burst;
isc_boolean_t throttle;
keepsending = keep_sending(&got_eof);
if (! keepsending && num_queries_outstanding == 0)
break;
burst = 0;
throttle = ISC_FALSE;
while (keepsending && !interrupted &&
num_queries_outstanding < max_queries_outstanding)
{
if (max_qps_limit > 0) {
throttle = need_throttle();
if (throttle)
break;
}
if (get_input_line(line, sizeof(line)) !=
ISC_R_SUCCESS)
{
got_eof = ISC_TRUE;
if (setup_phase) {
fprintf(stderr, "Input file "
"contains no queries.\n");
exit(1);
}
break;
}
result = process_line(line);
if (result == ISC_R_SUCCESS && setup_phase) {
setup_phase = ISC_FALSE;
printf("[Status] Sending %s (to %s)\n",
update_mode ? "updates" : "queries", server_addr_str);
}
/*
* This implements TCP-like "slow start".
*/
if (++burst == 2)
break;
}
if (num_queries_outstanding || throttle) {
for (;;) {
if (try_process_response(query_socket)) {
maybe_update_current_time();
break;
} else {
num_responses_since_update_time = 0;
}
/*
* There was nothing on the socket. We have
* free time. Use it to retire old queries,
* and then drop into select().
*/
retire_old_queries();
if (!wait_for_response(throttle))
break;
}
}
}
update_current_time();
time_of_end_of_run = time_now;
if (interrupted)
cancel_queries();
printf("[Status] Testing complete\n");
maybe_print_args(argc, argv);
print_statistics();
print_histogram();
cleanup();
return (0);
}
syntax highlighted by Code2HTML, v. 0.9.1