blob: da5902483f6f4ae7492cac9ac3f0e4e7c903b0f0 [file] [log] [blame]
/* $Id$ */
/*
* Copyright (C) 2003-2006 Benny Prijono <benny@prijono.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "pjsip_perf.h"
#include <stdlib.h> /* atoi */
#define THIS_FILE "main.c"
pjsip_perf_settings settings;
/* Show error message. */
void app_perror(const char *sender, const char *title, pj_status_t status)
{
char errmsg[PJ_ERR_MSG_SIZE];
pj_strerror(status, errmsg, sizeof(errmsg));
PJ_LOG(3,(sender, "%s: %s [code=%d]", title, errmsg, status));
}
/* Init default settings. */
static void init_settings(void)
{
pj_status_t status;
settings.stateless = 0;
settings.start_rate = 10;
settings.max_capacity = 64;
settings.duration = 0;
settings.thread_cnt = 1;
settings.local_port = 5060;
settings.log_level = 3;
settings.app_log_level = 3;
pjsip_method_set(&settings.method, PJSIP_OPTIONS_METHOD);
pj_init();
/* Create caching pool. */
pj_caching_pool_init(&settings.cp, &pj_pool_factory_default_policy,
4 * 1024 * 1024);
/* Create application pool. */
settings.pool = pj_pool_create(&settings.cp.factory, "pjsip-perf", 1024,
1024, NULL);
/* Create endpoint. */
status = pjsip_endpt_create(&settings.cp.factory, NULL, &settings.endpt);
if (status != PJ_SUCCESS) {
app_perror(THIS_FILE, "Unable to create endpoint", status);
return;
}
}
/* Poll function. */
static int PJ_THREAD_FUNC poll_pjsip(void *arg)
{
pj_status_t last_err = 0;
PJ_UNUSED_ARG(arg);
do {
pj_time_val timeout = { 0, 10 };
pj_status_t status;
status = pjsip_endpt_handle_events (settings.endpt, &timeout);
if (status != last_err) {
last_err = status;
app_perror(THIS_FILE, "handle_events() returned error", status);
}
} while (!settings.quit_flag);
return 0;
}
/*****************************************************************************
* This is a simple module to count and log messages
*/
static void on_rx_msg(pjsip_rx_data *rdata)
{
PJ_LOG(5,(THIS_FILE, "RX %d bytes %s from %s:%d:\n"
"%s\n"
"--end msg--",
rdata->msg_info.len,
pjsip_rx_data_get_info(rdata),
rdata->pkt_info.src_name,
rdata->pkt_info.src_port,
rdata->msg_info.msg_buf));
}
static void on_tx_msg(pjsip_tx_data *tdata)
{
PJ_LOG(5,(THIS_FILE, "TX %d bytes %s to %s:%d:\n"
"%s\n"
"--end msg--",
(tdata->buf.cur - tdata->buf.start),
pjsip_tx_data_get_info(tdata),
tdata->tp_info.dst_name,
tdata->tp_info.dst_port,
tdata->buf.start));
}
static pj_bool_t mod_counter_on_rx_request(pjsip_rx_data *rdata)
{
settings.rx_req++;
on_rx_msg(rdata);
return PJ_FALSE;
}
static pj_bool_t mod_counter_on_rx_response(pjsip_rx_data *rdata)
{
settings.rx_res++;
on_rx_msg(rdata);
return PJ_FALSE;
}
static pj_status_t mod_counter_on_tx_request(pjsip_tx_data *tdata)
{
settings.tx_req++;
on_tx_msg(tdata);
return PJ_SUCCESS;
}
static pj_status_t mod_counter_on_tx_response(pjsip_tx_data *tdata)
{
settings.tx_res++;
on_tx_msg(tdata);
return PJ_SUCCESS;
}
static pjsip_module mod_counter =
{
NULL, NULL, /* prev, next. */
{ "mod-counter", 11 }, /* Name. */
-1, /* Id */
PJSIP_MOD_PRIORITY_TRANSPORT_LAYER-1,/* Priority */
NULL, /* load() */
NULL, /* start() */
NULL, /* stop() */
NULL, /* unload() */
&mod_counter_on_rx_request, /* on_rx_request() */
&mod_counter_on_rx_response, /* on_rx_response() */
&mod_counter_on_tx_request, /* on_tx_request. */
&mod_counter_on_tx_response, /* on_tx_response() */
NULL, /* on_tsx_state() */
};
/*****************************************************************************
* Console application custom logging:
*/
static FILE *log_file;
static void app_log_writer(int level, const char *buffer, int len)
{
/* Write to both stdout and file. */
if (level <= settings.app_log_level)
pj_log_write(level, buffer, len);
if (log_file) {
fwrite(buffer, len, 1, log_file);
fflush(log_file);
}
}
static pj_status_t app_logging_init(void)
{
/* Redirect log function to ours */
pj_log_set_log_func( &app_log_writer );
/* If output log file is desired, create the file: */
if (settings.log_file) {
log_file = fopen(settings.log_file, "wt");
if (log_file == NULL) {
PJ_LOG(1,(THIS_FILE, "Unable to open log file %s",
settings.log_file));
return -1;
}
}
return PJ_SUCCESS;
}
void app_logging_shutdown(void)
{
/* Close logging file, if any: */
if (log_file) {
fclose(log_file);
log_file = NULL;
}
}
/* Initialize */
static pj_status_t initialize(void)
{
pj_sockaddr_in addr;
int i;
pj_status_t status;
/* Init logging */
if (app_logging_init() != PJ_SUCCESS)
return -1;
/* Create UDP transport. */
pj_memset(&addr, 0, sizeof(addr));
addr.sin_family = PJ_AF_INET;
addr.sin_port = pj_htons((pj_uint16_t)settings.local_port);
status = pjsip_udp_transport_start(settings.endpt, &addr, NULL,
settings.thread_cnt, NULL);
if (status != PJ_SUCCESS) {
app_perror(THIS_FILE, "Unable to start UDP transport", status);
return status;
}
/* Initialize transaction layer: */
status = pjsip_tsx_layer_init_module(settings.endpt);
if (status != PJ_SUCCESS) {
app_perror(THIS_FILE, "Transaction layer initialization error",
status);
return status;
}
/* Initialize UA layer module: */
pjsip_ua_init_module( settings.endpt, NULL );
/* Init core SIMPLE module : */
pjsip_evsub_init_module(settings.endpt);
/* Init presence module: */
pjsip_pres_init_module( settings.endpt, pjsip_evsub_instance());
/* Init xfer/REFER module */
pjsip_xfer_init_module( settings.endpt );
/* Init multimedia endpoint. */
status = pjmedia_endpt_create(&settings.cp.factory,
pjsip_endpt_get_ioqueue(settings.endpt), 0,
&settings.med_endpt);
if (status != PJ_SUCCESS) {
app_perror(THIS_FILE, "Unable to create media endpoint",
status);
return status;
}
/* Init OPTIONS test handler */
status = options_handler_init();
if (status != PJ_SUCCESS) {
app_perror(THIS_FILE, "Unable to create OPTIONS handler", status);
return status;
}
/* Init call test handler */
status = call_handler_init();
if (status != PJ_SUCCESS) {
app_perror(THIS_FILE, "Unable to initialize call handler", status);
return status;
}
/* Register message counter module. */
status = pjsip_endpt_register_module(settings.endpt, &mod_counter);
if (status != PJ_SUCCESS) {
app_perror(THIS_FILE, "Unable to register module", status);
return status;
}
/* Start worker thread. */
for (i=0; i<settings.thread_cnt; ++i) {
status = pj_thread_create(settings.pool, "pjsip-perf", &poll_pjsip,
NULL, 0, 0, &settings.thread[i]);
if (status != PJ_SUCCESS) {
app_perror(THIS_FILE, "Unable to create thread", status);
return status;
}
}
pj_log_set_level(settings.log_level);
return PJ_SUCCESS;
}
/* Shutdown */
static void shutdown(void)
{
int i;
/* Signal and wait worker thread to quit. */
settings.quit_flag = 1;
for (i=0; i<settings.thread_cnt; ++i) {
pj_thread_join(settings.thread[i]);
pj_thread_destroy(settings.thread[i]);
}
pjsip_endpt_destroy(settings.endpt);
pj_caching_pool_destroy(&settings.cp);
}
/* Verify that valid SIP url is given. */
pj_status_t verify_sip_url(const char *c_url)
{
pjsip_uri *p;
pj_pool_t *pool;
char *url;
int len = (c_url ? pj_ansi_strlen(c_url) : 0);
if (!len) return -1;
pool = pj_pool_create(&settings.cp.factory, "check%p", 1024, 0, NULL);
if (!pool) return -1;
url = pj_pool_alloc(pool, len+1);
pj_ansi_strcpy(url, c_url);
p = pjsip_parse_uri(pool, url, len, 0);
if (!p || pj_stricmp2(pjsip_uri_get_scheme(p), "sip") != 0)
p = NULL;
pj_pool_release(pool);
return p ? 0 : -1;
}
/* Usage */
static void usage(void)
{
puts("Usage:");
puts(" pjsip-perf [options] [target]");
puts("where");
puts(" target Optional default target URL");
puts("");
puts("General options:");
puts(" --help Display this help screen");
puts(" --version Display version info");
puts("");
puts("Logging options:");
puts(" --log-level=N Set log verbosity (default=3)");
puts(" --app-log-level=N Set screen log verbosity (default=3)");
puts(" --log-file=FILE Save log to FILE");
puts("");
puts("SIP options:");
puts(" --local-port=N SIP local port");
puts(" --stateless Handle incoming request statelessly if possible");
puts(" --thread-cnt=N Number of worker threads (default=1)");
puts("");
puts("Rate control:");
puts(" --start-rate=N Start rate in tasks per seconds (default=1)");
puts("");
puts("Capacity control:");
puts(" --max-capacity=N Maximum outstanding sessions (default=64)");
puts("");
puts("Duration control:");
puts(" --duration=secs Sessions duration (default=0)");
puts("");
}
/* Read options. */
static pj_status_t parse_options(int argc, char *argv[])
{
enum {
OPT_HELP,
OPT_VERSION,
OPT_LOG_LEVEL,
OPT_APP_LOG_LEVEL,
OPT_LOG_FILE,
OPT_LOCAL_PORT,
OPT_STATELESS,
OPT_THREAD_CNT,
OPT_START_RATE,
OPT_MAX_CAPACITY,
OPT_DURATION
};
struct pj_getopt_option long_opts[] = {
{ "help", 0, 0, OPT_HELP},
{ "version", 0, 0, OPT_VERSION},
{ "log-level", 1, 0, OPT_LOG_LEVEL},
{ "app-log-level", 1, 0, OPT_APP_LOG_LEVEL},
{ "log-file", 1, 0, OPT_LOG_FILE},
{ "local-port", 1, 0, OPT_LOCAL_PORT},
{ "stateless", 0, 0, OPT_STATELESS},
{ "thread-cnt", 1, 0, OPT_THREAD_CNT},
{ "start-rate", 1, 0, OPT_START_RATE},
{ "max-capacity", 1, 0, OPT_MAX_CAPACITY},
{ "duration", 1, 0, OPT_DURATION},
{ NULL, 0, 0, 0}
};
int c, option_index;
pj_optind = 0;
while ((c=pj_getopt_long(argc, argv, "", long_opts, &option_index)) != -1) {
switch (c) {
case OPT_HELP:
usage();
return PJ_EINVAL;
case OPT_VERSION:
pj_dump_config();
return PJ_EINVAL;
case OPT_LOG_LEVEL:
settings.log_level = atoi(pj_optarg);
break;
case OPT_APP_LOG_LEVEL:
settings.app_log_level = atoi(pj_optarg);
break;
case OPT_LOG_FILE:
settings.log_file = pj_optarg;
break;
case OPT_LOCAL_PORT:
settings.local_port = atoi(pj_optarg);
if (settings.local_port < 1 || settings.local_port > 65535) {
PJ_LOG(1,(THIS_FILE,"Invalid --local-port %s", pj_optarg));
return PJ_EINVAL;
}
break;
case OPT_STATELESS:
settings.stateless = 1;
break;
case OPT_THREAD_CNT:
settings.thread_cnt = atoi(pj_optarg);
if (settings.thread_cnt < 1 ||
settings.thread_cnt > PJ_ARRAY_SIZE(settings.thread))
{
PJ_LOG(1,(THIS_FILE,"Invalid --thread-cnt %s", pj_optarg));
return PJ_EINVAL;
}
break;
case OPT_START_RATE:
settings.start_rate = atoi(pj_optarg);
if (settings.start_rate < 1 || settings.start_rate > 1000000) {
PJ_LOG(1,(THIS_FILE,"Invalid --start-rate %s", pj_optarg));
return PJ_EINVAL;
}
break;
case OPT_MAX_CAPACITY:
settings.max_capacity = atoi(pj_optarg);
if (settings.max_capacity < 1 || settings.max_capacity > 65000) {
PJ_LOG(1,(THIS_FILE,
"Invalid --max-capacity %s (range=1-65000)",
pj_optarg));
return PJ_EINVAL;
}
break;
case OPT_DURATION:
settings.duration = atoi(pj_optarg);
if (settings.duration < 0 || settings.duration > 1000000) {
PJ_LOG(1,(THIS_FILE,"Invalid --duration %s", pj_optarg));
return PJ_EINVAL;
}
break;
}
}
if (pj_optind != argc) {
if (verify_sip_url(argv[pj_optind]) != PJ_SUCCESS) {
PJ_LOG(3,(THIS_FILE, "Invalid SIP URL %s", argv[pj_optind]));
return PJ_EINVAL;
}
settings.target = pj_str(argv[pj_optind]);
++pj_optind;
}
if (pj_optind != argc) {
printf("Error: unknown options %s\n", argv[pj_optind]);
return PJ_EINVAL;
}
return PJ_SUCCESS;
}
static void spawn_batch( pj_timer_heap_t *timer_heap,
struct pj_timer_entry *entry );
/* Completion callback. */
static void completion_cb(void *token, pj_bool_t success)
{
batch *batch = token;
if (success)
batch->success++;
else
batch->failed++;
if (batch->success+batch->failed == batch->rate) {
pj_time_val elapsed, sess_elapsed;
unsigned msec;
pj_gettimeofday(&batch->end_time);
elapsed = sess_elapsed = batch->end_time;
/* Batch time. */
PJ_TIME_VAL_SUB(elapsed, batch->start_time);
msec = PJ_TIME_VAL_MSEC(elapsed);
if (msec == 0) msec = 1;
/* Session time */
PJ_TIME_VAL_SUB(sess_elapsed, settings.session->start_time);
/* Spawn time */
PJ_TIME_VAL_SUB(batch->spawned_time, batch->start_time);
if (batch->failed) {
PJ_LOG(2,(THIS_FILE,
"%02d:%02d:%02d: %d tasks in %d.%ds (%d tasks/sec), "
"spawn=time=%d.%d, FAILED=%d",
(sess_elapsed.sec / 3600),
(sess_elapsed.sec % 3600) / 60,
(sess_elapsed.sec % 60),
batch->rate,
elapsed.sec, elapsed.msec,
batch->rate * 1000 / msec,
batch->spawned_time.sec,
batch->spawned_time.msec,
batch->failed));
} else {
PJ_LOG(3,(THIS_FILE,
"%02d:%02d:%02d: %d tasks in %d.%ds (%d tasks/sec), "
"spawn=time=%d.%d",
(sess_elapsed.sec / 3600),
(sess_elapsed.sec % 3600) / 60,
(sess_elapsed.sec % 60),
batch->rate,
elapsed.sec, elapsed.msec,
batch->rate * 1000 / msec,
batch->spawned_time.sec,
batch->spawned_time.msec));
}
if (!settings.session->stopping) {
pj_time_val interval;
if (msec >= 1000)
interval.sec = interval.msec = 0;
else
interval.sec = 0, interval.msec = 1000-msec;
settings.timer.cb = &spawn_batch;
pjsip_endpt_schedule_timer( settings.endpt, &settings.timer, &interval);
} else {
PJ_LOG(3,(THIS_FILE, "%.*s test session completed",
(int)settings.session->method.name.slen,
settings.session->method.name.ptr));
pj_pool_release(settings.session->pool);
settings.session = NULL;
}
}
}
/* Spawn new batch. */
static void spawn_batch( pj_timer_heap_t *timer_heap,
struct pj_timer_entry *entry )
{
session *sess = settings.session;
batch *batch;
pj_status_t status = PJ_SUCCESS;
pjsip_cred_info cred_info[1];
pj_time_val elapsed;
unsigned i;
PJ_UNUSED_ARG(timer_heap);
PJ_UNUSED_ARG(entry);
if (!pj_list_empty(&sess->free_list)) {
batch = sess->free_list.next;
pj_list_erase(batch);
} else {
batch = pj_pool_alloc(sess->pool, sizeof(struct batch));
}
pj_gettimeofday(&batch->start_time);
batch->rate = settings.cur_rate;
batch->started = 0;
batch->success = 0;
batch->failed = 0;
pj_gettimeofday(&batch->start_time);
batch->spawned_time = batch->start_time;
pj_list_push_back(&sess->active_list, batch);
for (i=0; i<batch->rate; ++i) {
pj_str_t from = { "sip:user@127.0.0.1", 18};
if (sess->method.id == PJSIP_OPTIONS_METHOD) {
status = options_spawn_test(&settings.target, &from,
&settings.target,
0, cred_info, NULL, batch,
&completion_cb);
} else if (sess->method.id == PJSIP_INVITE_METHOD) {
status = call_spawn_test( &settings.target, &from,
&settings.target,
0, cred_info, NULL, batch,
&completion_cb);
}
if (status != PJ_SUCCESS)
break;
batch->started++;
elapsed.sec = elapsed.msec = 0;
pjsip_endpt_handle_events(settings.endpt, &elapsed);
}
pj_gettimeofday(&batch->spawned_time);
///
#if 0
elapsed = batch->spawned_time;
PJ_TIME_VAL_SUB(elapsed, batch->start_time);
PJ_LOG(2,(THIS_FILE, "%d requests sent in %d ms", batch->started,
PJ_TIME_VAL_MSEC(elapsed)));
#endif
sess->total_created += batch->started;
batch = sess->active_list.next;
sess->outstanding = 0;
while (batch != &sess->active_list) {
sess->outstanding += (batch->started - batch->success - batch->failed);
if (batch->started == batch->success + batch->failed) {
struct batch *next = batch->next;
pj_list_erase(batch);
pj_list_push_back(&sess->free_list, batch);
batch = next;
} else {
batch = batch->next;
}
}
}
/* Start new session */
static void start_session(pj_bool_t auto_repeat)
{
pj_time_val interval = { 0, 0 };
pj_pool_t *pool;
session *sess;
pool = pjsip_endpt_create_pool(settings.endpt, "session", 4000, 4000);
if (!pool) {
app_perror(THIS_FILE, "Unable to create pool", PJ_ENOMEM);
return;
}
sess = pj_pool_zalloc(pool, sizeof(session));
sess->pool = pool;
sess->stopping = auto_repeat ? 0 : 1;
sess->method = settings.method;
pj_list_init(&sess->active_list);
pj_list_init(&sess->free_list);
pj_gettimeofday(&sess->start_time);
settings.session = sess;
settings.timer.cb = &spawn_batch;
pjsip_endpt_schedule_timer( settings.endpt, &settings.timer, &interval);
}
/* Dump state */
static void dump(pj_bool_t detail)
{
pjsip_endpt_dump(settings.endpt, detail);
pjsip_tsx_layer_dump(detail);
pjsip_ua_dump(detail);
}
/* help screen */
static void help_screen(void)
{
puts ("+============================================================================+");
printf("| Current mode: %-10s Current rate: %-5d Call Capacity: %-7d |\n",
settings.method.name.ptr, settings.cur_rate, settings.max_capacity);
printf("| Call Duration: %-7d |\n",
settings.duration);
printf("| Total tx requests: %-7u rx requests: %-7u |\n",
settings.tx_req, settings.rx_req);
printf("| tx responses: %-7u rx responses: %-7u |\n",
settings.tx_res, settings.rx_res);
puts ("+--------------------------------------+-------------------------------------+");
puts ("| Test Settings | Misc Commands: |");
puts ("| | |");
puts ("| m Change mode | |");
puts ("| + - Increment/decrement rate by 10 | d Dump status |");
puts ("| * / Increment/decrement rate by 100 | dd Dump detailed (e.g. tables) |");
puts ("+--------------------------------------+-------------------------------------+");
puts ("| Test Commands |");
puts ("| |");
puts ("| s Start single test batch c Clear counters |");
puts ("| sc Start continuous test x Stop continuous tests |");
puts ("+----------------------------------------------------------------------------+");
puts ("| q: Quit |");
puts ("+============================================================================+");
puts ("");
}
/*
* Input simple string
*/
static pj_bool_t simple_input(const char *title, char *buf, pj_size_t len)
{
char *p;
printf("%s (empty to cancel): ", title); fflush(stdout);
fgets(buf, len, stdin);
/* Remove trailing newlines. */
for (p=buf; ; ++p) {
if (*p=='\r' || *p=='\n') *p='\0';
else if (!*p) break;
}
if (!*buf)
return PJ_FALSE;
return PJ_TRUE;
}
/* Main input loop */
static void test_main(void)
{
char menuin[10];
char input[80];
settings.cur_rate = settings.start_rate;
help_screen();
for (;;) {
printf(">>>> "); fflush(stdout);
fgets(menuin, sizeof(menuin), stdin);
switch (menuin[0]) {
case 's':
if (settings.session != NULL) {
PJ_LOG(3,(THIS_FILE,"Error: another session is in progress"));
} else if (settings.target.slen == 0) {
PJ_LOG(3,(THIS_FILE,"Error: target URL is not configured"));
} else {
start_session(menuin[1]=='c');
}
break;
case 'x':
if (settings.session) {
settings.session->stopping = 1;
puts("Stopping sessions...");
} else {
PJ_LOG(3,(THIS_FILE,"Error: no sessions"));
}
break;
case 'c':
/* Clear counters */
settings.rx_req = settings.rx_res = settings.tx_req =
settings.tx_res = 0;
puts("Counters cleared");
break;
case 'm':
if (!simple_input("Change method [OPTIONS,INVITE]", input, sizeof(input)))
continue;
if (pj_ansi_stricmp(input, "OPTIONS")==0)
pjsip_method_set(&settings.method, PJSIP_OPTIONS_METHOD);
else if (pj_ansi_stricmp(input, "INVITE")==0)
pjsip_method_set(&settings.method, PJSIP_INVITE_METHOD);
else {
puts("Error: invalid method");
}
break;
case 'd':
dump(menuin[1]=='d');
break;
case '+':
settings.cur_rate += 10;
PJ_LOG(3,(THIS_FILE, "Rate is now %d", settings.cur_rate));
break;
case '-':
if (settings.cur_rate > 10) {
settings.cur_rate -= 10;
PJ_LOG(3,(THIS_FILE, "Rate is now %d", settings.cur_rate));
}
break;
case '*':
settings.cur_rate += 100;
PJ_LOG(3,(THIS_FILE, "Rate is now %d", settings.cur_rate));
break;
case '/':
if (settings.cur_rate > 100) {
settings.cur_rate -= 100;
PJ_LOG(3,(THIS_FILE, "Rate is now %d", settings.cur_rate));
}
break;
case 'q':
return;
default:
help_screen();
break;
}
}
}
/* main() */
int main(int argc, char *argv[])
{
pj_status_t status;
init_settings();
status = parse_options(argc, argv);
if (status != PJ_SUCCESS)
return 1;
status = initialize();
if (status != PJ_SUCCESS)
return 1;
test_main();
shutdown();
return 0;
}