Skip to content

Commit

Permalink
#417: bandwidth-limit option
Browse files Browse the repository at this point in the history
* adjust batch delay to slow things down when we're using up the bandwidth budget too quickly
* delay compression of the next screen update when we have used up the budget
* expose "bandwidth-limit" via xpra info
* command line option, parsing, man page, etc..

git-svn-id: https://xpra.org/svn/Xpra/trunk@17232 3bb7dfac-3a0b-4e04-842a-767bc560f471
  • Loading branch information
totaam committed Oct 23, 2017
1 parent 04a8c74 commit 4f64377
Show file tree
Hide file tree
Showing 12 changed files with 163 additions and 25 deletions.
3 changes: 2 additions & 1 deletion src/NEWS
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
v2.2 (2017-09-30)
v2.2 (2017-10-23)
======================
-- support RFB clients (ie: VNC) with bind-rfb or rfb-upgrade options
-- UDP transport (experimental) with bind-udp and udp://host:port URLs
-- TCP sockets can be upgrade to Websockets and / or SSL, RFB
-- multiple bind options for all socket types supported: tcp, ssl, ws, wss, udp, rfb
-- bandwidth-limit option
-- "xpra sessions" browser tool for both mDNS and local sessions
-- support arbitrary resolutions with Xvfb (not with Xdummy yet)
-- new OpenGL backends, with support for GTK3 on most platforms
Expand Down
11 changes: 11 additions & 0 deletions src/etc/xpra/conf.d/10_network.conf.in
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@ idle-timeout = 0
# Server idle timeout in seconds:
#server-idle-timeout = 600
server-idle-timeout = 0

# Bandwidth limit:
#no limit:
#bandwidth-limit = 0
#1Mbps:
#bandwidth-limit = 1000000
#bandwidth-limit = 1000Kbps
#bandwidth-limit = 1M
#10Mbps:
#bandwidth-limit = 10Mbps
bandwidth-limit = 0
9 changes: 9 additions & 0 deletions src/man/xpra.1
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ xpra \- viewer for remote, persistent X applications
[\fB\-\-microphone\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-microphone\-codec\fP=\fICODEC\fP]
[\fB\-\-sharing\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-bandwidth\-limit\fP=\fIBITSPERSECOND\fP]
[\fB\-\-bind\fP=\fIBIND_LOCATION\fP]
[\fB\-\-bind\-tcp\fP=\fI[HOST]:PORT\fP]
[\fB\-\-bind\-udp\fP=\fI[HOST]:PORT\fP]
Expand Down Expand Up @@ -175,6 +176,7 @@ xpra \- viewer for remote, persistent X applications
[\fB\-\-mouse\-polling\fP=\fIVALUE\fP]
[\fB\-\-socket\-dir\fP=\fIDIR\fP]
[\fB\-\-socket\-dirs\fP=\fIDIRS\fP]
[\fB\-\-bandwidth\-limit\fP=\fIBITSPERSECOND\fP]
[\fB\-\-pings\fP=\fIyes\fP|\fIno\fP]
[\fB\-\-encryption\fP=\fICIPHER\fP]
[\fB\-\-encryption\-keyfile\fP=\fIFILENAME\fP]
Expand Down Expand Up @@ -204,6 +206,7 @@ xpra \- viewer for remote, persistent X applications
[\fB\-\-speaker\-codec\fP=\fICODEC\fP]
[\fB\-\-microphone\fP=\fIon\fP|\fIoff\fP|\fIdisabled\fP]
[\fB\-\-microphone\-codec\fP=\fICODEC\fP]
[\fB\-\-bandwidth\-limit\fP=\fIBITSPERSECOND\fP]
[\fB\-\-bind\fP=\fISOCKET|DIRECTORY\fP]
[\fB\-\-bind\-tcp\fP=\fI[HOST]:PORT\fP]
[\fB\-\-bind\-udp\fP=\fI[HOST]:PORT\fP]
Expand Down Expand Up @@ -704,6 +707,12 @@ This may be a security risk if you are using xpra to constrain
what the clients can execute on the server.
.TP

\fB\-\-bandwidth\-limit\fP=\fIBITSPERSECOND\fP
Restrict bandwidth usage to below the limit given.
The client's value cannot raise the limit of the server.
The value may be specified using standard units, ie:
\fI1Mbps\fP or \fI500K\fP.
.TP

.SS Options for start, start-desktop, upgrade, proxy and shadow
.TP
Expand Down
16 changes: 11 additions & 5 deletions src/xpra/client/ui_client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,18 +178,21 @@ def __init__(self):
self.server_display = None
self.server_randr = False
self.pixel_counter = deque(maxlen=1000)
self.server_ping_latency = deque(maxlen=1000)
self.server_load = None
self.client_ping_latency = deque(maxlen=1000)
self._server_ok = True
self.last_ping_echoed_time = 0
self.server_last_info = None
self.info_request_pending = False
self.screen_size_change_pending = False
self.allowed_encodings = []
self.core_encodings = None
self.encoding = None

#network state:
self.server_ping_latency = deque(maxlen=1000)
self.server_load = None
self.client_ping_latency = deque(maxlen=1000)
self._server_ok = True
self.last_ping_echoed_time = 0
self.bandwidth_limit = 0

#webcam:
self.webcam_option = ""
self.webcam_forwarding = False
Expand Down Expand Up @@ -333,6 +336,7 @@ def init(self, opts):
self.title = opts.title
self.session_name = opts.session_name
self.auto_refresh_delay = opts.auto_refresh_delay
self.bandwidth_limit = opts.bandwidth_limit
if opts.max_size:
try:
self.max_window_size = [int(x.strip()) for x in opts.max_size.split("x", 1)]
Expand Down Expand Up @@ -1568,6 +1572,8 @@ def make_hello(self):
"sound.ogg-latency-fix" : True,
"av-sync" : self.av_sync,
"av-sync.delay.default" : 0, #start at 0 and rely on sound-control packets to set the correct value
#network:
"bandwidth-limit" : self.bandwidth_limit,
})
updict(capabilities, "window", {
"raise" : True,
Expand Down
6 changes: 4 additions & 2 deletions src/xpra/scripts/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ def may_create_user_config(xpra_conf_filename=DEFAULT_XPRA_CONF_FILENAME):
"gid" : int,
"min-port" : int,
"rfb-upgrade" : int,
"bandwidth-limit" : int,
#float options:
"auto-refresh-delay": float,
#boolean options:
Expand Down Expand Up @@ -625,7 +626,7 @@ def may_create_user_config(xpra_conf_filename=DEFAULT_XPRA_CONF_FILENAME):

#keep track of the options added since v1,
#so we can generate command lines that work with older supported versions:
OPTIONS_ADDED_SINCE_V1 = ["attach", "open-files", "pixel-depth", "uid", "gid", "chdir", "min-port", "rfb-upgrade"]
OPTIONS_ADDED_SINCE_V1 = ["attach", "open-files", "pixel-depth", "uid", "gid", "chdir", "min-port", "rfb-upgrade", "bandwidth-limit"]

CLIENT_OPTIONS = ["title", "username", "password", "session-name",
"dock-icon", "tray-icon", "window-icon",
Expand Down Expand Up @@ -687,7 +688,7 @@ def may_create_user_config(xpra_conf_filename=DEFAULT_XPRA_CONF_FILENAME):
"mmap", "mmap-group", "mdns",
"auth", "vsock-auth", "tcp-auth", "udp-auth", "ws-auth", "wss-auth", "ssl-auth", "rfb-auth",
"bind", "bind-vsock", "bind-tcp", "bind-udp", "bind-ssl", "bind-ws", "bind-wss", "bind-rfb",
"rfb-upgrade",
"rfb-upgrade", "bandwidth-limit",
"start", "start-child",
"start-after-connect", "start-child-after-connect",
"start-on-connect", "start-child-on-connect",
Expand Down Expand Up @@ -894,6 +895,7 @@ def addtrailingslash(v):
"gid" : getgid(),
"min-port" : 1024,
"rfb-upgrade" : 5,
"bandwidth-limit" : 0,
"auto-refresh-delay": 0.15,
"daemon" : CAN_DAEMONIZE,
"start-via-proxy" : None,
Expand Down
45 changes: 38 additions & 7 deletions src/xpra/scripts/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,12 @@ def err(*args):


def do_replace_option(cmdline, oldoption, newoption):
if oldoption in cmdline:
cmdline.remove(oldoption)
cmdline.append(newoption)
for i, x in enumerate(cmdline):
if x==oldoption:
cmdline[i] = newoption
elif newoption.find("=")<0 and x.startswith("%s=" % oldoption):
cmdline[i] = "%s=%s" % (newoption, x.split("=", 1)[1])

def do_legacy_bool_parse(cmdline, optionname, newoptionname=None):
#find --no-XYZ or --XYZ
#and replace it with --XYZ=yes|no
Expand Down Expand Up @@ -617,10 +620,13 @@ def ignore(defaults):
})

group = optparse.OptionGroup(parser, "Server Controlled Features",
"These options can be used to turn certain features on or off, "
"they can be specified on the client or on the server, "
"but the client cannot enable them if they are disabled on the server.")
"These options be specified on the client or on the server, "
"but the server's settings will have precedence over the client's.")
parser.add_option_group(group)
replace_option("--bwlimit", "--bandwidth-limit")
group.add_option("--bandwidth-limit", action="store",
dest="bandwidth_limit", default=defaults.bandwidth_limit,
help="Limit the bandwidth used. The value is specified in bits per second, use the value '0' to disable restrictions. Default: '%default'.")
replace_option("--readwrite", "--readonly=no")
replace_option("--readonly", "--readonly=yes")
group.add_option("--readonly", action="store", metavar="yes|no",
Expand Down Expand Up @@ -1127,10 +1133,35 @@ def ignore(defaults):
#and may have "none" or "all" special values
fixup_options(options, defaults)

#special case for bandwidth-limit, which can be specified using units:
try:
import re
v = options.bandwidth_limit
if not v:
options.bandwidth_limit = 0
else:
r = re.match('([0-9]*)(.*)', options.bandwidth_limit)
assert r
i = int(r.group(1))
unit = r.group(2).lower()
if unit.endswith("bps"):
unit = unit[:-3]
if unit=="b":
pass
elif unit=="k":
i *= 1000
elif unit=="m":
i *= 1000000
elif unit=="g":
i *= 1000000000
assert i>=250000, "value is too low"
options.bandwidth_limit = i
except Exception as e:
raise InitException("invalid bandwidth limit value '%s': %s" % (options.bandwidth_limit, e))
try:
options.dpi = int(options.dpi)
except Exception as e:
raise InitException("invalid dpi: %s" % e)
raise InitException("invalid dpi value '%s': %s" % (options.dpi, e))
if options.max_size:
try:
#split on "," or "x":
Expand Down
1 change: 1 addition & 0 deletions src/xpra/server/server_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,7 @@ def get_window_id(wid):
self.window_filters,
self.file_transfer,
self.supports_mmap, self.mmap_filename,
self.bandwidth_limit,
self.av_sync,
self.core_encodings, self.encodings, self.default_encoding, self.scaling_control,
self.sound_properties,
Expand Down
3 changes: 3 additions & 0 deletions src/xpra/server/server_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ def __init__(self):
self.exit_with_client = False
self.server_idle_timeout = 0
self.server_idle_timer = None
self.bandwidth_limit = 0

self.init_uuid()
self.init_control_commands()
Expand All @@ -208,6 +209,7 @@ def init(self, opts):
self.session_name = opts.session_name
set_name("Xpra", self.session_name or "Xpra")

self.bandwidth_limit = opts.bandwidth_limit
self.unix_socket_paths = []
self._socket_dir = opts.socket_dir or opts.socket_dirs[0]
self.encryption = opts.encryption
Expand Down Expand Up @@ -1553,6 +1555,7 @@ def up(prefix, d):
"sockets" : self.get_socket_info(),
"encryption" : self.encryption or "",
"tcp-encryption" : self.tcp_encryption or "",
"bandwidth-limit": self.bandwidth_limit,
})
up("network", ni)
up("threads", self.get_thread_info(proto))
Expand Down
44 changes: 42 additions & 2 deletions src/xpra/server/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ def __init__(self, protocol, disconnect_cb, idle_add, timeout_add, source_remove
window_filters,
file_transfer,
supports_mmap, mmap_filename,
bandwidth_limit,
av_sync,
core_encodings, encodings, default_encoding, scaling_control,
sound_properties,
Expand All @@ -249,6 +250,7 @@ def __init__(self, protocol, disconnect_cb, idle_add, timeout_add, source_remove
window_filters,
file_transfer,
supports_mmap, mmap_filename,
bandwidth_limit,
av_sync,
core_encodings, encodings, default_encoding, scaling_control,
sound_properties,
Expand Down Expand Up @@ -293,6 +295,8 @@ def __init__(self, protocol, disconnect_cb, idle_add, timeout_add, source_remove
self.mmap_client_token = None #the token we write that the client may check
self.mmap_client_token_index = 512
self.mmap_client_token_bytes = 0
# network constraints:
self.server_bandwidth_limit = bandwidth_limit
# mouse echo:
self.mouse_show = False
self.mouse_last_position = None
Expand Down Expand Up @@ -433,6 +437,7 @@ def init_vars(self):
self.vrefresh = -1
self.double_click_time = -1
self.double_click_distance = -1, -1
self.bandwidth_limit = self.server_bandwidth_limit
#what we send back in hello packet:
self.ui_client = True
self.wants_aliases = True
Expand Down Expand Up @@ -490,20 +495,43 @@ def close(self):
self.idle_add(ds.cleanup)


def update_bandwidth_limits(self):
if self.bandwidth_limit<=0:
return
#figure out how to distribute the bandwidth amongst the windows,
#we use the window size,
#(we should actually use the number of bytes actually sent: framerate, compression, etc..)
window_weight = {}
for wid, ws in self.window_sources.items():
weight = 0
if not ws.suspended:
ww, wh = ws.window_dimensions
#try to reserve bandwidth for at least one screen update,
#and add the number of pixels damaged:
weight = ww*wh + ws.statistics.get_damage_pixels()
window_weight[wid] = weight
log("update_bandwidth_limits() window weights=%s", window_weight)
total_weight = sum(window_weight.values())
for wid, ws in self.window_sources.items():
weight = window_weight.get(wid)
if weight is not None:
ws.bandwidth_limit = max(1, self.bandwidth_limit*weight/total_weight)

def recalculate_delays(self):
""" calls update_averages() on ServerSource.statistics (GlobalStatistics)
and WindowSource.statistics (WindowPerformanceStatistics) for each window id in calculate_window_ids,
this runs in the worker thread.
"""
log("recalculate_delays()")
if self.is_closed():
return
self.update_bandwidth_limits()
self.statistics.update_averages()
wids = list(self.calculate_window_ids) #make a copy so we don't clobber new wids
focus = self.get_focus()
sources = self.window_sources.items()
maximized_wids = [wid for wid, source in sources if source is not None and source.maximized]
fullscreen_wids = [wid for wid, source in sources if source is not None and source.fullscreen]
log("recalculate_delays() wids=%s, focus=%s, maximized=%s, fullscreen=%s", wids, focus, maximized_wids, fullscreen_wids)
for wid in wids:
#this is safe because we only add to this set from other threads:
self.calculate_window_ids.remove(wid)
Expand Down Expand Up @@ -745,6 +773,12 @@ def parse_batch_int(value, varname):
self.double_click_time = c.intget("double_click.time")
self.double_click_distance = c.intpair("double_click.distance")
self.window_frame_sizes = typedict(c.dictget("window.frame_sizes") or {})
bandwidth_limit = c.intget("bandwidth-limit", 0)
if self.server_bandwidth_limit<=0:
self.bandwidth_limit = bandwidth_limit
else:
self.bandwidth_limit = min(self.server_bandwidth_limit, bandwidth_limit)
netlog("server bandwidth-limit=%s, client bandwidth-limit=%s, value=%s", self.server_bandwidth_limit, bandwidth_limit, self.bandwidth_limit)

self.desktop_size = c.intpair("desktop_size")
if self.desktop_size is not None:
Expand Down Expand Up @@ -1559,6 +1593,7 @@ def get_info(self):
"suspended" : self.suspended,
"counter" : self.counter,
"hello-sent" : self.hello_sent,
"bandwidth-limit" : self.bandwidth_limit,
}
if self.desktop_mode_size:
info["desktop_mode_size"] = self.desktop_mode_size
Expand Down Expand Up @@ -2273,8 +2308,10 @@ def make_window_source(self, wid, window):
ws = self.window_sources.get(wid)
if ws is None:
batch_config = self.make_batch_config(wid, window)
ww, wh = window.get_dimensions()
ws = WindowVideoSource(
self.idle_add, self.timeout_add, self.source_remove,
ww, wh,
self.queue_size, self.call_in_encode_thread, self.queue_packet, self.compressed_wrapper,
self.statistics,
wid, window, batch_config, self.auto_refresh_delay,
Expand All @@ -2284,8 +2321,11 @@ def make_window_source(self, wid, window):
self.encoding, self.encodings, self.core_encodings, self.window_icon_encodings, self.encoding_options, self.icons_encoding_options,
self.rgb_formats,
self.default_encoding_options,
self.mmap, self.mmap_size)
self.mmap, self.mmap_size, self.bandwidth_limit)
self.window_sources[wid] = ws
if len(self.window_sources)>1:
#re-distribute bandwidth:
self.update_bandwidth_limits()
return ws

def damage(self, wid, window, x, y, w, h, options=None):
Expand Down
4 changes: 2 additions & 2 deletions src/xpra/server/window/batch_delay_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def get_low_limit(mmap_enabled, window_dimensions):
return low_limit


def calculate_batch_delay(wid, window_dimensions, has_focus, other_is_fullscreen, other_is_maximized, is_OR, soft_expired, batch, global_statistics, statistics):
def calculate_batch_delay(wid, window_dimensions, has_focus, other_is_fullscreen, other_is_maximized, is_OR, soft_expired, batch, global_statistics, statistics, bandwidth_limit):
"""
Calculates a new batch delay.
We first gather some statistics,
Expand All @@ -36,7 +36,7 @@ def calculate_batch_delay(wid, window_dimensions, has_focus, other_is_fullscreen
low_limit = get_low_limit(global_statistics.mmap_size>0, window_dimensions)

#for each indicator: (description, factor, weight)
factors = statistics.get_factors()
factors = statistics.get_factors(bandwidth_limit)
statistics.target_latency = statistics.get_target_client_latency(global_statistics.min_client_latency, global_statistics.avg_client_latency)
factors += global_statistics.get_factors(low_limit)
#damage pixels waiting in the packet queue: (extract data for our window id only)
Expand Down
Loading

0 comments on commit 4f64377

Please sign in to comment.