[Spice-devel] [RFC spice-gtk 4/5] qos: introduce channel priority throttling

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


From: Victor Toso <me at victortoso.com>

The main goal of this change is to avoid high bandwidth usage from
channels that don't directly affect the user experience such as webdav
and usbredir.

In order to throttle correctly, this patch sets a priority on READ and
WRITE per channel and we will only apply bandwidth throttling on low
priority channels.

The bandwidth throttling might be enabled if the current session's
rate is currently above 80% of its capacity and lower priority
channels are using more then the lower threshold that QOS may enforce.

When bandwidth throttling is enabled, low priority channels's rate
should be always below 80% of maximum rate for given IO. The lower
threshold for throttling is 40% of maximum rate.

Also, while the bandwidth throttling is enabled we will keep the rate
set by qos_main() for low priority channels at any given time.

Resolves: https://bugs.freedesktop.org/show_bug.cgi?id=96598

Signed-off-by: Victor Toso <victortoso at redhat.com>
---
 src/spice-session.c | 255 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 249 insertions(+), 6 deletions(-)

diff --git a/src/spice-session.c b/src/spice-session.c
index d270c63..0b73d3a 100644
--- a/src/spice-session.c
+++ b/src/spice-session.c
@@ -132,6 +132,14 @@ struct _SpiceSessionPrivate {
     guint             qos_main_id;
     gdouble           maximum_read_rate;
     gdouble           maximum_write_rate;
+
+    gint64            time_throttling_write_started;
+    gdouble           nbytes_allowed_to_write;
+    gdouble           nbytes_written;
+
+    gint64            time_throttling_read_started;
+    gdouble           nbytes_allowed_to_read;
+    gdouble           nbytes_read;
 };
 
 #define QOS_BANDWITH_TIMEOUT    5 /* Seconds */
@@ -149,6 +157,32 @@ typedef struct session_channel_qos {
     gsize data_read;
 } session_channel_qos;
 
+typedef enum QOSPriorityEnum {
+    LOW,
+    DEFAULT,
+    HIGH,
+} QOSPriorityEnum;
+
+static const struct {
+    QOSPriorityEnum write;
+    QOSPriorityEnum read;
+} qos_channel_priority[] = {
+    [0] = { 0, 0 },
+    [SPICE_CHANNEL_MAIN] = { HIGH, HIGH },
+    [SPICE_CHANNEL_DISPLAY] = { DEFAULT, HIGH },
+    [SPICE_CHANNEL_INPUTS] = { HIGH, DEFAULT },
+    [SPICE_CHANNEL_CURSOR] = { HIGH, DEFAULT },
+    [SPICE_CHANNEL_PLAYBACK] = { DEFAULT, DEFAULT },
+    [SPICE_CHANNEL_RECORD] = { DEFAULT, DEFAULT },
+    [SPICE_CHANNEL_TUNNEL] = { LOW, LOW },
+    [SPICE_CHANNEL_SMARTCARD] = { DEFAULT, DEFAULT },
+    [SPICE_CHANNEL_USBREDIR] = { LOW, LOW },
+    [SPICE_CHANNEL_PORT] = { LOW, LOW },
+    [SPICE_CHANNEL_WEBDAV] = { LOW, LOW },
+};
+
+G_STATIC_ASSERT(G_N_ELEMENTS(qos_channel_priority) == SPICE_END_CHANNEL);
+
 /**
  * SECTION:spice-session
  * @short_description: handles connection details, and active channels
@@ -2881,6 +2915,65 @@ static void update_spice_channel_qos_data(SpiceChannel *channel,
     qos->data_read = total_read_bytes;
 }
 
+/* Only applicable to channels with lower priority */
+#define GET_MINIMUM_RATE(rate) (0.40 * rate)
+#define GET_MAXIMUM_RATE(rate) (0.80 * rate)
+#define ON_LOWER_THROTTLING_RATE(rate, cmp_rate) (rate <= GET_MINIMUM_RATE(cmp_rate))
+#define ON_UPPER_THROTTLING_RATE(rate, cmp_rate) (rate >= GET_MAXIMUM_RATE(cmp_rate))
+
+/* Return the amout of bytes allowed to read or write for a low priority channel
+ * based on its current rate, the session's current and maximum rate (always
+ * related to its IO type).
+ *
+ * @is_throttling: true if related channels is already being throttled
+ * @current_low_prio_rate, @current_session_rate, @maximum_session_rate: In MB/s
+ *
+ * It always returns a value between GET_MINIMUM_RATE() and GET_MAXIMUM_RATE()
+ * or 0 if throttling should be disabled.
+ *
+ * If it needs to increase or lower the channel's rate, it will do by 10% of
+ * given @maximum_session_rate.
+ */
+static gdouble qos_get_allowed_nbytes(bool is_throttling,
+                                      gdouble current_low_prio_rate,
+                                      gdouble current_session_rate,
+                                      gdouble maximum_session_rate)
+{
+    gdouble nbytes = 0;
+
+    /* If current session rate is on upper limit of maximum session rate and low
+     * priority channels's rate is above lower threshold, we can start throttling */
+    if (!is_throttling) {
+        if (ON_UPPER_THROTTLING_RATE(current_session_rate, maximum_session_rate) &&
+            !ON_LOWER_THROTTLING_RATE(current_low_prio_rate, maximum_session_rate)) {
+
+            nbytes = MAX(current_low_prio_rate - 0.10 * maximum_session_rate,
+                         GET_MINIMUM_RATE(maximum_session_rate));
+            goto nbytes_in_timeframe;
+        }
+        return 0;
+    }
+
+    /* Throttling is already enabled */
+    if (ON_UPPER_THROTTLING_RATE(current_session_rate, maximum_session_rate)) {
+        /* Current session rate is still close to maximum session rate. Let's
+         * increase the throttling by reducing low priority rate */
+        nbytes = MAX(current_low_prio_rate - 0.10 * maximum_session_rate,
+                     GET_MINIMUM_RATE(maximum_session_rate));
+    } else if (current_low_prio_rate != GET_MAXIMUM_RATE(maximum_session_rate)) {
+        /* Enough bandwidth left, let's increase by 10% once again */
+        nbytes =  MIN(current_low_prio_rate + 0.10 * maximum_session_rate,
+                      GET_MAXIMUM_RATE(maximum_session_rate));
+    } else {
+        /* Enough bandwidth yet, let's disable the throttling */
+        return 0;
+    }
+
+nbytes_in_timeframe:
+    /* From MB per second to number of bytes in qos interval */
+    return (nbytes * 1024 * 1024 * QOS_BANDWITH_TIMEOUT);
+}
+
 /**
  * qos_main
  *
@@ -2896,38 +2989,125 @@ static gboolean qos_main(gpointer user_data)
     SpiceSession *self = user_data;
     SpiceSessionPrivate *s = self->priv;
     GList *iter, *list;
+    bool throttling_write = false, throttling_read = false;
+    bool disable_throttling_write = false, disable_throttling_read = false;
     gdouble session_wrate = 0.0, session_rrate = 0.0;
+    gdouble wrate_low_now = 0.0, rrate_low_now = 0.0;
+    gdouble allowed_to_write = 0.0, allowed_to_read = 0.0;
+    gint64 time;
 
     list = spice_session_get_channels(self);
     for (iter = list; iter != NULL; iter = iter->next) {
         SpiceChannel *channel;
         session_channel_qos *qos;
+        gint type;
 
         channel = SPICE_CHANNEL(iter->data);
         qos = g_hash_table_lookup(s->qos_table, channel);
         g_return_val_if_fail(qos != NULL, G_SOURCE_REMOVE);
 
         update_spice_channel_qos_data(channel, qos);
+        type = spice_channel_get_channel_type(channel);
+
+        if (qos_channel_priority[type].read == LOW)
+            rrate_low_now += qos->read_rate;
+
+        if (qos_channel_priority[type].write == LOW)
+            wrate_low_now += qos->write_rate;
 
         /* For the session */
         session_wrate += qos->write_rate;
         session_rrate += qos->read_rate;
     }
 
+    /* Heuristics to follow:
+     * - We should only apply throttling when we are very close to know the
+     *   maximum write and read rate. We consider changes up to 10% not
+     *   significant to disable throttling;
+     * - Only consider throttling if current rate is bigger then 90% of maximum
+     *   rate;
+     * - In case current rate is bigger then the percentage rate set at
+     *   GET_MAXIMUM_RATE, we will apply throttling in low priority channels.
+     * - Per iteration we might increase or lower the bandwidth of low priority
+     *   changes by 10% of maximum bandwidth;
+     * - Lowest rate for throttling is given by GET_MINIMUM_RATE from maximum
+     *   bandwidth value
+     */
+
+    /* As write rate is increasing, nothing to do */
     if (s->maximum_write_rate < session_wrate) {
         spice_debug("[qos] Maximum write rate changed: %2.4f -> %2.4f MB/s",
                     s->maximum_write_rate, session_wrate);
 
+        /* Disable throttling if we have not yet found the maximum rate */
+        if (session_wrate > 1.10 * s->maximum_write_rate)
+            throttling_write = false;
+
         s->maximum_write_rate = session_wrate;
+    } else {
+        allowed_to_write = qos_get_allowed_nbytes(throttling_write, wrate_low_now,
+                                                  session_wrate, s->maximum_write_rate);
+        throttling_write = (allowed_to_write != 0);
     }
 
     if (s->maximum_read_rate < session_rrate) {
         spice_debug("[qos] Maximum read rate changed: %2.4f -> %2.4f MB/s",
                     s->maximum_read_rate, session_rrate);
 
+        /* Disable throttling if we have not yet found the maximum rate */
+        if (session_rrate > 1.10 * s->maximum_read_rate)
+            throttling_read = false;
+
         s->maximum_read_rate = session_rrate;
+    } else {
+        allowed_to_read = qos_get_allowed_nbytes(throttling_read, rrate_low_now,
+                                                 session_rrate, s->maximum_read_rate);
+        throttling_read = (allowed_to_read != 0);
+    }
+
+    /* Check if we should disable throttling */
+    disable_throttling_read = (!throttling_read && s->nbytes_allowed_to_read != 0);
+    disable_throttling_write = (!throttling_write && s->nbytes_allowed_to_write != 0);
+
+    s->nbytes_allowed_to_read = allowed_to_read;
+    s->nbytes_allowed_to_write = allowed_to_write;
+    s->nbytes_read = s->nbytes_written = 0;
+
+    /* Always set the time throttling has started in order to calculate the
+     * amount of bytes the channels are allowed to read/write at given time */
+    time = g_get_monotonic_time();
+    s->time_throttling_write_started = (throttling_write) ? time : 0;
+    s->time_throttling_read_started = (throttling_read) ? time : 0;
+
+    if (!throttling_write && !throttling_read &&
+        !disable_throttling_write && !disable_throttling_read)
+        goto end_qos_main;
+
+    spice_debug("[qos] enable throttling: read=%s write=%s, "
+                "disable throttling: read=%s write=%s",
+                spice_yes_no(throttling_read),
+                spice_yes_no(throttling_write),
+                spice_yes_no(disable_throttling_read),
+                spice_yes_no(disable_throttling_write));
+
+    /* Apply or Disable throttling */
+    for (iter = list; iter != NULL; iter = iter->next) {
+        SpiceChannel *channel;
+        session_channel_qos *qos;
+        gint type;
+
+        channel = SPICE_CHANNEL(iter->data);
+        qos = g_hash_table_lookup(s->qos_table, channel);
+        type = spice_channel_get_channel_type(channel);
+
+        if (qos_channel_priority[type].read == LOW)
+            qos->throttling_read_enabled = throttling_read;
+
+        if (qos_channel_priority[type].write == LOW)
+            qos->throttling_write_enabled = throttling_write;
     }
 
+end_qos_main:
     g_list_free(list);
     return G_SOURCE_CONTINUE;
 }
@@ -2954,6 +3134,21 @@ gboolean spice_session_set_migration_session(SpiceSession *session, SpiceSession
     return TRUE;
 }
 
+/* Calculate the time ratio from now since throttling has started (@start_time)
+ * based on qos interval (QOS_BANDWITH_TIMEOUT) and uses this ratio to check
+ * allowed amount of bytes to be performed at this time.
+ * This check denies the usage of all @allowed_bytes in a shorter period of time
+ * then the qos interval which would make given channels to use more bandwidtt
+ * then QOS allows */
+static bool qos_allow_io(gint64 start_time,
+                         gdouble current_nbytes,
+                         gdouble allowed_nbytes)
+{
+    gdouble interval = g_get_monotonic_time() - start_time;
+    gdouble ratio = interval / (QOS_BANDWITH_TIMEOUT * 1000000.0);
+    return (current_nbytes < ratio * allowed_nbytes);
+}
+
 G_GNUC_INTERNAL
 gboolean spice_session_qos_can_channel_write(SpiceSession *session,
                                              SpiceChannel *channel)
@@ -2971,7 +3166,18 @@ gboolean spice_session_qos_can_channel_write(SpiceSession *session,
     qos = g_hash_table_lookup(s->qos_table, channel);
     g_return_val_if_fail(qos != NULL, FALSE);
 
-    return !qos->throttling_write_enabled;
+    if (qos->throttling_write_enabled) {
+        bool allow = qos_allow_io(s->time_throttling_write_started,
+                                  s->nbytes_written,
+                                  s->nbytes_allowed_to_write);
+#ifdef DEBUG_QOS
+        if (!allow)
+            CHANNEL_DEBUG(channel, "[qos] write denied");
+#endif
+        return allow;
+    }
+
+    return true;
 }
 
 G_GNUC_INTERNAL
@@ -2991,7 +3197,18 @@ gboolean spice_session_qos_can_channel_read(SpiceSession *session,
     qos = g_hash_table_lookup(s->qos_table, channel);
     g_return_val_if_fail(qos != NULL, FALSE);
 
-    return !qos->throttling_read_enabled;
+    if (qos->throttling_read_enabled) {
+        bool allow = qos_allow_io(s->time_throttling_read_started,
+                                  s->nbytes_read,
+                                  s->nbytes_allowed_to_read);
+#ifdef DEBUG_QOS
+        if (!allow)
+            CHANNEL_DEBUG(channel, "[qos] read denied");
+#endif
+        return allow;
+    }
+
+    return true;
 }
 
 G_GNUC_INTERNAL
@@ -3000,6 +3217,7 @@ void spice_session_qos_channel_has_read_nbytes(SpiceSession *session,
                                                gssize nbytes)
 {
     SpiceSessionPrivate *s;
+    gint type;
 
     g_return_if_fail(SPICE_IS_SESSION(session));
     g_return_if_fail(SPICE_IS_CHANNEL(channel));
@@ -3008,8 +3226,20 @@ void spice_session_qos_channel_has_read_nbytes(SpiceSession *session,
     if (!s->qos_enabled)
         return;
 
-    /* TODO */
-    return;
+    if (s->nbytes_allowed_to_read == 0)
+        return;
+
+    type = spice_channel_get_channel_type(channel);
+    if (qos_channel_priority[type].read != LOW)
+        return;
+
+    s->nbytes_read += nbytes;
+#if DEBUG_QOS
+    CHANNEL_DEBUG(channel, "[qos] (read) %.0f of %.0f [%.2f%%] (allowed: %.2f%%)",
+                  s->nbytes_read, s->nbytes_allowed_to_read,
+                  (s->nbytes_read * 100 / s->nbytes_allowed_to_read),
+                  (s->nbytes_allowed_to_read * 100 / (s->maximum_read_rate * 1024.0 * 1024.0 * QOS_BANDWITH_TIMEOUT)));
+#endif
 }
 
 G_GNUC_INTERNAL
@@ -3018,6 +3248,7 @@ void spice_session_qos_channel_has_write_nbytes(SpiceSession *session,
                                                 gssize nbytes)
 {
     SpiceSessionPrivate *s;
+    gint type;
 
     g_return_if_fail(SPICE_IS_SESSION(session));
     g_return_if_fail(SPICE_IS_CHANNEL(channel));
@@ -3026,6 +3257,18 @@ void spice_session_qos_channel_has_write_nbytes(SpiceSession *session,
     if (!s->qos_enabled)
         return;
 
-    /* TODO */
-    return;
+    if (s->nbytes_allowed_to_write == 0)
+        return;
+
+    type = spice_channel_get_channel_type(channel);
+    if (qos_channel_priority[type].write != LOW)
+        return;
+
+    s->nbytes_written += nbytes;
+#if DEBUG_QOS
+    CHANNEL_DEBUG(channel, "[qos] (write) %.0f of %.0f [%.2f%%] (allowed: %.2f%%)",
+                  s->nbytes_written, s->nbytes_allowed_to_write,
+                  (s->nbytes_written * 100 / s->nbytes_allowed_to_write),
+                  (s->nbytes_allowed_to_write * 100 / (s->maximum_write_rate * 1024.0 * 1024.0 * QOS_BANDWITH_TIMEOUT)));
+#endif
 }
-- 
2.9.3



More information about the Spice-devel mailing list