/*
 * 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 Resolution Performance Testing Tool
 ***
 ***	Version $Id: resperf.c 74726 2006-09-28 16:52:58Z rsr $
 ***/

#include "common.h"

/*
 * Global stuff
 */

/* The target traffic level at the end of the ramp-up */
double max_qps = 100000.0;

/* The time period over which we ramp up traffic */
double ramp_time = 60.0;

/* How long to wait for responses after sending traffic */
double wait_time = 40.0;

/* When to stop waiting for respnoses (valid during wait phase only) */
double end_time;

/* Interval between plot data points, in seconds */
double bucket_interval = 0.5;

/* The number of plot data points */
int n_buckets;

/* The plot data file */
const char *plotfile = "resperf.gnuplot";

/* The largest acceptable query loss when reporting max throughput */
double max_loss_percent = 100.0;

/*
 * The last plot data point containing actual data; this can
 * be less than than (n_buckets - 1) if the traffic sending
 * phase is cut short
 */
int last_bucket_used;

/*
 * The statistics for queries sent during one bucket_interval
 * of the traffic sending phase.
 */
struct ramp_bucket {
	int queries;
	int responses;
	int failures;
	double latency_sum;
};

/* Pointer to array of n_buckets ramp_bucket structures */
struct ramp_bucket *buckets;

/*
 * If true, we are in the wait phase, that is, we have stopped sending
 * queries and are just waiting for any remaining responses.
 */
isc_boolean_t wait_phase = ISC_FALSE;

/* The time when the wait phase began */
struct timeval wait_phase_began;

/*
 * show_startup_info:
 *   Show name/version
 */
static void
show_startup_info(void) {
	printf("\n"
"DNS Resolution Performance Testing Tool\n\n"
"Nominum Version " VERSION "\n\n");
}

/* Print program-specific usage */

void
show_usage_prog(void) {
	fprintf(stderr,
"               [-i plot_interval] [-m max_qps] [-P plotfile]\n"
"               [-r rampup_time] [-L max_loss]\n");
}

/* Print program-specific option descriptions */

void
show_options_prog(void) {
	fprintf(stderr,
"  -i specifies the time interval between plot data points, "
		"in seconds (default: %.1f)\n"
"  -m specifies the maximum number of queries per second (default: %.0f)\n"
"  -P specifies the name of the plot data file (default: %s)\n"
"  -r specifies the ramp-up time in seconds (default: %.0f)\n"
"  -L specifies the maximum acceptable query loss, "
		"in percent (default: %.0f)\n",
		bucket_interval, max_qps, plotfile, ramp_time,
		max_loss_percent);
}

/* Handle program-specific options */

isc_result_t
handle_prog_opt(int c) {
	isc_result_t result;

	switch (c) {
	case 'i':
		result = parse_udouble(&bucket_interval, optarg);
		if (result != ISC_R_SUCCESS) {
			fprintf(stderr, "Invalid plot interval "
				"(positive floating point value "
				"expected): %s\n", optarg);
			return (ISC_R_FAILURE);
		}
		break;
	case 'm':
		result = parse_udouble(&max_qps, optarg);
		if (result != ISC_R_SUCCESS) {
			fprintf(stderr, "Invalid max qps "
				"(positive floating point value "
				"expected): %s\n", optarg);
			return (ISC_R_FAILURE);
		}
		break;
	case 'P':
		plotfile = optarg;
		break;
	case 'r':
		result = parse_udouble(&ramp_time, optarg);
		if (result != ISC_R_SUCCESS) {
			fprintf(stderr, "Invalid ramp time "
				"(positive floating point value "
				"expected): %s\n", optarg);
			return (ISC_R_FAILURE);
		}
		break;
	case 'L':
		result = parse_udouble(&max_loss_percent, optarg);
		if (result != ISC_R_SUCCESS) {
			fprintf(stderr, "Invalid max query loss "
				"(positive floating point value "
				"expected): %s\n", optarg);
			return (ISC_R_FAILURE);
		}
		break;
	default:
		return (ISC_R_NOTFOUND);
	}
	return (ISC_R_SUCCESS);
}

/* Find the struct ramp_bucket for queries sent at time "when" */

static struct ramp_bucket *
find_bucket(struct timeval *when) {
	double sent_at = difftv(when, &time_of_program_start);
	int i = (int) floor(n_buckets * sent_at / ramp_time);
	/*
	 * Guard against array bounds violations due to roundoff
	 * errors or scheduling jitter
	 */
	if (i < 0)
		i = 0;
	if (i > n_buckets - 1)
		i = n_buckets - 1;
	return &buckets[i];
}

/*
 * 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 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;
	}

	{
		 double latency = difftv(&time_now, &s->sent_timestamp);
		 struct ramp_bucket *b = find_bucket(&s->sent_timestamp);
		 b->responses++;
		 if (!(rcode == dns_rcode_noerror ||
		       rcode == dns_rcode_nxdomain))
			 b->failures++;
		 b->latency_sum += latency;
	 }

	return (ISC_TRUE);
}

/*
 * print_statistics:
 *   Print out statistics based on the results of the test
 */
static void
print_statistics(void) {
	int i;
	double max_throughput;
	double loss_at_max_throughput;	
	double run_time = difftv(&time_of_end_of_run, &time_of_program_start);

	printf("\nStatistics:\n\n");

	printf("  Queries sent:         %u\n", num_queries_sent);
	printf("  Queries completed:    %u\n",
	       num_queries_sent - num_queries_outstanding);
	printf("  Queries lost:         %u\n", num_queries_outstanding);
	printf("  Ran for:              %.6lf seconds\n", run_time);

	/* Find the maximum throughput, subject to the -L option */
	max_throughput = 0.0;
	loss_at_max_throughput = 0.0;	
	for (i = 0; i <= last_bucket_used; i++) {
		struct ramp_bucket *b = &buckets[i];
		double responses_per_sec =
			(double) b->responses / bucket_interval;
		double loss = b->queries ?
			(b->queries - b->responses) / (double) b->queries : 0.0;
		double loss_percent = loss * 100.0;
		if (loss_percent > max_loss_percent)
			break;
		if (responses_per_sec > max_throughput) {
			max_throughput = responses_per_sec;
			loss_at_max_throughput = loss_percent;
		}
	}
	printf("  Maximum throughput:   %.6lf qps\n", max_throughput);
	printf("  Lost at that point:   %.2f%%\n", loss_at_max_throughput);
}

static struct ramp_bucket *
init_buckets(int n) {
	struct ramp_bucket *p = malloc(n * sizeof(*p));
	int i;
	for (i = 0; i < n; i++) {
		p[i].queries = p[i].responses = p[i].failures = 0;
		p[i].latency_sum = 0.0;
	}
	return p;
}

/*
 * Send a query based on a line of input.
 * Return ISC_R_NOMORE if we ran out of query IDs.
 */
static isc_result_t
do_one_line() {
	char line[MAX_INPUT_LEN + 1];
	if (get_input_line(line, sizeof(line)) != ISC_R_SUCCESS) {
		fprintf(stderr, "ran out of query data\n");
		exit(1);
	}
	return process_line(line);
}

static void
enter_wait_phase() {
	wait_phase = ISC_TRUE;
	printf("[Status] Waiting for more responses\n");
	wait_phase_began = time_now;
	end_time = difftv(&time_now, &time_of_program_start) + wait_time;
}

int
main(int argc, char **argv) {
	int i;
	FILE *plotf;

	show_startup_info();

	if (setup(argc, argv, "i:m:P:r:L:") != ISC_R_SUCCESS)
		exit(1);

	n_buckets = ceil(ramp_time / bucket_interval);
	buckets = init_buckets(n_buckets);
	
	update_current_time();
	time_of_program_start = time_now;

	printf("[Status] Sending\n");

	for (;;) {
		int scheduled_n, should_send;
		double time_since_start =
			difftv(&time_now, &time_of_program_start);
		if (wait_phase) {
			if (time_since_start >= end_time)
				break;
		} else {
			if (time_since_start >= ramp_time)
				enter_wait_phase();
		}
		if (! wait_phase) {
			/* How many queries should we have sent by now? */
			scheduled_n = 0.5 * max_qps *
				time_since_start * time_since_start / ramp_time;
			should_send = scheduled_n - num_queries_sent;
			if (should_send >= 1000) {
				printf("[Status] Fell behind by %d queries, "
				       "ending test at %.0f qps\n",
				       should_send,
					max_qps * time_since_start / ramp_time);
				enter_wait_phase();
			}
			if (should_send > 0) {
				isc_result_t result = do_one_line();
				if (result == ISC_R_SUCCESS)
					find_bucket(&time_now)->queries++;
				if (result == ISC_R_NOMORE) {
					printf("[Status] Reached 65536 outstanding queries\n");
					enter_wait_phase();
				}
			}
		}
		(void) try_process_response(query_socket);
		update_current_time();
	}

	update_current_time();
	time_of_end_of_run = time_now;

	printf("[Status] Testing complete\n");

	maybe_print_args(argc, argv);

	plotf = fopen(plotfile, "w");
	if (! plotf) {
		fprintf(stderr, "could not open %s: %s", plotfile,
			strerror(errno));
		exit(1);
	}

	/* Print column headers */
	fprintf(plotf, "# time target_qps actual_qps "
		"responses_per_sec failures_per_sec "
		"avg_latency\n");

	/* Don't print unused buckets */
	last_bucket_used = find_bucket(&wait_phase_began) - buckets;

	/* Don't print a partial bucket at the end */
	if (last_bucket_used > 0)
		--last_bucket_used;

	for (i = 0; i <= last_bucket_used; i++) {
		double bucket_qps = (i + 0.5) * max_qps / n_buckets;
		double latency = buckets[i].responses ?
			buckets[i].latency_sum / buckets[i].responses : 0;
		fprintf(plotf, "%7.3f %8.2f %8.2f %8.2f %8.2f %8.6f\n",
			(i + 0.5) * ramp_time / n_buckets,
			bucket_qps,
			(double) buckets[i].queries / bucket_interval,
			(double) buckets[i].responses / bucket_interval,
			(double) buckets[i].failures / bucket_interval,
			latency);
	}

	fclose(plotf);

	print_statistics();

	cleanup();

	return 0;
}


syntax highlighted by Code2HTML, v. 0.9.1