diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml index 4a276bd3c7..d5f1e5ae89 100644 --- a/man/systemd-analyze.xml +++ b/man/systemd-analyze.xml @@ -274,8 +274,8 @@ Timestamp units-load-finish: Thu 2019-03-14 23:28:07 CET <command>systemd-analyze plot</command> - This command prints an SVG graphic detailing which system services have been started at what - time, highlighting the time they spent on initialization. + This command prints either an SVG graphic, detailing which system services have been started at what + time, highlighting the time they spent on initialization, or the raw time data in JSON or table format. <command>Plot a bootchart</command> @@ -1211,7 +1211,17 @@ $ systemd-analyze verify /tmp/source:alias.service corresponds to a higher security threat. The JSON version of the table is printed to standard output. The MODE passed to the option can be one of three: which is the default, and - which respectively output a prettified or shorted JSON version of the security table. + which respectively output a prettified or shorted JSON version of the security table. + + With the plot command, genereate a JSON formatted output of the raw time data. + The format is a JSON array with objects containing the following fields: name + which is the unit name, activated which is the time after startup the + service was activated, activating which is how long after startup the service + was initially started, time which is how long the service took to activate + from when it was initially started, deactivated which is the time after startup + that the service was deactivated, deactivating whcih is the time after startup + that the service was initially told to deactivate. + @@ -1242,6 +1252,21 @@ $ systemd-analyze verify /tmp/source:alias.service other paths. + + + + When used with the plot command, the raw time data is output in a table. + + + + + + + When used with the plot command in combination with either + or , no legends or hints are included in the output. + + + diff --git a/shell-completion/bash/systemd-analyze b/shell-completion/bash/systemd-analyze index b1baec9978..5edba7bf58 100644 --- a/shell-completion/bash/systemd-analyze +++ b/shell-completion/bash/systemd-analyze @@ -62,7 +62,7 @@ _systemd_analyze() { ) local -A VERBS=( - [STANDALONE]='time blame plot unit-paths exit-status calendar timestamp timespan' + [STANDALONE]='time blame unit-paths exit-status calendar timestamp timespan' [CRITICAL_CHAIN]='critical-chain' [DOT]='dot' [DUMP]='dump' @@ -72,6 +72,7 @@ _systemd_analyze() { [SECURITY]='security' [CONDITION]='condition' [INSPECT_ELF]='inspect-elf' + [PLOT]='plot' ) local CONFIGS='systemd/bootchart.conf systemd/coredump.conf systemd/journald.conf @@ -195,6 +196,11 @@ _systemd_analyze() { comps=$( compgen -A file -- "$cur" ) compopt -o filenames fi + + elif __contains_word "$verb" ${VERBS[PLOT]}; then + if [[ $cur = -* ]]; then + comps='--help --version --system --user --global --no-pager --json=off --json=pretty --json=short --table --no-legend' + fi fi COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) diff --git a/shell-completion/zsh/_systemd-analyze b/shell-completion/zsh/_systemd-analyze index e305995cef..2e046ea111 100644 --- a/shell-completion/zsh/_systemd-analyze +++ b/shell-completion/zsh/_systemd-analyze @@ -40,6 +40,13 @@ _describe -t groups 'file system groups' _groups || compadd "$@" } +(( $+functions[_systemd-analyze_plot] )) || + _systemd-analyze_plot() { + local -a _options + _options=( '--json=off' '--json=pretty' '--json=short' '--table' '--no-legend' ) + _describe 'plot options' _options + } + (( $+functions[_systemd-analyze_commands] )) || _systemd-analyze_commands(){ local -a _systemd_analyze_cmds @@ -48,7 +55,8 @@ 'time:Print time spent in the kernel before reaching userspace' 'blame:Print list of running units ordered by time to init' 'critical-chain:Print a tree of the time critical chain of units' - 'plot:Output SVG graphic showing service initialization' + 'plot:Output SVG graphic showing service initialization, or raw time data in +JSON or table format' 'dot:Dump dependency graph (in dot(1) format)' 'dump:Dump server status' 'cat-config:Cat systemd config files' @@ -97,9 +105,11 @@ _arguments \ '--offline=[Perform a security review of the specified unit files]:BOOL:(yes no)' \ '--threshold=[Set a value to compare the overall security exposure level with]: NUMBER' \ '--security-policy=[Use customized requirements to compare unit files against]: PATH' \ - '--json=[Generate a JSON output of the security analysis table]:MODE:(pretty short off)' \ + "--json=[Generate a JSON output of the security analysis table or plot's raw time data]:MODE:(pretty short off)" \ + "--table=[Generate a table of plot's raw time data]" \ '--profile=[Include the specified profile in the security review of units]: PATH' \ '--no-pager[Do not pipe output into a pager]' \ + "--no-legend[Do not show the headers and footers for plot's raw time data formats]" \ '--man=[Do (not) check for existence of man pages]:BOOL:(yes no)' \ '--generators=[Do (not) run unit generators]:BOOL:(yes no)' \ '--order[When generating graph for dot, show only order]' \ diff --git a/src/analyze/analyze-plot.c b/src/analyze/analyze-plot.c index 100bdc3787..24f4add099 100644 --- a/src/analyze/analyze-plot.c +++ b/src/analyze/analyze-plot.c @@ -5,6 +5,7 @@ #include "analyze-time-data.h" #include "bus-error.h" #include "bus-map-properties.h" +#include "format-table.h" #include "sort-util.h" #include "version.h" @@ -37,7 +38,7 @@ typedef struct HostInfo { char *architecture; } HostInfo; -static HostInfo* free_host_info(HostInfo *hi) { +static HostInfo *free_host_info(HostInfo *hi) { if (!hi) return NULL; @@ -87,7 +88,7 @@ static int acquire_host_info(sd_bus *bus, HostInfo **hi) { } r = bus_map_all_properties( - system_bus ?: bus, + system_bus ? : bus, "org.freedesktop.hostname1", "/org/freedesktop/hostname1", hostname_map, @@ -156,15 +157,14 @@ static void svg_graph_box(double height, double begin, double end) { SCALE_Y * height); } } - static int plot_unit_times(UnitTimes *u, double width, int y) { bool b; if (!u->name) return 0; - svg_bar("activating", u->activating, u->activated, y); - svg_bar("active", u->activated, u->deactivating, y); + svg_bar("activating", u->activating, u->activated, y); + svg_bar("active", u->activated, u->deactivating, y); svg_bar("deactivating", u->deactivating, u->deactivated, y); /* place the text on the left if we have passed the half of the svg width */ @@ -178,41 +178,27 @@ static int plot_unit_times(UnitTimes *u, double width, int y) { return 1; } -int verb_plot(int argc, char *argv[], void *userdata) { - _cleanup_(free_host_infop) HostInfo *host = NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - _cleanup_(unit_times_free_arrayp) UnitTimes *times = NULL; - _cleanup_free_ char *pretty_times = NULL; - bool use_full_bus = arg_scope == LOOKUP_SCOPE_SYSTEM; - BootTimes *boot; +static void limit_times_to_boot(const BootTimes *boot, UnitTimes *u) { + if (u->deactivated > u->activating && u->deactivated <= boot->finish_time && u->activated == 0 + && u->deactivating == 0) + u->activated = u->deactivating = u->deactivated; + if (u->activated < u->activating || u->activated > boot->finish_time) + u->activated = boot->finish_time; + if (u->deactivating < u->activated || u->deactivating > boot->finish_time) + u->deactivating = boot->finish_time; + if (u->deactivated < u->deactivating || u->deactivated > boot->finish_time) + u->deactivated = boot->finish_time; +} + +static int produce_plot_as_svg( + UnitTimes *times, + const HostInfo *host, + const BootTimes *boot, + const char *pretty_times) { + int m = 1, y = 0; UnitTimes *u; - int n, m = 1, y = 0, r; double width; - r = acquire_bus(&bus, &use_full_bus); - if (r < 0) - return bus_log_connect_error(r, arg_transport); - - n = acquire_boot_times(bus, &boot); - if (n < 0) - return n; - - n = pretty_boot_time(bus, &pretty_times); - if (n < 0) - return n; - - if (use_full_bus || arg_scope != LOOKUP_SCOPE_SYSTEM) { - n = acquire_host_info(bus, &host); - if (n < 0) - return n; - } - - n = acquire_time_data(bus, ×); - if (n <= 0) - return n; - - typesafe_qsort(times, n, compare_unit_start); - width = SCALE_X * (boot->firmware_time + boot->finish_time); if (width < 800.0) width = 800.0; @@ -245,16 +231,8 @@ int verb_plot(int argc, char *argv[], void *userdata) { if (text_width > text_start && text_width + text_start > width) width = text_width + text_start; - if (u->deactivated > u->activating && - u->deactivated <= boot->finish_time && - u->activated == 0 && u->deactivating == 0) - u->activated = u->deactivating = u->deactivated; - if (u->activated < u->activating || u->activated > boot->finish_time) - u->activated = boot->finish_time; - if (u->deactivating < u->activated || u->deactivating > boot->finish_time) - u->deactivating = boot->finish_time; - if (u->deactivated < u->deactivating || u->deactivated > boot->finish_time) - u->deactivated = boot->finish_time; + limit_times_to_boot(boot, u); + m++; } @@ -391,5 +369,101 @@ int verb_plot(int argc, char *argv[], void *userdata) { svg("\n"); + return 0; +} + +static int show_table(Table *table, const char *word) { + int r; + + assert(table); + assert(word); + + if (table_get_rows(table) > 1) { + table_set_header(table, arg_legend); + + if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) + r = table_print_json(table, NULL, arg_json_format_flags | JSON_FORMAT_COLOR_AUTO); + else + r = table_print(table, NULL); + if (r < 0) + return table_log_print_error(r); + } + + if (arg_legend) { + if (table_get_rows(table) > 1) + printf("\n%zu %s listed.\n", table_get_rows(table) - 1, word); + else + printf("No %s.\n", word); + } + + return 0; +} + +static int produce_plot_as_text(UnitTimes *times, const BootTimes *boot) { + _cleanup_(table_unrefp) Table *table = NULL; + int r; + + table = table_new("name", "activated", "activating", "time", "deactivated", "deactivating"); + if (!table) + return log_oom(); + + for (; times->has_data; times++) { + limit_times_to_boot(boot, times); + + r = table_add_many( + table, + TABLE_STRING, times->name, + TABLE_TIMESPAN_MSEC, times->activated, + TABLE_TIMESPAN_MSEC, times->activating, + TABLE_TIMESPAN_MSEC, times->time, + TABLE_TIMESPAN_MSEC, times->deactivated, + TABLE_TIMESPAN_MSEC, times->deactivating); + if (r < 0) + return table_log_add_error(r); + } + + return show_table(table, "Units"); +} + +int verb_plot(int argc, char *argv[], void *userdata) { + _cleanup_(free_host_infop) HostInfo *host = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(unit_times_free_arrayp) UnitTimes *times = NULL; + _cleanup_free_ char *pretty_times = NULL; + bool use_full_bus = arg_scope == LOOKUP_SCOPE_SYSTEM; + BootTimes *boot; + int n, r; + + r = acquire_bus(&bus, &use_full_bus); + if (r < 0) + return bus_log_connect_error(r, arg_transport); + + n = acquire_boot_times(bus, &boot); + if (n < 0) + return n; + + n = pretty_boot_time(bus, &pretty_times); + if (n < 0) + return n; + + if (use_full_bus || arg_scope != LOOKUP_SCOPE_SYSTEM) { + n = acquire_host_info(bus, &host); + if (n < 0) + return n; + } + + n = acquire_time_data(bus, ×); + if (n <= 0) + return n; + + typesafe_qsort(times, n, compare_unit_start); + + if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF) || arg_table) + r = produce_plot_as_text(times, boot); + else + r = produce_plot_as_svg(times, host, boot, pretty_times); + if (r < 0) + return r; + return EXIT_SUCCESS; } diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index cac2d3adba..ef2e60962b 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -105,6 +105,8 @@ char *arg_unit = NULL; JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; bool arg_quiet = false; char *arg_profile = NULL; +bool arg_legend = true; +bool arg_table = false; STATIC_DESTRUCTOR_REGISTER(arg_dot_from_patterns, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_dot_to_patterns, strv_freep); @@ -217,8 +219,10 @@ static int help(int argc, char *argv[], void *userdata) { " --security-policy=PATH Use custom JSON security policy instead\n" " of built-in one\n" " --json=pretty|short|off Generate JSON output of the security\n" - " analysis table\n" + " analysis table, or plot's raw time data\n" " --no-pager Do not pipe output into a pager\n" + " --no-legend Disable column headers and hints in plot\n" + " with either --table or --json=\n" " --system Operate on system systemd instance\n" " --user Operate on user systemd instance\n" " --global Operate on global user configuration\n" @@ -238,6 +242,7 @@ static int help(int argc, char *argv[], void *userdata) { " specified time\n" " --profile=name|PATH Include the specified profile in the\n" " security review of the unit(s)\n" + " --table Output plot's raw time data as a table\n" " -h --help Show this help\n" " --version Show package version\n" " -q --quiet Do not emit hints\n" @@ -280,6 +285,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_SECURITY_POLICY, ARG_JSON, ARG_PROFILE, + ARG_TABLE, + ARG_NO_LEGEND, }; static const struct option options[] = { @@ -310,6 +317,8 @@ static int parse_argv(int argc, char *argv[]) { { "unit", required_argument, NULL, 'U' }, { "json", required_argument, NULL, ARG_JSON }, { "profile", required_argument, NULL, ARG_PROFILE }, + { "table", optional_argument, NULL, ARG_TABLE }, + { "no-legend", optional_argument, NULL, ARG_NO_LEGEND }, {} }; @@ -448,14 +457,12 @@ static int parse_argv(int argc, char *argv[]) { r = safe_atou(optarg, &arg_iterations); if (r < 0) return log_error_errno(r, "Failed to parse iterations: %s", optarg); - break; case ARG_BASE_TIME: r = parse_timestamp(optarg, &arg_base_time); if (r < 0) return log_error_errno(r, "Failed to parse --base-time= parameter: %s", optarg); - break; case ARG_PROFILE: @@ -486,6 +493,15 @@ static int parse_argv(int argc, char *argv[]) { free_and_replace(arg_unit, mangled); break; } + + case ARG_TABLE: + arg_table = true; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + break; + case '?': return -EINVAL; @@ -497,9 +513,9 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --offline= is only supported for security right now."); - if (arg_json_format_flags != JSON_FORMAT_OFF && !STRPTR_IN_SET(argv[optind], "security", "inspect-elf")) + if (arg_json_format_flags != JSON_FORMAT_OFF && !STRPTR_IN_SET(argv[optind], "security", "inspect-elf", "plot")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Option --json= is only supported for security and inspect-elf right now."); + "Option --json= is only supported for security, inspect-elf, and plot right now."); if (arg_threshold != 100 && !streq_ptr(argv[optind], "security")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), @@ -536,6 +552,16 @@ static int parse_argv(int argc, char *argv[]) { if (streq_ptr(argv[optind], "condition") && arg_unit && optind < argc - 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No conditions can be passed if --unit= is used."); + if ((!arg_legend && !streq_ptr(argv[optind], "plot")) || + (streq_ptr(argv[optind], "plot") && !arg_legend && !arg_table && FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF))) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --no-legend is only supported for plot with either --table or --json=."); + + if (arg_table && !streq_ptr(argv[optind], "plot")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --table is only supported for plot right now."); + + if (arg_table && !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--table and --json= are mutually exclusive."); + return 1; /* work to do */ } diff --git a/src/analyze/analyze.h b/src/analyze/analyze.h index da12058c43..e4af7b47e0 100644 --- a/src/analyze/analyze.h +++ b/src/analyze/analyze.h @@ -36,6 +36,8 @@ extern char *arg_unit; extern JsonFormatFlags arg_json_format_flags; extern bool arg_quiet; extern char *arg_profile; +extern bool arg_legend; +extern bool arg_table; int acquire_bus(sd_bus **bus, bool *use_full_bus); diff --git a/test/units/testsuite-65.sh b/test/units/testsuite-65.sh index ebe1f57b52..4093c5a2a7 100755 --- a/test/units/testsuite-65.sh +++ b/test/units/testsuite-65.sh @@ -18,7 +18,16 @@ systemd-analyze || : systemd-analyze time || : systemd-analyze blame || : systemd-analyze critical-chain || : +# plot systemd-analyze plot >/dev/null || : +systemd-analyze plot --json=pretty >/dev/null || : +systemd-analyze plot --json=short >/dev/null || : +systemd-analyze plot --json=off >/dev/null || : +systemd-analyze plot --json=pretty --no-legend >/dev/null || : +systemd-analyze plot --json=short --no-legend >/dev/null || : +systemd-analyze plot --json=off --no-legend >/dev/null || : +systemd-analyze plot --table >/dev/null || : +systemd-analyze plot --table --no-legend >/dev/null || : # legacy/deprecated options (moved to systemctl, but still usable from analyze) systemd-analyze log-level systemd-analyze log-level "$(systemctl log-level)"