[Spice-devel] [RFC spice-gtk 5/5] spicy: save data measurements for each channel

Victor Toso victortoso at redhat.com
Fri Apr 7 13:19:30 UTC 2017


From: Victor Toso <me at victortoso.com>

In order to verify the network usage per channel, we introduce the
--save-network-data option to spicy which will store the network data
in the following format:

 index[1], s-write, s-read, ch1-write, ch1-read, ..., chN-write, chN-read
 index[2], s-write, s-read, ch1-write, ch1-read, ..., chN-write, chN-read
 index[3], s-write, s-read, ch1-write, ch1-read, ..., chN-write, chN-read
 ...
 #column 2 session write
 #column 3 session read
 #column 4 channel1-name write
 ...

With this format, we can use the data to plot charts of the overall
network usage per channel. A tiny script is provided that uses gnuplot
for plotting, it should be used as:

$ ./tools/spicy-gnuplot.sh spicy-network-data-HHMMSS

Signed-off-by: Victor Toso <victortoso at redhat.com>
---
 tools/spicy-gnuplot.sh |  19 ++++
 tools/spicy.c          | 259 ++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 277 insertions(+), 1 deletion(-)
 create mode 100755 tools/spicy-gnuplot.sh

diff --git a/tools/spicy-gnuplot.sh b/tools/spicy-gnuplot.sh
new file mode 100755
index 0000000..87acb8a
--- /dev/null
+++ b/tools/spicy-gnuplot.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# FIXME:
+# Not putting too much effort on this for now. It should be a python
+# script with more arguments to generate different tipes of charts
+function plot_spicy_file()
+{
+    local data=$(grep "#column" $1 | \
+        awk -v file=$1 '{print "\""file "\" using 1:"$2" title \""$3" "$4"\", "}')
+    data="set key outside; set style data lines; plot $data"
+    gnuplot -p -e "$data"
+}
+
+if [ "$#" -ne 1 ] || ! [ -f "$1" ]; then
+    echo "Usage: $0 <spicy-network-data-file>" >&2
+    exit 1
+fi
+
+plot_spicy_file $1
diff --git a/tools/spicy.c b/tools/spicy.c
index eeb640d..82d5134 100644
--- a/tools/spicy.c
+++ b/tools/spicy.c
@@ -39,6 +39,7 @@
 #include "spicy-connect.h"
 
 typedef struct spice_connection spice_connection;
+typedef struct spice_channel_network_data spice_channel_network_data;
 
 enum {
     STATE_SCROLL_LOCK,
@@ -91,6 +92,13 @@ G_DEFINE_TYPE (SpiceWindow, spice_window, G_TYPE_OBJECT);
 #define CHANNELID_MAX 4
 #define MONITORID_MAX 4
 
+struct spice_channel_network_data {
+    gint64 time;    /* Time when measurements were taken */
+    gsize wdata;    /* in mega bytes */
+    gsize rdata;
+    gdouble wrate;  /* in mega bytes per second */
+    gdouble rrate;
+};
 
 // FIXME: turn this into an object, get rid of fixed wins array, use
 // signals to replace the various callback that iterate over wins array
@@ -105,12 +113,29 @@ struct spice_connection {
     gboolean         agent_connected;
     int              channels;
     int              disconnecting;
+    int              network_data_id;
+
+    struct {
+        GHashTable        *table; /* Map channels to spice_channel_network_data */
+        GList             *list; /* List of channels in a fixed order for reference */
+        GFile             *file;
+        GFileOutputStream *stream;
+        guint              index;
+        GQueue            *network_data_queue;
+        bool               ongoing_saving_async;
+    } netdata;
 
     /* key: SpiceFileTransferTask, value: TransferTaskWidgets */
     GHashTable *transfers;
     GtkWidget *transfer_dialog;
 };
 
+typedef struct {
+    spice_connection *conn;
+    gchar *data;
+    gsize size;
+} save_network_data_op;
+
 static spice_connection *connection_new(void);
 static void connection_connect(spice_connection *conn);
 static void connection_disconnect(spice_connection *conn);
@@ -121,10 +146,13 @@ static void usb_connect_failed(GObject               *object,
                                gpointer               data);
 static gboolean is_gtk_session_property(const gchar *property);
 static void del_window(spice_connection *conn, SpiceWindow *win);
+static void write_network_data_op_async(spice_connection *conn,
+                                        save_network_data_op *op);
 
 /* options */
 static gboolean fullscreen = false;
 static gboolean version = false;
+static gboolean should_save_network_data = false;
 static char *spicy_title = NULL;
 /* globals */
 static GMainLoop     *mainloop = NULL;
@@ -1724,11 +1752,17 @@ static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data)
 {
     spice_connection *conn = data;
     int id;
+    spice_channel_network_data *netdata;
 
     g_object_get(channel, "channel-id", &id, NULL);
     conn->channels++;
     SPICE_DEBUG("new channel (#%d)", id);
 
+    netdata = g_new0(spice_channel_network_data, 1);
+    netdata->time = g_get_monotonic_time();
+    g_hash_table_insert(conn->netdata.table, channel, netdata);
+    conn->netdata.list = g_list_append(conn->netdata.list, channel);
+
     if (SPICE_IS_MAIN_CHANNEL(channel)) {
         SPICE_DEBUG("new main channel");
         conn->main = SPICE_MAIN_CHANNEL(channel);
@@ -1854,21 +1888,237 @@ static spice_connection *connection_new(void)
     conn->transfers = g_hash_table_new_full(g_direct_hash, g_direct_equal,
                                             g_object_unref,
                                             (GDestroyNotify)transfer_task_widgets_free);
+    conn->netdata.table = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free);
+    conn->netdata.network_data_queue = g_queue_new();
     connections++;
     SPICE_DEBUG("%s (%d)", __FUNCTION__, connections);
     return conn;
 }
 
+static void write_network_data_op_cb(GObject *source_object,
+                                     GAsyncResult *res,
+                                     gpointer user_data)
+{
+    gboolean ret;
+    gsize bytes_written;
+    save_network_data_op *op = user_data;
+    GError *error = NULL;
+    spice_connection *conn = op->conn;
+
+    ret = g_output_stream_write_all_finish(G_OUTPUT_STREAM(source_object),
+                                           res, &bytes_written, &error);
+    g_assert_no_error(error);
+    g_assert_true(ret);
+    g_assert_cmpint(op->size, ==, bytes_written);
+    g_free(op->data);
+    g_free(op);
+
+    /* Keep writing till the queue is empty */
+    conn->netdata.ongoing_saving_async = false;
+    write_network_data_op_async(conn, NULL);
+}
+
+static void write_network_data_op_async(spice_connection *conn,
+                                        save_network_data_op *op)
+{
+    if (conn->netdata.ongoing_saving_async) {
+        g_assert_nonnull(op);
+        g_queue_push_tail(conn->netdata.network_data_queue, op);
+        return;
+    }
+
+    if (op == NULL)
+        op = g_queue_pop_head(conn->netdata.network_data_queue);
+
+    if (op == NULL)
+        return;
+
+    conn->netdata.ongoing_saving_async = true;
+    g_output_stream_write_all_async(G_OUTPUT_STREAM(conn->netdata.stream),
+                                    op->data, op->size, G_PRIORITY_DEFAULT,
+                                    NULL, write_network_data_op_cb, op);
+}
+
+static void save_network_data_comments_sync(spice_connection *conn)
+{
+    GString *str;
+    GList *it;
+    gint i;
+    GError *error = NULL;
+
+    str = g_string_new(NULL);
+    /* Index note: gnuplot start with 1 */
+    g_string_printf(str,
+                    "#column 2 bandwidth\n"
+                    "#column 3 session write\n"
+                    "#column 4 session read\n");
+    i = 5;
+    for (it = conn->netdata.list; it != NULL; it = it->next) {
+        SpiceChannel *channel = SPICE_CHANNEL(it->data);
+        gint type;
+        const gchar *name;
+
+        g_object_get(channel, "channel-type", &type, NULL);
+        name = spice_channel_type_to_string(type);
+        g_string_append_printf(str,
+                               "#column %d %s write\n"
+                               "#column %d %s read\n",
+                               i, name, (i + 1), name);
+        i += 2;
+    }
+
+    /* sync: it should block */
+    g_output_stream_write(G_OUTPUT_STREAM(conn->netdata.stream),
+                          str->str, str->len,
+                          NULL, &error);
+    g_string_free(str, TRUE);
+    g_assert_no_error(error);
+}
+
+static void save_network_data(spice_connection *conn,
+                              gdouble session_write_rate,
+                              gdouble session_read_rate)
+{
+    save_network_data_op *op;
+    GString *str;
+    GList *it;
+
+    /* Format to be saved is:
+     *   index, session write+read rate, session_write_rate, session_read_rate,
+     *   channel1_write_rate, channel1_read_rate, ..., channelN_read_rate
+     * When the file is closed, we include a comment with mapping pair of
+     * columns to the channel's name (i.e 1-main, n-webdav) */
+
+    conn->netdata.index++;
+    str = g_string_new(NULL);
+    g_string_printf(str, "%u, %.5f, %.5f, %.5f", conn->netdata.index,
+                    (session_write_rate + session_read_rate),
+                    session_write_rate, session_read_rate);
+    for (it = conn->netdata.list; it != NULL; it = it->next) {
+        spice_channel_network_data *d;
+
+        d = g_hash_table_lookup(conn->netdata.table, it->data);
+        g_string_append_printf(str, ", %.5f, %.5f", d->wrate, d->rrate);
+    }
+    g_string_append_printf(str, "\n");
+
+    op = g_new0(save_network_data_op, 1);
+    op->conn = conn;
+    op->data = g_string_free(str, FALSE);
+    op->size = strlen(op->data);
+
+    write_network_data_op_async(conn, op);
+}
+
+static gboolean network_data_cb(gpointer user_data)
+{
+    spice_connection *conn;
+    GList *it, *list;
+    gdouble session_wrate, session_rrate;
+    gint64 time;
+
+    conn = user_data;
+    time = g_get_monotonic_time();
+
+    session_wrate = session_rrate = 0.0;
+    list = spice_session_get_channels(conn->session);
+    for (it = list; it != NULL; it = it->next) {
+        SpiceChannel *channel;
+        gsize total_read_bytes, total_write_bytes;
+        gdouble wrate, rrate, data, interval;
+        spice_channel_network_data *netdata;
+
+        channel = SPICE_CHANNEL(it->data);
+        g_object_get(channel,
+                     "total-read-bytes", &total_read_bytes,
+                     "total-write-bytes", &total_write_bytes,
+                     NULL);
+
+        netdata = g_hash_table_lookup(conn->netdata.table, channel);
+        g_return_val_if_fail(netdata != NULL, G_SOURCE_REMOVE);
+
+        interval = time - netdata->time;
+        data = total_write_bytes - netdata->wdata;
+        wrate = (data * 1000000.0) / (interval * 1024.0 * 1024.0);
+        data = total_read_bytes - netdata->rdata;
+        rrate = (data * 1000000.0) / (interval * 1024.0 * 1024.0);
+
+        netdata->time = time;
+        netdata->wdata = total_write_bytes;
+        netdata->wrate = wrate;
+        netdata->rdata = total_read_bytes;
+        netdata->rrate = rrate;
+
+        /* For the session */
+        session_wrate += wrate;
+        session_rrate += rrate;
+    }
+    g_list_free(list);
+    save_network_data(conn, session_wrate, session_rrate);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void save_network_stop(spice_connection *conn)
+{
+    gchar *path;
+
+    if (conn->network_data_id != 0)
+        g_source_remove(conn->network_data_id);
+
+    save_network_data_comments_sync(conn);
+
+    path = g_file_get_path(conn->netdata.file);
+    SPICE_DEBUG("Data saved, file is being closed now %s", path);
+    g_free(path);
+    g_object_unref(conn->netdata.file);
+    g_object_unref(conn->netdata.stream);
+}
+
+static void save_network_start(spice_connection *conn)
+{
+    GDateTime *date;
+    gchar *current_dir, *date_str, *filename, *path;
+    GError *error = NULL;
+
+    date = g_date_time_new_now_local();
+    date_str = g_date_time_format(date, "%H%M%S");
+    filename = g_strdup_printf("spicy-network-data-%s", date_str);
+    g_free(date_str);
+    g_date_time_unref(date);
+
+    current_dir = g_get_current_dir();
+    path = g_build_filename(current_dir, filename, NULL);
+    g_free(current_dir);
+    g_free(filename);
+
+    conn->netdata.file = g_file_new_for_path(path);
+    SPICE_DEBUG("Network data will be stored at %s", path);
+    g_free(path);
+
+    conn->netdata.stream = g_file_create(conn->netdata.file, G_FILE_CREATE_NONE, NULL, &error);
+    g_assert_no_error(error);
+
+    conn->network_data_id = g_timeout_add_seconds(5, network_data_cb, conn);
+}
+
 static void connection_connect(spice_connection *conn)
 {
     conn->disconnecting = false;
-    spice_session_connect(conn->session);
+    if (!spice_session_connect(conn->session))
+        return;
+
+    if (should_save_network_data)
+        save_network_start(conn);
 }
 
 static void connection_disconnect(spice_connection *conn)
 {
     if (conn->disconnecting)
         return;
+
+    save_network_stop(conn);
+
     conn->disconnecting = true;
     spice_session_disconnect(conn->session);
 }
@@ -1877,6 +2127,8 @@ static void connection_destroy(spice_connection *conn)
 {
     g_object_unref(conn->session);
     g_hash_table_unref(conn->transfers);
+    g_hash_table_unref(conn->netdata.table);
+    g_list_free(conn->netdata.list);
     free(conn);
 
     connections--;
@@ -1909,6 +2161,11 @@ static GOptionEntry cmd_entries[] = {
         .description      = "Set the window title",
         .arg_description  = "<title>",
     },{
+        .long_name        = "save-network-data",
+        .arg              = G_OPTION_ARG_NONE,
+        .arg_data         = &should_save_network_data,
+        .description      = "Save network data in csv format",
+    },{
         /* end of list */
     }
 };
-- 
2.9.3



More information about the Spice-devel mailing list