root/usr/src/cmd/latencytop/latencytop.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright (c) 2008-2009, Intel Corporation.
 * All Rights Reserved.
 */

#include <unistd.h>
#include <getopt.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <libgen.h>
#include <signal.h>
#include "latencytop.h"

#define CMPOPT(a, b)    strncmp((a), (b), sizeof (b))

/*
 * This variable is used to check if "dynamic variable drop" in dtrace
 * has happened.
 */
boolean_t lt_drop_detected = 0;

lt_config_t g_config;

typedef enum {
        LT_CMDOPT_INTERVAL,
        LT_CMDOPT_LOG_FILE,
        LT_CMDOPT_LOG_LEVEL,
        LT_CMDOPT_LOG_INTERVAL,
        LT_CMDOPT_CONFIG_FILE,
        LT_CMDOPT_F_FILTER,
        LT_CMDOPT_F_SCHED,
        LT_CMDOPT_F_SOBJ,
        LT_CMDOPT_F_LOW,
        LT_CMDOPT_SELECT,
        LT_CMDOPT__LAST /* Must be the last one */
} lt_cmd_option_id_t;

/*
 * Check for duplicate command line options.
 * Returns TRUE if duplicate options with different values are found,
 * returns FALSE otherwise.
 */
static int
check_opt_dup(lt_cmd_option_id_t id, uint64_t value)
{

        static int opt_set[(int)LT_CMDOPT__LAST];
        static uint64_t opt_val[(int)LT_CMDOPT__LAST];

        const char *errmsg[] = {
                "-t is set more than once with different values.",
                "-o is set more than once.",
                "-k is set more than once with different values.",
                "-l is set more than once with different values.",
                "-c is set more than once.",
                "-f [no]filter is set more than once with different values.",
                "-f [no]sched is set more than once with different values.",
                "-f [no]sobj is set more than once with different values.",
                "-f [no]low is set more than once with different values.",
                "-s is set more than once with different values."
        };

        g_assert(sizeof (errmsg)/sizeof (errmsg[0]) == (int)LT_CMDOPT__LAST);

        if (!opt_set[(int)id]) {
                opt_set[(int)id] = TRUE;
                opt_val[(int)id] = value;
                return (FALSE);
        }

        if (opt_val[(int)id] != value) {
                (void) fprintf(stderr, "%s\n", errmsg[(int)id]);
                return (TRUE);
        }

        return (FALSE);
}

/*
 * Print command-line help message.
 */
static void
print_usage(const char *execname, int long_help)
{
        char buffer[PATH_MAX];
        (void) snprintf(buffer, sizeof (buffer), "%s", execname);

        if (!long_help) {
                /* Print short help to stderr. */
                (void) fprintf(stderr, "Usage: %s [option(s)], ",
                    basename(buffer));
                (void) fprintf(stderr, "use '%s -h' for details.\n",
                    basename(buffer));
                return;
        }

        (void) printf("Usage: %s [option(s)]\n", basename(buffer));
        (void) printf("Options:\n"
            "    -h, --help\n"
            "        Print this help.\n"
            "    -t, --interval TIME\n"
            "        Set refresh interval to TIME. "
            "Valid range [1...60] seconds, default = 5\n"
        /*
         * Option "-c, --config FILE" is not user-visible for now.
         * When we have chance to properly document the format of translation
         * rules, we'll make it user-visible.
         */
            "    -o, --output-log-file FILE\n"
            "        Output kernel log to FILE. Default = "
            DEFAULT_KLOG_FILE "\n"
            "    -k, --kernel-log-level LEVEL\n"
            "        Set kernel log level to LEVEL.\n"
            "        0(default) = None, 1 = Unmapped, 2 = Mapped, 3 = All.\n"
            "    -f, --feature [no]feature1,[no]feature2,...\n"
            "        Enable/disable features in LatencyTOP.\n"
            "        [no]filter:\n"
            "        Filter large interruptible latencies, e.g. sleep.\n"
            "        [no]sched:\n"
            "        Monitors sched (PID=0).\n"
            "        [no]sobj:\n"
            "        Monitors synchronization objects.\n"
            "        [no]low:\n"
            "        Lower overhead by sampling small latencies.\n"
            "    -l, --log-period TIME\n"
            "        Write and restart log every TIME seconds, TIME >= 60\n"
            "    -s --select [ pid=<pid> | pgid=<pgid> ]\n"
            "        Monitor only the given process or processes in the "
            "given process group.\n");
}

/*
 * Properly exit latencytop when it receives SIGINT or SIGTERM.
 */
/* ARGSUSED */
static void
signal_handler(int sig)
{
        lt_gpipe_break("q");
}

/*
 * Convert string to integer. It returns error if extra characters are found.
 */
static int
to_int(const char *str, int *result)
{
        char *tail = NULL;
        long ret;

        if (str == NULL || result == NULL) {
                return (-1);
        }

        ret = strtol(str, &tail, 10);

        if (tail != NULL && *tail != '\0') {
                return (-1);
        }

        *result = (int)ret;

        return (0);
}

/*
 * The main function.
 */
int
main(int argc, char *argv[])
{
        const char *opt_string = "t:o:k:hf:l:c:s:";
        struct option const longopts[] = {
                {"interval", required_argument, NULL, 't'},
                {"output-log-file", required_argument, NULL, 'o'},
                {"kernel-log-level", required_argument, NULL, 'k'},
                {"help", no_argument, NULL, 'h'},
                {"feature", required_argument, NULL, 'f'},
                {"log-period", required_argument, NULL, 'l'},
                {"config", required_argument, NULL, 'c'},
                {"select", required_argument, NULL, 's'},
                {NULL, 0, NULL, 0}
        };

        int optc;
        int longind = 0;
        int running = 1;
        int unknown_option = FALSE;
        int refresh_interval = 5;
        int klog_level = 0;
        int log_interval = 0;
        long long last_logged = 0;
        char *token = NULL;
        int retval = 0;
        int gpipe;
        int err;
        uint64_t collect_end;
        uint64_t current_time;
        uint64_t delta_time;
        char logfile[PATH_MAX] = "";
        int select_id;
        int select_value;
        char *select_str;
        boolean_t no_dtrace_cleanup = B_TRUE;

        lt_gpipe_init();
        (void) signal(SIGINT, signal_handler);
        (void) signal(SIGTERM, signal_handler);

        /* Default global settings */
        g_config.lt_cfg_enable_filter = 0;
        g_config.lt_cfg_trace_sched = 0;
        g_config.lt_cfg_trace_syncobj = 1;
        g_config.lt_cfg_low_overhead_mode = 0;
        g_config.lt_cfg_trace_pid = 0;
        g_config.lt_cfg_trace_pgid = 0;
        /* dtrace snapshot every 1 second */
        g_config.lt_cfg_snap_interval = 1000;
#ifdef EMBED_CONFIGS
        g_config.lt_cfg_config_name = NULL;
#else
        g_config.lt_cfg_config_name = lt_strdup(DEFAULT_CONFIG_NAME);
#endif

        /* Parse command line arguments. */
        while ((optc = getopt_long(argc, argv, opt_string,
            longopts, &longind)) != -1) {
                switch (optc) {
                case 'h':
                        print_usage(argv[0], TRUE);
                        goto end_none;
                case 't':
                        if (to_int(optarg, &refresh_interval) != 0 ||
                            refresh_interval < 1 || refresh_interval > 60) {
                                lt_display_error(
                                    "Invalid refresh interval: %s\n", optarg);
                                unknown_option = TRUE;
                        } else if (check_opt_dup(LT_CMDOPT_INTERVAL,
                            refresh_interval)) {
                                unknown_option = TRUE;
                        }

                        break;
                case 'k':
                        if (to_int(optarg, &klog_level) != 0 ||
                            lt_klog_set_log_level(klog_level) != 0) {
                                lt_display_error(
                                    "Invalid log level: %s\n", optarg);
                                unknown_option = TRUE;
                        } else if (check_opt_dup(LT_CMDOPT_LOG_LEVEL,
                            refresh_interval)) {
                                unknown_option = TRUE;
                        }

                        break;
                case 'o':
                        if (check_opt_dup(LT_CMDOPT_LOG_FILE, optind)) {
                                unknown_option = TRUE;
                        } else if (strlen(optarg) >= sizeof (logfile)) {
                                lt_display_error(
                                    "Log file name is too long: %s\n",
                                    optarg);
                                unknown_option = TRUE;
                        } else {
                                (void) strncpy(logfile, optarg,
                                    sizeof (logfile));
                        }

                        break;
                case 'f':
                        for (token = strtok(optarg, ","); token != NULL;
                            token = strtok(NULL, ",")) {
                                int v = TRUE;

                                if (strncmp(token, "no", 2) == 0) {
                                        v = FALSE;
                                        token = &token[2];
                                }

                                if (CMPOPT(token, "filter") == 0) {
                                        if (check_opt_dup(LT_CMDOPT_F_FILTER,
                                            v)) {
                                                unknown_option = TRUE;
                                        } else {
                                                g_config.lt_cfg_enable_filter
                                                    = v;
                                        }
                                } else if (CMPOPT(token, "sched") == 0) {
                                        if (check_opt_dup(LT_CMDOPT_F_SCHED,
                                            v)) {
                                                unknown_option = TRUE;
                                        } else {
                                                g_config.lt_cfg_trace_sched
                                                    = v;
                                        }
                                } else if (CMPOPT(token, "sobj") == 0) {
                                        if (check_opt_dup(LT_CMDOPT_F_SOBJ,
                                            v)) {
                                                unknown_option = TRUE;
                                        } else {
                                                g_config.lt_cfg_trace_syncobj
                                                    = v;
                                        }
                                } else if (CMPOPT(token, "low") == 0) {
                                        if (check_opt_dup(LT_CMDOPT_F_LOW,
                                            v)) {
                                                unknown_option = TRUE;
                                        } else {
                                                g_config.
                                                    lt_cfg_low_overhead_mode
                                                    = v;
                                        }
                                } else {
                                        lt_display_error(
                                            "Unknown feature: %s\n", token);
                                        unknown_option = TRUE;
                                }
                        }

                        break;
                case 'l':
                        if (to_int(optarg, &log_interval) != 0 ||
                            log_interval < 60) {
                                lt_display_error(
                                    "Invalid log interval: %s\n", optarg);
                                unknown_option = TRUE;
                        } else if (check_opt_dup(LT_CMDOPT_LOG_INTERVAL,
                            log_interval)) {
                                unknown_option = TRUE;
                        }

                        break;
                case 'c':
                        if (strlen(optarg) >= PATH_MAX) {
                                lt_display_error(
                                    "Configuration name is too long.\n");
                                unknown_option = TRUE;
                        } else if (check_opt_dup(LT_CMDOPT_CONFIG_FILE,
                            optind)) {
                                unknown_option = TRUE;
                        } else {
                                g_config.lt_cfg_config_name =
                                    lt_strdup(optarg);
                        }

                        break;
                case 's':
                        if (strncmp(optarg, "pid=", 4) == 0) {
                                select_id = 0;
                                select_str = &optarg[4];
                        } else if (strncmp(optarg, "pgid=", 5) == 0) {
                                select_id = 1;
                                select_str = &optarg[5];
                        } else {
                                lt_display_error(
                                    "Invalid select option: %s\n", optarg);
                                unknown_option = TRUE;
                                break;
                        }

                        if (to_int(select_str, &select_value) != 0) {
                                lt_display_error(
                                    "Invalid select option: %s\n", optarg);
                                unknown_option = TRUE;
                                break;
                        }

                        if (select_value <= 0) {
                                lt_display_error(
                                    "Process/process group ID must be "
                                    "greater than 0: %s\n", optarg);
                                unknown_option = TRUE;
                                break;
                        }

                        if (check_opt_dup(LT_CMDOPT_SELECT,
                            (((uint64_t)select_id) << 32) | select_value)) {
                                unknown_option = TRUE;
                                break;
                        }

                        if (select_id == 0) {
                                g_config.lt_cfg_trace_pid = select_value;
                        } else {
                                g_config.lt_cfg_trace_pgid = select_value;
                        }
                        break;
                default:
                        unknown_option = TRUE;
                        break;
                }
        }

        if (!unknown_option && strlen(logfile) > 0) {
                err = lt_klog_set_log_file(logfile);

                if (err == -1) {
                        lt_display_error("Log file name is too long: %s\n",
                            logfile);
                        unknown_option = TRUE;
                } else if (err == -2) {
                        lt_display_error("Cannot write to log file: %s\n",
                            logfile);
                        unknown_option = TRUE;
                }
        }

        /* Throw error for invalid/junk arguments */
        if (optind  < argc) {
                int tmpind = optind;
                (void) fprintf(stderr, "Unknown option(s): ");

                while (tmpind < argc) {
                        (void) fprintf(stderr, "%s ", argv[tmpind++]);
                }

                (void) fprintf(stderr, "\n");
                unknown_option = TRUE;
        }

        if (unknown_option) {
                print_usage(argv[0], FALSE);
                retval = 1;
                goto end_none;
        }

        (void) printf("%s\n%s\n", TITLE, COPYRIGHT);

        /*
         * Initialization
         */
        lt_klog_init();

        if (lt_table_init() != 0) {
                lt_display_error("Unable to load configuration table.\n");
                retval = 1;
                goto end_notable;
        }

        if (lt_dtrace_init() != 0) {
                lt_display_error("Unable to initialize dtrace.\n");
                retval = 1;
                goto end_nodtrace;
        }

        last_logged = lt_millisecond();

        (void) printf("Collecting data for %d seconds...\n",
            refresh_interval);

        gpipe = lt_gpipe_readfd();
        collect_end = last_logged + refresh_interval * 1000;
        for (;;) {
                fd_set read_fd;
                struct timeval timeout;
                int tsleep = collect_end - lt_millisecond();

                if (tsleep <= 0) {
                        break;
                }

                /*
                 * Interval when we call dtrace_status() and collect
                 * aggregated data.
                 */
                if (tsleep > g_config.lt_cfg_snap_interval) {
                        tsleep = g_config.lt_cfg_snap_interval;
                }

                timeout.tv_sec = tsleep / 1000;
                timeout.tv_usec = (tsleep % 1000) * 1000;

                FD_ZERO(&read_fd);
                FD_SET(gpipe, &read_fd);

                if (select(gpipe + 1, &read_fd, NULL, NULL, &timeout) > 0) {
                        goto end_ubreak;
                }

                (void) lt_dtrace_work(0);
        }

        lt_display_init();

        do {
                current_time = lt_millisecond();

                lt_stat_clear_all();
                (void) lt_dtrace_collect();

                delta_time = current_time;
                current_time = lt_millisecond();
                delta_time = current_time - delta_time;

                if (log_interval > 0 &&
                    current_time - last_logged > log_interval * 1000) {
                        lt_klog_write();
                        last_logged = current_time;
                }

                running = lt_display_loop(refresh_interval * 1000 -
                    delta_time);

                /*
                 * This is to avoid dynamic variable drop
                 * in DTrace.
                 */
                if (lt_drop_detected == B_TRUE) {
                        if (lt_dtrace_deinit() != 0) {
                                no_dtrace_cleanup = B_FALSE;
                                retval = 1;
                                break;
                        }

                        lt_drop_detected = B_FALSE;
                        if (lt_dtrace_init() != 0) {
                                retval = 1;
                                break;
                        }
                }
        } while (running != 0);

        lt_klog_write();

        /* Cleanup */
        lt_display_deinit();

end_ubreak:
        if (no_dtrace_cleanup == B_FALSE || lt_dtrace_deinit() != 0)
                retval = 1;

        lt_stat_free_all();

end_nodtrace:
        lt_table_deinit();

end_notable:
        lt_klog_deinit();

end_none:
        lt_gpipe_deinit();

        if (g_config.lt_cfg_config_name != NULL) {
                free(g_config.lt_cfg_config_name);
        }

        return (retval);
}