/*
 *    HardInfo - Displays System Information
 *    Copyright (C) 2003-2009 L. A. F. Pereira <l@tia.mat.br>
 *
 *    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, version 2 or later.
 *
 *    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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 */

#define _GNU_SOURCE
#include <config.h>
#include <hardinfo.h>
#include <iconcache.h>
#include <shell.h>
#include <syncmanager.h>

#include <sys/resource.h>
#include <sys/time.h>

#include <signal.h>
#include <sys/types.h>

#include "appf.h"
#include "benchmark.h"
#include "cpu_util.h"

#include "benchmark/bench_results.c"

bench_value bench_results[BENCHMARK_N_ENTRIES];

static void do_benchmark(void (*benchmark_function)(void), int entry);
static gchar *benchmark_include_results_reverse(bench_value result,
                                                const gchar *benchmark);
static gchar *benchmark_include_results(bench_value result,
                                        const gchar *benchmark);

/* ModuleEntry entries, scan_*(), callback_*(), etc. */
#include "benchmark/benches.c"

char *bench_value_to_str(bench_value r)
{
    gboolean has_rev = r.revision >= 0;
    gboolean has_extra = r.extra && *r.extra != 0;
    gboolean has_user_note = r.user_note && *r.user_note != 0;
    char *ret = g_strdup_printf("%lf; %lf; %d", r.result, r.elapsed_time,
                                r.threads_used);
    if (has_rev || has_extra || has_user_note)
        ret = appf(ret, "; ", "%d", r.revision);
    if (has_extra || has_user_note)
        ret = appf(ret, "; ", "%s", r.extra);
    if (has_user_note)
        ret = appf(ret, "; ", "%s", r.user_note);
    return ret;
}

bench_value bench_value_from_str(const char *str)
{
    bench_value ret = EMPTY_BENCH_VALUE;
    char rstr[32] = "", estr[32] = "", *p;
    int t, c, v;
    char extra[256], user_note[256];
    if (str) {
        /* try to handle floats from locales that use ',' or '.' as decimal sep
         */
        c = sscanf(
            str, "%[-+0-9.,]; %[-+0-9.,]; %d; %d; %255[^\r\n;|]; %255[^\r\n;|]",
            rstr, estr, &t, &v, extra, user_note);
        if (c >= 3) {
            if ((p = strchr(rstr, ','))) {
                *p = '.';
            }
            if ((p = strchr(estr, ','))) {
                *p = '.';
            }
            ret.result = g_ascii_strtod(rstr, NULL);
            ret.elapsed_time = g_ascii_strtod(estr, NULL);
            ret.threads_used = t;
        }
        if (c >= 4) {
            ret.revision = v;
        }
        if (c >= 5) {
            strcpy(ret.extra, extra);
        }
        if (c >= 6) {
            strcpy(ret.user_note, user_note);
        }
    }
    return ret;
}

typedef struct _ParallelBenchTask ParallelBenchTask;

struct _ParallelBenchTask {
    gint thread_number;
    guint start, end;
    gpointer data, callback;
    int *stop;
};

static gpointer benchmark_crunch_for_dispatcher(gpointer data)
{
    ParallelBenchTask *pbt = (ParallelBenchTask *)data;
    gpointer (*callback)(void *data, gint thread_number);
    gpointer return_value = g_malloc(sizeof(double));
    int count = 0;

    if ((callback = pbt->callback)) {
        while (!*pbt->stop) {
            callback(pbt->data, pbt->thread_number);
            /* don't count if didn't finish in time */
            if (!*pbt->stop)
                count++;
        }
    } else {
        DEBUG("this is thread %p; callback is NULL and it should't be!",
              g_thread_self());
    }

    g_free(pbt);

    *(double *)return_value = (double)count;
    return return_value;
}

bench_value benchmark_crunch_for(float seconds,
                                 gint n_threads,
                                 gpointer callback,
                                 gpointer callback_data)
{
    int cpu_procs, cpu_cores, cpu_threads, cpu_nodes;
    int thread_number, stop = 0;
    GSList *threads = NULL, *t;
    GTimer *timer;
    bench_value ret = EMPTY_BENCH_VALUE;

    timer = g_timer_new();

    cpu_procs_cores_threads_nodes(&cpu_procs, &cpu_cores, &cpu_threads, &cpu_nodes);
    if (n_threads > 0)
        ret.threads_used = n_threads;
    else if (n_threads < 0)
        ret.threads_used = cpu_cores;
    else
        ret.threads_used = cpu_threads;

    g_timer_start(timer);
    for (thread_number = 0; thread_number < ret.threads_used; thread_number++) {
        ParallelBenchTask *pbt = g_new0(ParallelBenchTask, 1);
        GThread *thread;

        DEBUG("launching thread %d", thread_number);

        pbt->thread_number = thread_number;
        pbt->data = callback_data;
        pbt->callback = callback;
        pbt->stop = &stop;

#if GLIB_CHECK_VERSION(2,32,0)
        thread = g_thread_new("dispatcher", (GThreadFunc)benchmark_crunch_for_dispatcher, pbt);
#else
        thread = g_thread_create((GThreadFunc)benchmark_crunch_for_dispatcher, pbt,TRUE,NULL);
#endif
        threads = g_slist_prepend(threads, thread);

        DEBUG("thread %d launched as context %p", thread_number, thread);
    }

    /* wait for time */
    // while ( g_timer_elapsed(timer, NULL) < seconds ) { }
    g_usleep(seconds * 1000000);

    /* signal all threads to stop */
    stop = 1;
    g_timer_stop(timer);

    ret.result = 0;
    DEBUG("waiting for all threads to finish");
    for (t = threads; t; t = t->next) {
        DEBUG("waiting for thread with context %p", t->data);
        gpointer *rv = g_thread_join((GThread *)t->data);
        ret.result += *(double *)rv;
        g_free(rv);
    }

    ret.elapsed_time = g_timer_elapsed(timer, NULL);

    g_slist_free(threads);
    g_timer_destroy(timer);

    return ret;
}

static gpointer benchmark_parallel_for_dispatcher(gpointer data)
{
    ParallelBenchTask *pbt = (ParallelBenchTask *)data;
    gpointer (*callback)(unsigned int start, unsigned int end, void *data,
                         gint thread_number);
    gpointer return_value = NULL;

    if ((callback = pbt->callback)) {
        DEBUG("this is thread %p; items %d -> %d, data %p", g_thread_self(),
              pbt->start, pbt->end, pbt->data);
        return_value =
            callback(pbt->start, pbt->end, pbt->data, pbt->thread_number);
        DEBUG("this is thread %p; return value is %p", g_thread_self(),
              return_value);
    } else {
        DEBUG("this is thread %p; callback is NULL and it should't be!",
              g_thread_self());
    }

    g_free(pbt);

    return return_value;
}

/* one call for each thread to be used */
bench_value
benchmark_parallel(gint n_threads, gpointer callback, gpointer callback_data)
{
    int cpu_procs, cpu_cores, cpu_threads, cpu_nodes;
    cpu_procs_cores_threads_nodes(&cpu_procs, &cpu_cores, &cpu_threads, &cpu_nodes);

    if (n_threads == 0)
        n_threads = cpu_threads;
    else if (n_threads == -1)
        n_threads = cpu_cores;

    return benchmark_parallel_for(n_threads, 0, n_threads, callback,
                                  callback_data);
}

/* Note:
 *    benchmark_parallel_for(): element [start] included, but [end] is excluded.
 *    callback(): expected to processes elements [start] through [end]
 * inclusive.
 */
bench_value benchmark_parallel_for(gint n_threads,
                                   guint start,
                                   guint end,
                                   gpointer callback,
                                   gpointer callback_data)
{
    gchar *temp;
    int cpu_procs, cpu_cores, cpu_threads, cpu_nodes;
    guint iter_per_thread, iter, thread_number = 0;
    GSList *threads = NULL, *t;
    GTimer *timer;

    bench_value ret = EMPTY_BENCH_VALUE;

    timer = g_timer_new();

    cpu_procs_cores_threads_nodes(&cpu_procs, &cpu_cores, &cpu_threads, &cpu_nodes);

    if (n_threads > 0)
        ret.threads_used = n_threads;
    else if (n_threads < 0)
        ret.threads_used = cpu_cores;
    else
        ret.threads_used = cpu_threads;

    while (ret.threads_used > 0) {
        iter_per_thread = (end - start) / ret.threads_used;

        if (iter_per_thread == 0) {
            DEBUG("not enough items per thread; disabling one thread");
            ret.threads_used--;
        } else {
            break;
        }
    }

    DEBUG("Using %d threads across %d logical processors; processing %d "
          "elements (%d per thread)",
          ret.threads_used, cpu_threads, (end - start), iter_per_thread);

    g_timer_start(timer);
    for (iter = start; iter < end;) {
        ParallelBenchTask *pbt = g_new0(ParallelBenchTask, 1);
        GThread *thread;

        guint ts = iter, te = iter + iter_per_thread;
        /* add the remainder of items/iter_per_thread to the last thread */
        if (end - te < iter_per_thread)
            te = end;
        iter = te;

        DEBUG("launching thread %d", 1 + thread_number);

        pbt->thread_number = thread_number++;
        pbt->start = ts;
        pbt->end = te - 1;
        pbt->data = callback_data;
        pbt->callback = callback;

#if GLIB_CHECK_VERSION(2,32,0)
        thread = g_thread_new("dispatcher", (GThreadFunc)benchmark_parallel_for_dispatcher, pbt);
#else
        thread = g_thread_create((GThreadFunc)benchmark_parallel_for_dispatcher, pbt,TRUE,NULL);
#endif

        threads = g_slist_prepend(threads, thread);

        DEBUG("thread %d launched as context %p", thread_number, thread);
    }

    DEBUG("waiting for all threads to finish");
    for (t = threads; t; t = t->next) {
        DEBUG("waiting for thread with context %p", t->data);
        gpointer *rv = g_thread_join((GThread *)t->data);
        if (rv) {
            if (ret.result == -1.0)
                ret.result = 0;
            ret.result += *(double *)rv;
        }
        g_free(rv);
    }

    g_timer_stop(timer);
    ret.elapsed_time = g_timer_elapsed(timer, NULL);

    g_slist_free(threads);
    g_timer_destroy(timer);

    DEBUG("finishing; all threads took %f seconds to finish", ret.elapsed_time);

    return ret;
}

gchar *hi_more_info(gchar *entry)
{
    const gchar *info = moreinfo_lookup_with_prefix("BENCH", entry);
    return g_strdup(info ? info : "?");
}

gchar *hi_get_field(gchar *field)
{
    const gchar *info = moreinfo_lookup_with_prefix("BENCH", field);
    return g_strdup(info ? info : field);
}

static void br_mi_add(char **results_list, bench_result *b, gboolean select)
{
    static unsigned int ri = 0; /* to ensure key is unique */
    gchar *rkey, *lbl, *elbl, *this_marker;

    if (select) {
        this_marker = format_with_ansi_color(_("This Machine"), "0;30;43",
                                             params.fmt_opts);
    } else {
        this_marker = "";
    }

    rkey = g_strdup_printf("%s__%d", b->machine->mid, ri++);

    lbl = g_strdup_printf("%s%s%s%s", this_marker, select ? " " : "",
                          b->machine->cpu_name,
                          b->legacy ? problem_marker() : "");
    elbl = key_label_escape(lbl);

    *results_list = h_strdup_cprintf("$@%s%s$%s=%.2f|%s\n", *results_list,
                                     select ? "*" : "", rkey, elbl,
                                     b->bvalue.result, b->machine->cpu_config);

    moreinfo_add_with_prefix("BENCH", rkey, bench_result_more_info(b));

    g_free(lbl);
    g_free(elbl);
    g_free(rkey);
    if (*this_marker)
        g_free(this_marker);
}
gint bench_result_sort(gconstpointer a, gconstpointer b)
{
    bench_result *A = (bench_result *)a, *B = (bench_result *)b;
    if (A->bvalue.result < B->bvalue.result)
        return -1;
    if (A->bvalue.result > B->bvalue.result)
        return 1;
    return 0;
}

struct append_machine_result_json_data {
    GSList **result_list;
    const gchar *benchmark_name;
};

static void append_machine_result_json(JsonArray *array,
                                       guint index,
                                       JsonNode *element_node,
                                       gpointer user_data)
{
    struct append_machine_result_json_data *data = user_data;
    bench_result *result;

    result = bench_result_benchmarkjson(data->benchmark_name, element_node);
    *data->result_list = g_slist_append(*data->result_list, result);
}

static GSList *benchmark_include_results_json(const gchar *path,
                                              bench_value r,
                                              const gchar *benchmark)
{
    JsonParser *parser;
    JsonNode *root;
    bench_result *this_machine = NULL;
    GSList *result_list = NULL;
    GError *error=NULL;

    DEBUG("Loading benchmark results from JSON file %s", path);

    parser = json_parser_new();
    json_parser_load_from_file(parser, path, &error);
    if(error){
        DEBUG ("Unable to parse JSON %s %s", path, error->message);
        g_error_free(error);
        g_object_unref(parser);
        return result_list;
    }

    root = json_parser_get_root(parser);
    if (json_node_get_node_type(root) != JSON_NODE_OBJECT)  goto out;

    JsonObject *results = json_node_get_object(root);
    if (results) {
        JsonArray *machines = json_object_get_array_member(results, benchmark);

        if (machines) {
            struct append_machine_result_json_data data = {
                .result_list = &result_list,
                .benchmark_name = benchmark,
            };
            json_array_foreach_element(machines, append_machine_result_json,
                                       &data);
        }
    }

out:
    g_object_unref(parser);

    return result_list;
}

static gchar *find_benchmark_conf(void)
{
    const gchar *config_dir = g_get_user_config_dir();
    gchar *path;

    path = g_build_filename(config_dir, "hardinfo2", "benchmark.json", NULL);
    if (g_file_test(path, G_FILE_TEST_EXISTS))
        return path;
    g_free(path);

    path = g_build_filename(params.path_data, "benchmark.json", NULL);
    if (g_file_test(path, G_FILE_TEST_EXISTS))
        return path;
    g_free(path);

    return NULL;
}

struct bench_window {
    int min, max;
};

static struct bench_window get_bench_window(GSList *result_list,
                                            const bench_result *this_machine)
{
    struct bench_window window = {};
    int size = params.max_bench_results;
    int len = g_slist_length(result_list);

    if (size == 0)
        size = 1;
    else if (size < 0)
        size = len;

    int loc = g_slist_index(result_list, this_machine); /* -1 if not found */
    if (loc >= 0) {
        window.min = loc - size / 2;
        window.max = window.min + size;
        if (window.min < 0) {
            window.min = 0;
            window.max = MIN(size, len);
        } else if (window.max > len) {
            window.max = len;
            window.min = MAX(len - size, 0);
        }
    } else {
        window.min = 0;
        window.max = len;
    }

    DEBUG("...len: %d, loc: %d, win_size: %d, win: [%d..%d]\n", len, loc, size,
          window.min, window.max - 1);

    return window;
}

static gboolean is_in_bench_window(const struct bench_window *window, int i)
{
    return i >= window->min && i < window->max;
}

static gchar *benchmark_include_results_internal(bench_value this_machine_value,
                                                 const gchar *benchmark,
                                                 ShellOrderType order_type)
{
    bench_result *this_machine;
    GSList *result_list, *li;
    gchar *results = g_strdup("");
    gchar *output;
    gchar *path;
    gint i;

    path = find_benchmark_conf();
    if (path) {
        result_list = benchmark_include_results_json(
            path, this_machine_value, benchmark);
    }

    /* this result */
    if (this_machine_value.result > 0.0) {
        this_machine = bench_result_this_machine(benchmark, this_machine_value);
        result_list = g_slist_prepend(result_list, this_machine);
    } else {
        this_machine = NULL;
    }

    /* sort */
    result_list = g_slist_sort(result_list, bench_result_sort);
    if (order_type == SHELL_ORDER_DESCENDING)
        result_list = g_slist_reverse(result_list);

    /* prepare for shell */
    moreinfo_del_with_prefix("BENCH");

    const struct bench_window window =
        get_bench_window(result_list, this_machine);
    for (i = 0, li = result_list; li; li = g_slist_next(li), i++) {
        bench_result *br = li->data;

        if (is_in_bench_window(&window, i))
            br_mi_add(&results, br, br == this_machine);

        bench_result_free(br); /* no longer needed */
    }
    g_slist_free(result_list);

    output = g_strdup_printf("[$ShellParam$]\n"
                             "Zebra=1\n"
                             "OrderType=%d\n"
                             "ViewType=4\n"
                             "ColumnTitle$Extra1=%s\n"    /* CPU Clock */
                             "ColumnTitle$Progress=%s\n"  /* Results */
                             "ColumnTitle$TextValue=%s\n" /* CPU */
                             "ShowColumnHeaders=true\n"
                             "[%s]\n%s",
                             order_type, _("CPU Config"), _("Results"),
                             _("CPU"), benchmark, results);

    g_free(path);
    g_free(results);

    return output;
}

static gchar *benchmark_include_results_reverse(bench_value result,
                                                const gchar *benchmark)
{
    return benchmark_include_results_internal(result, benchmark,
                                              SHELL_ORDER_DESCENDING);
}

static gchar *benchmark_include_results(bench_value result,
                                        const gchar *benchmark)
{
    return benchmark_include_results_internal(result, benchmark,
                                              SHELL_ORDER_ASCENDING);
}

typedef struct _BenchmarkDialog BenchmarkDialog;
struct _BenchmarkDialog {
    GtkWidget *dialog;
    bench_value r;
};

static gboolean
do_benchmark_handler(GIOChannel *source, GIOCondition condition, gpointer data)
{
    BenchmarkDialog *bench_dialog = (BenchmarkDialog *)data;
    GIOStatus status;
    gchar *result;
    bench_value r = EMPTY_BENCH_VALUE;

    status = g_io_channel_read_line(source, &result, NULL, NULL, NULL);
    if (status != G_IO_STATUS_NORMAL) {
        DEBUG("error while reading benchmark result");
        r.result = -1.0f;
        bench_dialog->r = r;
        gtk_widget_destroy(bench_dialog->dialog);
        return FALSE;
    }

    r = bench_value_from_str(result);
    /* attach a user note */
    if (params.bench_user_note)
        strncpy(r.user_note, params.bench_user_note, 255);
    bench_dialog->r = r;

    gtk_widget_destroy(bench_dialog->dialog);
    g_free(result);

    return FALSE;
}

static void do_benchmark(void (*benchmark_function)(void), int entry)
{
    int old_priority = 0;

    if (params.skip_benchmarks)
        return;

    if (params.gui_running) {
        gchar *argv[] = {params.argv0, "-b",           entries[entry].name,
                         "-m",         "benchmark.so", "-a",
                         NULL};
        GPid bench_pid;
        gint bench_stdout;
        GtkWidget *bench_dialog;
        GtkWidget *bench_image;
        BenchmarkDialog *benchmark_dialog;
        GSpawnFlags spawn_flags = G_SPAWN_STDERR_TO_DEV_NULL;
        gchar *bench_status;
        GtkWidget *content_area, *box, *label;
        bench_value r = EMPTY_BENCH_VALUE;
        bench_results[entry] = r;

        bench_status =
            g_strdup_printf(_("Benchmarking: <b>%s</b>."), entries[entry].name);

        shell_view_set_enabled(FALSE);
        shell_status_update(bench_status);

        g_free(bench_status);

        bench_image = icon_cache_get_image("benchmark.png");

	bench_dialog = gtk_dialog_new_with_buttons ("Benchmarking...",
                                      GTK_WINDOW(shell_get_main_shell()->transient_dialog),
                                      GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
                                      "Stop",
                                      GTK_BUTTONS_CLOSE,
                                      NULL);

	gtk_widget_set_sensitive(GTK_WIDGET(shell_get_main_shell()->transient_dialog), FALSE);

	content_area = gtk_dialog_get_content_area (GTK_DIALOG(bench_dialog));

#if GTK_CHECK_VERSION(3,0,0)
	box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1);
#else
	box = gtk_hbox_new(FALSE, 1);
#endif
	label = gtk_label_new ("Please do not move your mouse\n"
	    "or press any keys.");

	gtk_widget_show (bench_image);

#if GTK_CHECK_VERSION(3,0,0)
	gtk_widget_set_halign (bench_image, GTK_ALIGN_START);
#else
        gtk_misc_set_alignment(GTK_MISC(bench_image), 0.0, 0.0);
#endif

	g_signal_connect_swapped (bench_dialog,
				  "response",
				  G_CALLBACK (gtk_widget_destroy),
				  bench_dialog);

	gtk_box_pack_start (GTK_BOX(box), bench_image, TRUE, TRUE, 10);
	gtk_box_pack_start (GTK_BOX(box), label, TRUE, TRUE, 10);
	gtk_container_add (GTK_CONTAINER(content_area), box);

	gtk_window_set_deletable(GTK_WINDOW(bench_dialog), FALSE);
	gtk_widget_show_all (bench_dialog);

        while (gtk_events_pending()) {
            gtk_main_iteration();
        }

        benchmark_dialog = g_new0(BenchmarkDialog, 1);
        benchmark_dialog->dialog = bench_dialog;
        benchmark_dialog->r = r;

        if (!g_path_is_absolute(params.argv0)) {
            spawn_flags |= G_SPAWN_SEARCH_PATH;
        }

        if (g_spawn_async_with_pipes(NULL, argv, NULL, spawn_flags, NULL, NULL,
                                     &bench_pid, NULL, &bench_stdout, NULL,
                                     NULL)) {
            GIOChannel *channel;
            guint watch_id;

            DEBUG("spawning benchmark; pid=%d", bench_pid);

            channel = g_io_channel_unix_new(bench_stdout);
            watch_id = g_io_add_watch(channel, G_IO_IN, do_benchmark_handler,
                                      benchmark_dialog);

            switch (gtk_dialog_run(GTK_DIALOG(bench_dialog))) {
            case GTK_RESPONSE_NONE:
                DEBUG("benchmark finished");
                break;
            case GTK_RESPONSE_ACCEPT:
                DEBUG("cancelling benchmark");

                gtk_widget_destroy(bench_dialog);
                g_source_remove(watch_id);
                kill(bench_pid, SIGINT);
            }

            bench_results[entry] = benchmark_dialog->r;

            g_io_channel_unref(channel);
            shell_view_set_enabled(TRUE);
            shell_status_set_enabled(TRUE);
            gtk_widget_set_sensitive(
                GTK_WIDGET(shell_get_main_shell()->transient_dialog), TRUE);
            g_free(benchmark_dialog);

            shell_status_update(_("Done."));

            return;
        }

        gtk_widget_destroy(bench_dialog);
        g_free(benchmark_dialog);
        shell_status_set_enabled(TRUE);
        shell_status_update(_("Done."));
    }

    setpriority(PRIO_PROCESS, 0, -20);
    benchmark_function();
    setpriority(PRIO_PROCESS, 0, old_priority);
}

gchar *hi_module_get_name(void) { return g_strdup(_("Benchmarks")); }

guchar hi_module_get_weight(void) { return 240; }

ModuleEntry *hi_module_get_entries(void) { return entries; }

const ModuleAbout *hi_module_get_about(void)
{
    static const ModuleAbout ma = {
        .author = "L. A. F. Pereira",
        .description = N_("Perform tasks and compare with other systems"),
        .version = VERSION,
        .license = "GNU GPL version 2 or later.",
    };

    return &ma;
}

static gchar *get_benchmark_results(gsize *len)
{
    void (*scan_callback)(gboolean);
    JsonBuilder *builder;
    JsonGenerator *generator;
    JsonNode *root;
    bench_machine *this_machine;
    gchar *out;
    guint i;

    for (i = 0; i < G_N_ELEMENTS(entries); i++) {
        if (!entries[i].name || !entries[i].scan_callback)
            continue;
        if (entries[i].flags & MODULE_FLAG_HIDE)
            continue;

        scan_callback = entries[i].scan_callback;
        if (scan_callback)
            scan_callback(bench_results[i].result < 0.0);
    }

    this_machine = bench_machine_this();
    builder = json_builder_new();
    json_builder_begin_object(builder);
    for (i = 0; i < G_N_ELEMENTS(entries); i++) {
        if (!entries[i].name || entries[i].flags & MODULE_FLAG_HIDE)
            continue;
        if (bench_results[i].result < 0.0) {
            /* Benchmark failed? */
            continue;
        }

        json_builder_set_member_name(builder, entries[i].name);

        json_builder_begin_object(builder);

#define ADD_JSON_VALUE(type, name, value)                                      \
    do {                                                                       \
        json_builder_set_member_name(builder, (name));                         \
        json_builder_add_##type##_value(builder, (value));                     \
    } while (0)

        ADD_JSON_VALUE(string, "Board", this_machine->board);
        ADD_JSON_VALUE(int, "MemoryInKiB", this_machine->memory_kiB);
        ADD_JSON_VALUE(string, "CpuName", this_machine->cpu_name);
        ADD_JSON_VALUE(string, "CpuDesc", this_machine->cpu_desc);
        ADD_JSON_VALUE(string, "CpuConfig", this_machine->cpu_config);
        ADD_JSON_VALUE(string, "CpuConfig", this_machine->cpu_config);
        ADD_JSON_VALUE(string, "OpenGlRenderer", this_machine->ogl_renderer);
        ADD_JSON_VALUE(string, "GpuDesc", this_machine->gpu_desc);
        ADD_JSON_VALUE(int, "NumCpus", this_machine->processors);
        ADD_JSON_VALUE(int, "NumCores", this_machine->cores);
        ADD_JSON_VALUE(int, "NumNodes", this_machine->nodes);
        ADD_JSON_VALUE(int, "NumThreads", this_machine->threads);
        ADD_JSON_VALUE(string, "MachineId", this_machine->mid);
        ADD_JSON_VALUE(int, "PointerBits", this_machine->ptr_bits);
        ADD_JSON_VALUE(boolean, "DataFromSuperUser", this_machine->is_su_data);
        ADD_JSON_VALUE(int, "PhysicalMemoryInMiB",
                       this_machine->memory_phys_MiB);
        ADD_JSON_VALUE(string, "MemoryTypes", this_machine->ram_types);
        ADD_JSON_VALUE(int, "MachineDataVersion",
                       this_machine->machine_data_version);
        ADD_JSON_VALUE(string, "MachineType", this_machine->machine_type);

        ADD_JSON_VALUE(boolean, "Legacy", FALSE);
        ADD_JSON_VALUE(string, "ExtraInfo", bench_results[i].extra);
        ADD_JSON_VALUE(string, "UserNote", bench_results[i].user_note);
        ADD_JSON_VALUE(double, "BenchmarkResult", bench_results[i].result);
        ADD_JSON_VALUE(double, "ElapsedTime", bench_results[i].elapsed_time);
        ADD_JSON_VALUE(int, "UsedThreads", bench_results[i].threads_used);
        ADD_JSON_VALUE(int, "BenchmarkVersion", bench_results[i].revision);

#undef ADD_JSON_VALUE

        json_builder_end_object(builder);
    }
    json_builder_end_object(builder);

    generator = json_generator_new();
    json_generator_set_root(generator, json_builder_get_root(builder));
    json_generator_set_pretty(generator, TRUE);

    out = json_generator_to_data(generator, len);

    g_object_unref(generator);
    g_object_unref(builder);
    bench_machine_free(this_machine);

    return out;
}

static gchar *run_benchmark(gchar *name)
{
    int i;

    DEBUG("name = %s", name);

    for (i = 0; entries[i].name; i++) {
        if (g_str_equal(entries[i].name, name)) {
            void (*scan_callback)(gboolean rescan);

            if ((scan_callback = entries[i].scan_callback)) {
                scan_callback(FALSE);

#define CHK_RESULT_FORMAT(F)                                                   \
    (params.result_format && strcmp(params.result_format, F) == 0)

                if (params.run_benchmark) {
                    /* attach the user note */
                    if (params.bench_user_note)
                        strncpy(bench_results[i].user_note,
                                params.bench_user_note, 255);

                    if (CHK_RESULT_FORMAT("shell")) {
                        bench_result *b =
                            bench_result_this_machine(name, bench_results[i]);
                        char *temp = bench_result_more_info_complete(b);
                        bench_result_free(b);
                        return temp;
                    }
                    /* defaults to "short" which is below */
                }

                return bench_value_to_str(bench_results[i]);
            }
        }
    }

    return NULL;
}

const ShellModuleMethod *hi_exported_methods(void)
{
    static const ShellModuleMethod m[] = {
        {"runBenchmark", run_benchmark},
        {NULL},
    };

    return m;
}

void hi_module_init(void)
{
    static SyncEntry se[] = {
        {
            .name = N_("Send benchmark results"),
            .file_name = "benchmark.json",
            .generate_contents_for_upload = get_benchmark_results,
        },
        {
            .name = N_("Receive benchmark results"),
            .file_name = "benchmark.json",
        },
    };

    sync_manager_add_entry(&se[0]);
    sync_manager_add_entry(&se[1]);

    guint i;
    for (i = 0; i < G_N_ELEMENTS(entries) - 1 /* account for NULL */; i++)
        bench_results[i] = (bench_value)EMPTY_BENCH_VALUE;
}

gchar **hi_module_get_dependencies(void)
{
    static gchar *deps[] = {"devices.so", NULL};

    return deps;
}