diff --git a/js/ui/scripting.js b/js/ui/scripting.js index 5270b1e09..12bbded58 100644 --- a/js/ui/scripting.js +++ b/js/ui/scripting.js @@ -1,6 +1,7 @@ /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; const Mainloop = imports.mainloop; const Meta = imports.gi.Meta; @@ -137,16 +138,38 @@ function _collect(scriptModule, outputFile) { scriptModule.finish(); if (outputFile) { - let result = {}; - for (let metric in scriptModule.METRICS) { - result[metric] = { - value: scriptModule.METRICS[metric], - description: scriptModule.METRIC_DESCRIPTIONS[metric] - }; - } + let f = Gio.file_new_for_path(outputFile); + let raw = f.replace(null, false, + Gio.FileCreateFlags.NONE, + null); + let out = Gio.BufferedOutputStream.new_sized (raw, 4096); + Shell.write_string_to_stream (out, "{\n"); - let contents = JSON.stringify(result); - GLib.file_set_contents(outputFile, contents, contents.length); + Shell.write_string_to_stream(out, '"events":\n'); + Shell.PerfLog.get_default().dump_events(out); + + Shell.write_string_to_stream(out, ',\n"metrics":\n[ '); + let first = true; + for (let name in scriptModule.METRICS) { + let value = scriptModule.METRICS[name]; + let description = scriptModule.METRIC_DESCRIPTIONS[name]; + + if (!first) + Shell.write_string_to_stream(out, ',\n '); + first = false; + + Shell.write_string_to_stream(out, + '{ "name": ' + JSON.stringify(name) + ',\n' + + ' "description": ' + JSON.stringify(description) + ',\n' + + ' "value": ' + JSON.stringify(value) + ' }'); + } + Shell.write_string_to_stream(out, ' ]'); + + Shell.write_string_to_stream (out, ',\n"log":\n'); + Shell.PerfLog.get_default().dump_log(out); + + Shell.write_string_to_stream (out, '\n}\n'); + out.close(null); } else { let metrics = []; for (let metric in scriptModule.METRICS) diff --git a/src/gnome-shell.in b/src/gnome-shell.in index b8bb4bd6a..78b2cdbb3 100644 --- a/src/gnome-shell.in +++ b/src/gnome-shell.in @@ -314,16 +314,17 @@ def run_performance_test(): if options.perf_warmup and i == 0: continue - for metric, details in output.iteritems(): - if not metric in metric_summaries: + for metric in output['metrics']: + name = metric['name'] + if not name in metric_summaries: summary = {} - summary['description'] = details['description'] + summary['description'] = metric['description'] summary['values'] = [] - metric_summaries[metric] = summary + metric_summaries[name] = summary else: - summary = metric_summaries[metric] + summary = metric_summaries[name] - summary['values'].append(details['value']) + summary['values'].append(metric['value']) for metric in sorted(metric_summaries.keys()): summary = metric_summaries[metric] diff --git a/src/shell-perf-log.c b/src/shell-perf-log.c index 5c596825d..262e47e15 100644 --- a/src/shell-perf-log.c +++ b/src/shell-perf-log.c @@ -255,6 +255,13 @@ define_event (ShellPerfLog *perf_log, return NULL; } + /* We could do stricter validation, but this will break our JSON dumps */ + if (strchr (name, '"') != NULL) + { + g_warning ("Event names can't include '\"'"); + return NULL; + } + if (g_hash_table_lookup (perf_log->events_by_name, name) != NULL) { g_warning ("Duplicate event event for '%s'\n", name); @@ -680,13 +687,15 @@ shell_perf_log_collect_statistics (ShellPerfLog *perf_log) * shell_perf_log_replay: * @perf_log: a #ShellPerfLog * @replay_function: function to call for each event in the log + * @user_data: data to pass to @replay_function * * Replays the log by calling the given function for each event * in the log. */ void shell_perf_log_replay (ShellPerfLog *perf_log, - ShellPerfReplayFunction replay_function) + ShellPerfReplayFunction replay_function, + gpointer user_data) { gint64 event_time = perf_log->start_time; GList *iter; @@ -754,8 +763,203 @@ shell_perf_log_replay (ShellPerfLog *perf_log, pos += strlen ((char *)(block->buffer + pos)) + 1; } - replay_function (event_time, event->name, event->signature, &arg); + replay_function (event_time, event->name, event->signature, &arg, user_data); g_value_unset (&arg); } } } + +static char * +escape_quotes (const char *input) +{ + GString *result; + const char *p; + + if (strchr (input, '"') == NULL) + return (char *)input; + + result = g_string_new (NULL); + for (p = input; *p; p++) + { + if (*p == '"') + g_string_append (result, "\\\""); + else + g_string_append_c (result, *p); + } + + return g_string_free (result, FALSE); +} + +static gboolean +write_string (GOutputStream *out, + const char *str, + GError **error) +{ + return g_output_stream_write_all (out, str, strlen (str), + NULL, NULL, + error); +} + +/** + * shell_perf_log_dump_events: + * @perf_log: a #ShellPerfLog + * @out: output stream into which to write the event definitions + * @error: location to store #GError, or %NULL + * + * Dump the definition of currently defined events and statistics, formatted + * as JSON, to the specified output stream. The JSON output is an array, + * with each element being a dictionary of the form: + * + * { name: , + * description: events->len; i++) + { + ShellPerfEvent *event = g_ptr_array_index (perf_log->events, i); + char *escaped_description = escape_quotes (event->description); + gboolean is_statistic = g_hash_table_lookup (perf_log->statistics_by_name, event->name) != NULL; + + if (i != 0) + g_string_append (output, ",\n "); + + g_string_append_printf (output, + "{ \"name\": \"%s\",\n" + " \"description\": \"%s\"", + event->name, escaped_description); + if (is_statistic) + g_string_append (output, ",\n \"statistic\": true"); + + g_string_append (output, " }"); + + if (escaped_description != event->description) + g_free (escaped_description); + } + + g_string_append (output, " ]"); + + return write_string (out, g_string_free (output, FALSE), error); +} + +typedef struct { + GOutputStream *out; + GError *error; + gboolean first; +} ReplayToJsonClosure; + +static void +replay_to_json (gint64 time, + const char *name, + const char *signature, + GValue *arg, + gpointer user_data) +{ + ReplayToJsonClosure *closure = user_data; + char *event_str; + + if (closure->error != NULL) + return; + + if (!closure->first) + { + if (!write_string (closure->out, ",\n ", &closure->error)) + return; + } + + closure->first = FALSE; + + if (strcmp (signature, "") == 0) + { + event_str = g_strdup_printf ("[%" G_GINT64_FORMAT ", \"%s\"]", time, name); + } + else if (strcmp (signature, "i") == 0) + { + event_str = g_strdup_printf ("[%" G_GINT64_FORMAT ", \"%s\", %i]", + time, + name, + g_value_get_int (arg)); + } + else if (strcmp (signature, "x") == 0) + { + event_str = g_strdup_printf ("[%" G_GINT64_FORMAT ", \"%s\", %"G_GINT64_FORMAT "]", + time, + name, + g_value_get_int64 (arg)); + } + else if (strcmp (signature, "s") == 0) + { + const char *arg_str = g_value_get_string (arg); + char *escaped = escape_quotes (arg_str); + + event_str = g_strdup_printf ("[%" G_GINT64_FORMAT ", \"%s\", \"%s\"]", + time, + name, + g_value_get_string (arg)); + + if (escaped != arg_str) + g_free (escaped); + } + else + { + g_assert_not_reached (); + } + + if (!write_string (closure->out, event_str, &closure->error)) + return; +} + +/** + * shell_perf_log_dump_log: + * @perf_log: a #ShellPerfLog + * @out: output stream into which to write the event log + * @error: location to store #GError, or %NULL + * + * Writes the performance event log, formatted as JSON, to the specified + * output stream. For performance reasons, the output stream passed + * in should generally be a buffered (or memory) output stream, since + * it will be written to in small pieces. The JSON output is an array + * with the elements of the array also being arrays, of the form + * '['