blob: 7bffe8bc97610f7f3c86e0a0a0d38d04960c4d8a [file] [log] [blame]
/*===-- timeit.c - LLVM Test Suite Timing Tool ------------------*- C++ -*-===*\
|* *|
|* Part of the LLVM Project, under the Apache License v2.0 with LLVM *|
|* Exceptions. *|
|* See https://llvm.org/LICENSE.txt for license information. *|
|* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception *|
|* *|
\*===----------------------------------------------------------------------===*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/resource.h>
#include <sys/time.h>
#include <sys/wait.h>
/* Enumeration for our exit status codes. */
enum ExitCode {
/* \brief Indicates a failure monitoring the target. */
EXITCODE_MONITORING_FAILURE = 66,
/* \brief Indicates a failure in exec() which usually means an invalid program
* name. */
EXITCODE_EXEC_FAILURE = 67,
EXITCODE_EXEC_NOENTRY = 127,
EXITCODE_EXEC_NOPERMISSION = 126,
/* \brief Indicates that we were unexpectedly signalled(). */
EXITCODE_SIGNALLED = 68,
/* \brief Indicates the child was signalled. */
EXITCODE_CHILD_SIGNALLED = 69
};
/* \brief Record our own program name, for error messages. */
static const char *g_program_name = 0;
/* \brief Record the child command name, for error messages. */
static const char *g_target_program = 0;
/* \brief If given, report output in POSIX mode format. */
static int g_posix_mode = 0;
/* \brief If non-zero, execute the program with a timeout of the given number
* of seconds.
*/
static int g_timeout_in_seconds = 0;
/* \brief If non-zero, the PID of the process being monitored. */
static pid_t g_monitored_pid = 0;
/* \brief If non-zero, the path to attempt to chdir() to before executing the
* target. */
static const char *g_target_exec_directory = 0;
/* \brief If non-zero, the path to write the summary information to (exit status
* and timing). */
static const char *g_summary_file = 0;
/* \brief If non-zero, the path to redirect the target standard input to. */
static const char *g_target_redirect_input = 0;
/* \brief If non-zero, the path to redirect the target stdout to. */
static const char *g_target_redirect_stdout = 0;
/* \brief If non-zero, the path to redirect the target stderr to. */
static const char *g_target_redirect_stderr = 0;
/* \brief If non-zero, append exit status at end of output file. */
static int g_append_exitstats = 0;
/* @name Resource Limit Variables */
/* @{ */
/* \brief If non-sentinel, the CPU time limit to set for the target. */
static rlim_t g_target_cpu_limit = ~(rlim_t) 0;
/* \brief If non-sentinel, the stack size limit to set for the target. */
static rlim_t g_target_stack_size_limit = ~(rlim_t) 0;
/* \brief If non-sentinel, the data size limit to set for the target. */
static rlim_t g_target_data_size_limit = ~(rlim_t) 0;
/* \brief If non-sentinel, the RSS size limit to set for the target. */
static rlim_t g_target_rss_size_limit = ~(rlim_t) 0;
/* \brief If non-sentinel, the file size limit to set for the target. */
static rlim_t g_target_file_size_limit = ~(rlim_t) 0;
/* \brief If non-sentinel, the core limit to set for the target. */
static rlim_t g_target_core_limit = ~(rlim_t) 0;
/* \brief If non-sentinel, the file count limit to set for the target. */
static rlim_t g_target_file_count_limit = ~(rlim_t) 0;
/* \brief If non-sentinel, the subprocess count limit to set for the target. */
static rlim_t g_target_subprocess_count_limit = ~(rlim_t) 0;
/* @} */
static double sample_wall_time(void) {
struct timeval t;
gettimeofday(&t, NULL);
return (double) t.tv_sec + t.tv_usec * 1.e-6;
}
static void terminate_handler(int signal) {
/* If we are monitoring a process, kill its process group and assume we will
* complete normally.
*/
if (g_monitored_pid) {
fprintf(stderr, ("%s: error: received signal %d. "
"killing monitored process(es): %s\n"),
g_program_name, signal, g_target_program);
/* Kill the process group of monitored_pid. */
kill(-g_monitored_pid, SIGKILL);
return;
}
fprintf(stderr, "%s: error: received signal %d. exiting.\n",
g_program_name, signal);
/* Otherwise, we received a signal we should treat as for ourselves, and exit
* quickly. */
_exit(EXITCODE_SIGNALLED);
}
static void timeout_handler(int signal) {
(void)signal;
fprintf(stderr, "%s: TIMING OUT PROCESS: %s\n", g_program_name,
g_target_program);
/* We should always be monitoring a process when we receive an alarm. Kill its
* process group and assume we will terminate normally.
*/
kill(-g_monitored_pid, SIGKILL);
}
static int monitor_child_process(pid_t pid, double start_time) {
double real_time, user_time, sys_time;
struct rusage usage;
int res, status;
/* Record the PID we are monitoring, for use in the signal handlers. */
g_monitored_pid = pid;
/* If we are running with a timeout, set up an alarm now. */
if (g_timeout_in_seconds) {
sigset_t masked;
sigemptyset(&masked);
sigaddset(&masked, SIGALRM);
alarm(g_timeout_in_seconds);
}
/* Wait for the process to terminate. */
do {
res = waitpid(pid, &status, 0);
} while (res < 0 && errno == EINTR);
if (res < 0) {
perror("waitpid");
return EXITCODE_MONITORING_FAILURE;
}
/* Record the real elapsed time as soon as we can. */
real_time = sample_wall_time() - start_time;
/* Just in case, kill the child process group. */
kill(-pid, SIGKILL);
/* Collect the other resource data on the children. */
if (getrusage(RUSAGE_CHILDREN, &usage) < 0) {
perror("getrusage");
return EXITCODE_MONITORING_FAILURE;
}
user_time = (double) usage.ru_utime.tv_sec + usage.ru_utime.tv_usec/1000000.0;
sys_time = (double) usage.ru_stime.tv_sec + usage.ru_stime.tv_usec/1000000.0;
/* If the process was signalled, report a more interesting status. */
int exit_status;
if (WIFSIGNALED(status)) {
fprintf(stderr, "%s: error: child terminated by signal %d\n",
g_program_name, WTERMSIG(status));
/* Propagate the signalled status to the caller. */
exit_status = 128 + WTERMSIG(status);
} else if (WIFEXITED(status)) {
exit_status = WEXITSTATUS(status);
} else {
/* This should never happen, but if it does assume some kind of failure. */
exit_status = EXITCODE_MONITORING_FAILURE;
}
// If we are not using a summary file, report the information as /usr/bin/time
// would.
if (!g_summary_file) {
if (g_posix_mode) {
fprintf(stderr, "real %12.4f\nuser %12.4f\nsys %12.4f\n",
real_time, user_time, sys_time);
} else {
fprintf(stderr, "%12.4f real %12.4f user %12.4f sys\n",
real_time, user_time, sys_time);
}
} else {
/* Otherwise, write the summary data in a simple parsable format. */
FILE *fp = fopen(g_summary_file, "w");
if (!fp) {
perror("fopen");
return EXITCODE_MONITORING_FAILURE;
}
fprintf(fp, "exit %d\n", exit_status);
fprintf(fp, "%-10s %.4f\n", "real", real_time);
fprintf(fp, "%-10s %.4f\n", "user", user_time);
fprintf(fp, "%-10s %.4f\n", "sys", sys_time);
fclose(fp);
}
if (g_append_exitstats && g_target_program) {
FILE *fp_stdout = fopen(g_target_redirect_stdout, "a");
if (!fp_stdout) {
perror("fopen");
return EXITCODE_MONITORING_FAILURE;
}
fprintf(fp_stdout, "exit %d\n", exit_status);
fclose(fp_stdout);
/* let timeit itself report success */
exit_status = 0;
}
return exit_status;
}
#define set_resource_limit(resource, value) \
set_resource_limit_actual(#resource, resource, value)
static void set_resource_limit_actual(const char *resource_name, int resource,
rlim_t value) {
/* Get the current limit. */
struct rlimit current;
getrlimit(resource, &current);
/* Set the limits to as close as requested, assuming we are not super-user. */
struct rlimit requested;
requested.rlim_cur = requested.rlim_max = \
(value < current.rlim_max) ? value : current.rlim_max;
if (setrlimit(resource, &requested) < 0) {
fprintf(stderr, "%s: warning: unable to set limit for %s (to {%lu, %lu})\n",
g_program_name, resource_name, (unsigned long) requested.rlim_cur,
(unsigned long) requested.rlim_max);
}
}
static int streq(const char *a, const char *b) {
return strcmp(a, b) == 0;
}
static int execute_target_process(char * const argv[]) {
/* Create a new process group for pid, and the process tree it may spawn. We
* do this, because later on we might want to kill pid _and_ all processes
* spawned by it and its descendants.
*/
setpgid(0, 0);
/* Redirect the standard input, if requested. */
if (g_target_redirect_input) {
FILE *fp = fopen(g_target_redirect_input, "r");
if (!fp) {
perror("fopen");
return EXITCODE_MONITORING_FAILURE;
}
int fd = fileno(fp);
if (dup2(fd, 0) < 0) {
perror("dup2");
return EXITCODE_MONITORING_FAILURE;
}
fclose(fp);
}
/* Redirect the standard output, if requested. */
FILE *fp_stdout = NULL;
if (g_target_redirect_stdout) {
fp_stdout = fopen(g_target_redirect_stdout, "w");
if (!fp_stdout) {
perror("fopen");
return EXITCODE_MONITORING_FAILURE;
}
int fd = fileno(fp_stdout);
if (dup2(fd, STDOUT_FILENO) < 0) {
perror("dup2");
return EXITCODE_MONITORING_FAILURE;
}
}
if (g_target_redirect_stderr) {
FILE *fp_stderr = NULL;
int fd;
if (streq(g_target_redirect_stdout, g_target_redirect_stderr))
fd = fileno(fp_stdout);
else {
fp_stderr = fopen(g_target_redirect_stderr, "w");
if (!fp_stderr) {
perror("fopen");
return EXITCODE_MONITORING_FAILURE;
}
fd = fileno(fp_stderr);
}
if (dup2(fd, STDERR_FILENO) < 0) {
perror("dup2");
return EXITCODE_MONITORING_FAILURE;
}
if (fp_stderr != NULL)
fclose(fp_stderr);
}
if (fp_stdout != NULL)
fclose(fp_stdout);
/* Honor any requested resource limits. */
if (g_target_cpu_limit != ~(rlim_t) 0) {
set_resource_limit(RLIMIT_CPU, g_target_cpu_limit);
}
if (g_target_stack_size_limit != ~(rlim_t) 0) {
set_resource_limit(RLIMIT_STACK, g_target_stack_size_limit);
}
if (g_target_data_size_limit != ~(rlim_t) 0) {
set_resource_limit(RLIMIT_DATA, g_target_data_size_limit);
}
if (g_target_rss_size_limit != ~(rlim_t) 0) {
set_resource_limit(RLIMIT_RSS, g_target_rss_size_limit);
}
if (g_target_file_size_limit != ~(rlim_t) 0) {
set_resource_limit(RLIMIT_FSIZE, g_target_file_size_limit);
}
if (g_target_core_limit != ~(rlim_t) 0) {
set_resource_limit(RLIMIT_CORE, g_target_core_limit);
}
if (g_target_file_count_limit != ~(rlim_t) 0) {
set_resource_limit(RLIMIT_NOFILE, g_target_file_count_limit);
}
if (g_target_subprocess_count_limit != ~(rlim_t) 0) {
set_resource_limit(RLIMIT_NPROC, g_target_subprocess_count_limit);
}
/* Honor the desired target execute directory. */
if (g_target_exec_directory) {
if (chdir(g_target_exec_directory) < 0) {
perror("chdir");
return EXITCODE_MONITORING_FAILURE;
}
}
execvp(argv[0], argv);
perror("execv");
if (errno == ENOENT) {
return EXITCODE_EXEC_NOENTRY;
} else if (errno == EACCES) {
return EXITCODE_EXEC_NOPERMISSION;
}
return EXITCODE_EXEC_FAILURE;
}
static int execute(char * const argv[]) {
double start_time;
pid_t pid;
/* Set up signal handlers so we can terminate the monitored process(es) on
* SIGINT or SIGTERM. */
signal(SIGINT, terminate_handler);
signal(SIGTERM, terminate_handler);
/* Set up a signal handler to terminate the process on timeout. */
signal(SIGALRM, timeout_handler);
start_time = sample_wall_time();
/* Fork the child process. */
pid = fork();
if (pid < 0) {
perror("fork");
return EXITCODE_MONITORING_FAILURE;
}
/* If we are in the context of the child process, spawn it. */
if (pid == 0) {
/* Setup and execute the target process. This never returns except on
* failure. */
return execute_target_process(argv);
}
/* Otherwise, we are in the context of the monitoring process. */
return monitor_child_process(pid, start_time);
}
static void usage(int is_error) {
#define WRAPPED "\n "
fprintf(stderr, "usage: %s [options] command ... arguments ...\n",
g_program_name);
fprintf(stderr, "Options:\n");
fprintf(stderr, " %-20s %s", "-h, --help",
"Show this help text.\n");
fprintf(stderr, " %-20s %s", "-p, --posix",
"Report time in /usr/bin/time POSIX format.\n");
fprintf(stderr, " %-20s %s", "-t, --timeout <N>",
"Execute the subprocess with a timeout of N seconds.\n");
fprintf(stderr, " %-20s %s", "-c, --chdir <PATH>",
"Execute the subprocess in the given working directory.\n");
fprintf(stderr, " %-20s %s", "--summary <PATH>",
"Write monitored process summary (exit code and time) to PATH.\n");
fprintf(stderr, " %-20s %s", "--redirect-output <PATH>",
WRAPPED "Redirect stdout and stderr for the target to PATH.\n");
fprintf(stderr, " %-20s %s", "--redirect-stdout <PATH>",
WRAPPED "Redirect stdout for the target to PATH.\n");
fprintf(stderr, " %-20s %s", "--redirect-stderr <PATH>",
WRAPPED "Redirect stderr for the target to PATH.\n");
fprintf(stderr, " %-20s %s", "--redirect-input <PATH>",
WRAPPED "Redirect stdin for the target to PATH.\n");
fprintf(stderr, " %-20s %s", "--limit-cpu <N>",
WRAPPED "Limit the target to N seconds of CPU time.\n");
fprintf(stderr, " %-20s %s", "--limit-stack-size <N>",
WRAPPED "Limit the target to N bytes of stack space.\n");
fprintf(stderr, " %-20s %s", "--limit-data-size <N>",
WRAPPED "Limit the target to N bytes of data.\n");
fprintf(stderr, " %-20s %s", "--limit-rss-size <N>",
WRAPPED "Limit the target to N bytes of resident memory.\n");
fprintf(stderr, " %-20s %s", "--limit-file-size <N>",
WRAPPED "Limit the target to creating files no more than N bytes.\n");
fprintf(stderr, " %-20s %s", "--limit-core <N>",
WRAPPED "Limit the size for which core files will be generated.\n");
fprintf(stderr, " %-20s %s", "--limit-file-count <N>",
(WRAPPED
"Limit the maximum number of open files the target can have.\n"));
fprintf(stderr, " %-20s %s", "--limit-subprocess-count <N>",
(WRAPPED
"Limit the maximum number of simultaneous processes "
"the target can use.\n"));
_exit(is_error);
}
int main(int argc, char * const argv[]) {
int i;
g_program_name = argv[0];
for (i = 1; i != argc; ++i) {
const char *arg = argv[i];
if (arg[0] != '-')
break;
if (streq(arg, "-h") || streq(arg, "--help")) {
usage(/*is_error=*/0);
}
if (streq(arg, "-p") || streq(arg, "--posix")) {
g_posix_mode = 1;
continue;
}
if (streq(arg, "-t") || streq(arg, "--timeout")) {
if (i + 1 == argc) {
fprintf(stderr, "error: %s argument requires an option\n", arg);
usage(/*is_error=*/1);
}
g_timeout_in_seconds = atoi(argv[++i]);
continue;
}
if (streq(arg, "--summary")) {
if (i + 1 == argc) {
fprintf(stderr, "error: %s argument requires an option\n", arg);
usage(/*is_error=*/1);
}
g_summary_file = argv[++i];
continue;
}
if (streq(arg, "--redirect-input")) {
if (i + 1 == argc) {
fprintf(stderr, "error: %s argument requires an option\n", arg);
usage(/*is_error=*/1);
}
g_target_redirect_input = argv[++i];
continue;
}
if (streq(arg, "--redirect-output")) {
if (i + 1 == argc) {
fprintf(stderr, "error: %s argument requires an option\n", arg);
usage(/*is_error=*/1);
}
g_target_redirect_stdout = argv[++i];
g_target_redirect_stderr = g_target_redirect_stdout;
continue;
}
if (streq(arg, "--redirect-stdout")) {
if (i + 1 == argc) {
fprintf(stderr, "error: %s argument requires an option\n", arg);
usage(/*is_error=*/1);
}
g_target_redirect_stdout = argv[++i];
continue;
}
if (streq(arg, "--redirect-stderr")) {
if (i + 1 == argc) {
fprintf(stderr, "error: %s argument requires an option\n", arg);
usage(/*is_error=*/1);
}
g_target_redirect_stderr = argv[++i];
continue;
}
if (streq(arg, "--append-exitstatus")) {
g_append_exitstats = 1;
continue;
}
if (streq(arg, "-c") || streq(arg, "--chdir")) {
if (i + 1 == argc) {
fprintf(stderr, "error: %s argument requires an option\n", arg);
usage(/*is_error=*/1);
}
g_target_exec_directory = argv[++i];
continue;
}
if (strncmp(arg, "--limit-", 8) == 0) {
rlim_t value;
if (i + 1 == argc) {
fprintf(stderr, "error: %s argument requires an option\n", arg);
usage(/*is_error=*/1);
}
value = atoi(argv[++i]);
if (streq(arg, "--limit-cpu")) {
g_target_cpu_limit = value;
} else if (streq(arg, "--limit-stack-size")) {
g_target_stack_size_limit = value;
} else if (streq(arg, "--limit-data-size")) {
g_target_data_size_limit = value;
} else if (streq(arg, "--limit-rss-size")) {
g_target_rss_size_limit = value;
} else if (streq(arg, "--limit-file-size")) {
g_target_file_size_limit = value;
} else if (streq(arg, "--limit-core")) {
g_target_core_limit = value;
} else if (streq(arg, "--limit-file-count")) {
g_target_file_count_limit = value;
} else if (streq(arg, "--limit-subprocess-count")) {
g_target_subprocess_count_limit = value;
} else {
fprintf(stderr, "error: invalid limit argument '%s'\n", arg);
usage(/*is_error=*/1);
}
continue;
}
fprintf(stderr, "error: invalid argument '%s'\n", arg);
usage(/*is_error=*/1);
}
if (i == argc) {
fprintf(stderr, "error: no command (or arguments) was given\n");
usage(/*is_error=*/1);
}
g_target_program = argv[i];
return execute(&argv[i]);
}