Skip to content

Commit

Permalink
#849:
Browse files Browse the repository at this point in the history
* show sound queue buffer levels in session info graphs
* dynamically tune the queue min and max values to try to keep the level within a reasonable range

git-svn-id: https://xpra.org/svn/Xpra/trunk@10227 3bb7dfac-3a0b-4e04-842a-767bc560f471
  • Loading branch information
totaam committed Aug 6, 2015
1 parent 51124ff commit a46d597
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 60 deletions.
74 changes: 51 additions & 23 deletions src/xpra/client/gtk_base/session_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import datetime

from xpra.os_util import os_info, bytestostr
from xpra.util import prettify_plug_name
from xpra.util import prettify_plug_name, typedict
from xpra.gtk_common.graph import make_graph_pixmap
from collections import deque
from xpra.simple_stats import values_to_scaled_values, values_to_diff_scaled_values, to_std_unit, std_unit_dec
Expand Down Expand Up @@ -373,13 +373,17 @@ def maths_labels():
if SHOW_PIXEL_STATS:
bandwidth_label += ",\nand number of pixels rendered"
self.bandwidth_graph = self.add_graph_button(bandwidth_label, self.save_graphs)
self.connect("realize", self.populate_graphs)
self.latency_graph = self.add_graph_button(None, self.save_graphs)
self.sound_queue_graph = self.add_graph_button(None, self.save_graphs)
self.connect("realize", self.populate_graphs)
self.pixel_in_data = deque(maxlen=N_SAMPLES+4)
self.net_in_bytecount = deque(maxlen=N_SAMPLES+4)
self.net_out_bytecount = deque(maxlen=N_SAMPLES+4)
self.sound_in_bytecount = deque(maxlen=N_SAMPLES+4)
self.sound_out_bytecount = deque(maxlen=N_SAMPLES+4)
self.sound_out_queue_min = deque(maxlen=N_SAMPLES+4)
self.sound_out_queue_max = deque(maxlen=N_SAMPLES+4)
self.sound_out_queue_cur = deque(maxlen=N_SAMPLES+4)

self.set_border_width(15)
self.add(self.tab_box)
Expand Down Expand Up @@ -507,6 +511,14 @@ def populate(self, *args):
self.sound_in_bytecount.append(self.client.sound_in_bytecount)
if self.client.sound_out_bytecount>0:
self.sound_out_bytecount.append(self.client.sound_out_bytecount)
ss = self.client.sound_sink
if ss:
info = ss.get_info()
if info:
info = typedict(info)
self.sound_out_queue_cur.append(info.intget("queue.cur"))
self.sound_out_queue_min.append(info.intget("queue.min"))
self.sound_out_queue_max.append(info.intget("queue.max"))

#count pixels in the last second:
since = time.time()-1
Expand Down Expand Up @@ -912,8 +924,9 @@ def populate_graphs(self, *args):
return True
start_x_offset = min(1.0, (time.time()-self.last_populate_time)*0.95)
rect = box.get_allocation()
h = max(200, h-bh-20, rect.height-bh-20)
w = max(360, rect.width-20)
maxw, maxh = self.client.get_root_size()
h = min(maxh, max(200, h-bh-20, rect.height-bh-20))
w = min(maxw, max(360, rect.width-20))
#bandwidth graph:
labels, datasets = [], []
if self.net_in_bytecount and self.net_out_bytecount:
Expand Down Expand Up @@ -944,38 +957,53 @@ def unit(scale):

if labels and datasets:
pixmap = make_graph_pixmap(datasets, labels=labels,
width=w, height=h/2,
width=w, height=h//3,
title="Bandwidth", min_y_scale=10, rounding=10,
start_x_offset=start_x_offset)
self.bandwidth_graph.set_size_request(*pixmap.get_size())
self.bandwidth_graph.set_from_pixmap(pixmap, None)
if self.client.server_info_request:
pass

def norm_lists(items):
#ensures we always have exactly 20 values,
#(and skip if we don't have any)
values, labels = [], []
for l, name in items:
if len(l)==0:
continue
l = list(l)
if len(l)<20:
for _ in range(20-len(l)):
l.insert(0, None)
values.append(l)
labels.append(name)
return values, labels

#latency graph:
latency_graph_items = (
latency_values, latency_labels = norm_lists((
(self.avg_ping_latency, "network"),
(self.avg_batch_delay, "batch delay"),
(self.avg_damage_out_latency, "encode&send"),
(self.avg_decoding_latency, "decoding"),
(self.avg_total, "frame total"),
)
latency_graph_values = []
labels = []
for l, name in latency_graph_items:
if len(l)==0:
continue
l = list(l)
if len(l)<20:
for _ in range(20-len(l)):
l.insert(0, None)
latency_graph_values.append(l)
labels.append(name)
pixmap = make_graph_pixmap(latency_graph_values, labels=labels,
width=w, height=h/2,
))
pixmap = make_graph_pixmap(latency_values, labels=latency_labels,
width=w, height=h//3,
title="Latency (ms)", min_y_scale=10, rounding=25,
start_x_offset=start_x_offset)
self.latency_graph.set_size_request(*pixmap.get_size())
self.latency_graph.set_from_pixmap(pixmap, None)
#sound queue graph:
queue_values, queue_labels = norm_lists((
(self.sound_out_queue_max, "Max"),
(self.sound_out_queue_cur, "Level"),
(self.sound_out_queue_min, "Min"),
))
pixmap = make_graph_pixmap(queue_values, labels=queue_labels,
width=w, height=h//3,
title="Sound Buffer (ms)", min_y_scale=10, rounding=25,
start_x_offset=start_x_offset)
self.sound_queue_graph.set_size_request(*pixmap.get_size())
self.sound_queue_graph.set_from_pixmap(pixmap, None)
return True

def save_graphs(self, *args):
Expand All @@ -996,7 +1024,7 @@ def save_graphs(self, *args):
if response == gtk.RESPONSE_OK:
if len(filenames)==1:
filename = filenames[0]
pixmaps = [image.get_pixmap()[0] for image in [self.bandwidth_graph, self.latency_graph]]
pixmaps = [image.get_pixmap()[0] for image in [self.bandwidth_graph, self.latency_graph, self.sound_queue_graph]]
log("saving pixmaps %s and %s to %s", pixmaps, filename)
w, h = 0, 0
for pixmap in pixmaps:
Expand Down
103 changes: 66 additions & 37 deletions src/xpra/sound/sink.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
# later version. See the file COPYING for details.

import sys, os, time
import sys, os

from xpra.sound.sound_pipeline import SoundPipeline, gobject, one_arg_signal
from xpra.sound.sound_pipeline import SoundPipeline, gobject, glib, one_arg_signal
from xpra.sound.pulseaudio_util import has_pa
from xpra.sound.gstreamer_util import plugin_str, get_decoder_parser, get_queue_time, normv, MP3, CODECS, CODEC_ORDER, gst, QUEUE_LEAK, MS_TO_NS

Expand Down Expand Up @@ -46,11 +46,6 @@
DEFAULT_SINK = SINKS[0]
QUEUE_SILENT = 0
QUEUE_TIME = get_queue_time(450)
QUEUE_MIN_TIME = get_queue_time(QUEUE_TIME//10//MS_TO_NS, "MIN")
assert QUEUE_MIN_TIME<=QUEUE_TIME

VARIABLE_MIN_QUEUE = os.environ.get("XPRA_VARIABLE_MIN_QUEUE", "1")=="1"


GST_FORMAT_BUFFERS = 4

Expand Down Expand Up @@ -94,7 +89,7 @@ def __init__(self, sink_type=None, sink_options={}, codecs=CODECS, codec_options
pipeline_els.append("volume name=volume volume=%s" % volume)
queue_el = ["queue",
"name=queue",
"min-threshold-time=%s" % QUEUE_MIN_TIME,
"min-threshold-time=0",
"max-size-buffers=0",
"max-size-bytes=0",
"max-size-time=%s" % QUEUE_TIME,
Expand Down Expand Up @@ -131,51 +126,81 @@ def cleanup(self):
def queue_pushing(self, *args):
ltime = self.queue.get_property("current-level-time")/MS_TO_NS
log("queue pushing: level=%i", ltime)
self.queue_state = "pushing"
self.emit_info()
self.check_levels("pushing")
return 0

def queue_running(self, *args):
ltime = self.queue.get_property("current-level-time")/MS_TO_NS
log("queue running: level=%s", ltime)
if self.queue_state=="underrun" and VARIABLE_MIN_QUEUE:
self.check_levels("running")
return 0

def check_levels(self, new_state):
if self.queue_state=="underrun":
#lift min time restrictions:
self.queue.set_property("min-threshold-time", 0)
elif self.queue_state == "overrun":
clt = self.queue.get_property("current-level-time")
qpct = min(QUEUE_TIME, clt)*100//QUEUE_TIME
log("resetting max-size-time back to %ims (level=%ims, %i%%)", QUEUE_TIME//MS_TO_NS, clt//MS_TO_NS, qpct)
self.queue.set_property("max-size-time", QUEUE_TIME)
self.queue_state = "running"
self.queue_state = new_state
self.emit_info()
return 0


def queue_underrun(self, *args):
if self.queue_state=="underrun" or self.queue_state=="starting":
return
ltime = self.queue.get_property("current-level-time")/MS_TO_NS
log("queue underrun: level=%i", ltime)
if self.queue_state!="underrun" and VARIABLE_MIN_QUEUE:
#lift min time restrictions:
self.queue.set_property("min-threshold-time", QUEUE_MIN_TIME)
log("queue underrun: level=%i, previous state=%s", ltime, self.queue_state)
self.queue_state = "underrun"
self.emit_info()
MIN_QUEUE = QUEUE_TIME//2
mts = self.queue.get_property("min-threshold-time")
if mts==MIN_QUEUE:
return 0
#set min time restrictions to fill up queue:
log("increasing the min-threshold-time to %ims", MIN_QUEUE//MS_TO_NS)
self.queue.set_property("min-threshold-time", MIN_QUEUE)
def restore():
if self.queue.get_property("min-threshold-time")==0:
log("min-threshold-time already restored!")
return False
ltime = self.queue.get_property("current-level-time")//MS_TO_NS
if ltime==0:
log("not restored! (still underrun: %ims)", ltime)
return True
log("resetting the min-threshold-time back to %ims", 0)
self.queue.set_property("min-threshold-time", 0)
self.queue_state = "running"
return False
glib.timeout_add(1000, restore)
return 0

def queue_overrun(self, *args):
if self.queue_state=="overrun":
return
ltime = self.queue.get_property("current-level-time")//MS_TO_NS
pqs = self.queue_state
log("queue overrun: level=%i, previous state=%s", ltime, self.queue_state)
self.queue_state = "overrun"
#no overruns for the first 2 seconds:
if ltime<QUEUE_TIME//MS_TO_NS//2*75//100:
elapsed = time.time()-self.start_time
log("queue overrun ignored: level=%i, elapsed time=%.1f", ltime, elapsed)
return 0
log("queue overrun: level=%i, previous state=%s", ltime//MS_TO_NS, pqs)
if pqs!="overrun":
log("halving the max-size-time to %ims", QUEUE_TIME//2//MS_TO_NS)
self.queue.set_property("max-size-time", QUEUE_TIME//2)
self.overruns += 1
self.emit("overrun", ltime)
self.emit_info()
REDUCED_QT = QUEUE_TIME//2
#empty the queue by reducing its max size:
log("queue overrun: halving the max-size-time to %ims", REDUCED_QT//MS_TO_NS)
self.queue.set_property("max-size-time", REDUCED_QT)
self.overruns += 1
self.emit("overrun", ltime)
def restore():
if self.queue.get_property("max-size-time")==QUEUE_TIME:
log("max-size-time already restored!")
return False
ltime = self.queue.get_property("current-level-time")//MS_TO_NS
if ltime>=REDUCED_QT:
log("max-size-time not restored! (still overrun: %ims)", ltime)
return True
log("raising the max-size-time back to %ims", QUEUE_TIME//MS_TO_NS)
self.queue.set_property("max-size-time", QUEUE_TIME)
self.queue_state = "running"
return False
glib.timeout_add(1000, restore)
return 0

def eos(self):
Expand All @@ -189,11 +214,13 @@ def get_info(self):
info = SoundPipeline.get_info(self)
if QUEUE_TIME>0:
clt = self.queue.get_property("current-level-time")
qmax = self.queue.get_property("max-size-time")
qmin = self.queue.get_property("min-threshold-time")
updict(info, "queue", {
"time" : QUEUE_TIME//MS_TO_NS,
"min_time" : QUEUE_MIN_TIME//MS_TO_NS,
"used_pct" : min(QUEUE_TIME, clt)*100//QUEUE_TIME,
"used" : clt//MS_TO_NS,
"min" : qmin//MS_TO_NS,
"max" : qmax//MS_TO_NS,
"cur" : clt//MS_TO_NS,
"pct" : min(QUEUE_TIME, clt)*100//qmax,
"overruns" : self.overruns,
"state" : self.queue_state})
return info
Expand All @@ -202,6 +229,9 @@ def add_data(self, data, metadata=None):
if not self.src:
log("add_data(..) dropped")
return
if self.queue_state=="overrun":
log("add_data(..) queue in overrun state, data dropped")
return
#having a timestamp causes problems with the queue and overruns:
if "timestamp" in metadata:
del metadata["timestamp"]
Expand Down Expand Up @@ -239,7 +269,6 @@ def push_buffer(self, buf):


def main():
import glib
from xpra.platform import init, clean
init("Sound-Record")
try:
Expand Down Expand Up @@ -302,7 +331,7 @@ def check_for_end(*args):
glib.timeout_add(500, glib_mainloop.quit)
return False
return True
gobject.timeout_add(1000, check_for_end)
glib.timeout_add(1000, check_for_end)

glib_mainloop.run()
return 0
Expand Down

0 comments on commit a46d597

Please sign in to comment.