1# -*- coding: utf-8 -*- 2# This file is part of Xpra. 3# Copyright (C) 2011-2021 Antoine Martin <antoine@xpra.org> 4# Copyright (C) 2010 Nathaniel Smith <njs@pobox.com> 5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 6# later version. See the file COPYING for details. 7 8import sys 9import datetime 10import platform 11from time import monotonic 12from collections import deque 13from gi.repository import GLib, Gtk, Gdk 14 15from xpra.version_util import XPRA_VERSION 16from xpra.os_util import bytestostr, strtobytes, get_linux_distribution 17from xpra.util import prettify_plug_name, typedict, envint, csv 18from xpra.gtk_common.graph import make_graph_imagesurface 19from xpra.simple_stats import values_to_scaled_values, values_to_diff_scaled_values, to_std_unit, std_unit_dec, std_unit 20from xpra.client import mixin_features 21from xpra.client.gobject_client_base import InfoTimerClient 22from xpra.gtk_common.gtk_util import ( 23 add_close_accel, label, 24 TableBuilder, imagebutton, get_gtk_version_info, 25 get_icon_pixbuf, 26 ) 27from xpra.net.net_util import get_network_caps 28from xpra.log import Logger 29 30log = Logger("info") 31 32N_SAMPLES = 20 #how many sample points to show on the graphs 33SHOW_PIXEL_STATS = True 34SHOW_SOUND_STATS = True 35SHOW_RECV = True 36 37 38def make_version_str(version): 39 if version and isinstance(version, (tuple, list)): 40 version = ".".join(bytestostr(x) for x in version) 41 return bytestostr(version or "unknown") 42 43def make_datetime(date, time): 44 if not time: 45 return bytestostr(date) 46 return "%s %s" % (bytestostr(date), bytestostr(time)) 47 48def title_box(label_str): 49 eb = Gtk.EventBox() 50 l = label(label_str) 51 l.modify_fg(Gtk.StateType.NORMAL, Gdk.Color(red=48*256, green=0, blue=0)) 52 al = Gtk.Alignment(xalign=0.0, yalign=0.5, xscale=0.0, yscale=0.0) 53 al.set_padding(0, 0, 10, 10) 54 al.add(l) 55 eb.add(al) 56 eb.modify_bg(Gtk.StateType.NORMAL, Gdk.Color(red=219*256, green=226*256, blue=242*256)) 57 return eb 58 59def pixelstr(v): 60 if v<0: 61 return "n/a" 62 return std_unit_dec(v) 63def fpsstr(v): 64 if v<0: 65 return "n/a" 66 return "%s" % (int(v*10)/10.0) 67 68def average(seconds, pixel_counter): 69 now = monotonic() 70 total = 0 71 total_n = 0 72 mins = None 73 maxs = 0 74 avgs = 0 75 mint = now-seconds #ignore records older than N seconds 76 startt = now #when we actually start counting from 77 for _, t, count in pixel_counter: 78 if t>=mint: 79 total += count 80 total_n += 1 81 startt = min(t, startt) 82 if mins: 83 mins = min(mins,count) 84 else: 85 mins = count 86 maxs = max(maxs, count) 87 avgs += count 88 if total==0 or startt==now: 89 return None 90 avgs = avgs/total_n 91 elapsed = now-startt 92 return int(total/elapsed), total_n/elapsed, mins, avgs, maxs 93 94def dictlook(d, k, fallback=None): 95 #deal with old-style non-namespaced dicts first: 96 #"batch.delay.avg" 97 if d is None: 98 return fallback 99 v = d.get(k) 100 if v is not None: 101 return v 102 parts = (b"client."+strtobytes(k)).split(b".") 103 v = newdictlook(d, parts, fallback) 104 if v is None: 105 parts = strtobytes(k).split(b".") 106 v = newdictlook(d, parts, fallback) 107 if v is None: 108 return fallback 109 return v 110 111def newdictlook(d, parts, fallback=None): 112 #ie: {}, ["batch", "delay", "avg"], 0 113 v = d 114 for p in parts: 115 try: 116 newv = v.get(p) 117 if newv is None: 118 newv = v.get(strtobytes(p)) 119 if newv is None: 120 return fallback 121 v = newv 122 except Exception: 123 return fallback 124 return v 125 126def slabel(text="", tooltip=None, font=None): 127 l = label(text, tooltip, font) 128 l.set_selectable(True) 129 return l 130 131 132class SessionInfo(Gtk.Window): 133 134 def __init__(self, client, session_name, conn, show_client=True, show_server=True): 135 assert show_client or show_server 136 self.show_client = show_client 137 self.show_server = show_server 138 super().__init__() 139 self.client = client 140 self.session_name = session_name 141 self.connection = conn 142 self.last_populate_time = 0 143 self.last_populate_statistics = 0 144 self.is_closed = False 145 self.set_title(self.get_window_title()) 146 self.set_destroy_with_parent(True) 147 self.set_resizable(True) 148 self.set_decorated(True) 149 window_icon_pixbuf = get_icon_pixbuf("statistics.png") or get_icon_pixbuf("xpra.png") 150 if window_icon_pixbuf: 151 self.set_icon(window_icon_pixbuf) 152 self.set_position(Gtk.WindowPosition.CENTER) 153 154 #tables on the left in a vbox with buttons at the top: 155 self.tab_box = Gtk.VBox(False, 0) 156 self.tab_button_box = Gtk.HBox(True, 0) 157 self.tabs = [] #pairs of button, table 158 self.populate_cb = None 159 self.tab_box.pack_start(self.tab_button_box, expand=False, fill=True, padding=0) 160 161 fillexpand = Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND 162 163 #Package Table: 164 tb = self.table_tab("package.png", "Software", self.populate_software)[0] 165 if show_client and show_server: 166 #title row: 167 tb.attach(title_box(""), 0, xoptions=fillexpand, xpadding=0) 168 tb.attach(title_box("Client"), 1, xoptions=fillexpand, xpadding=0) 169 tb.attach(title_box("Server"), 2, xoptions=fillexpand, xpadding=0) 170 tb.inc() 171 172 def clrow(label_str, client_label, server_label, **kwargs): 173 args = [label_str] 174 if show_client: 175 args.append(client_label) 176 if show_server: 177 args.append(server_label) 178 tb.new_row(*args, **kwargs) 179 180 def csrow(label_str, client_str, server_str, **kwargs): 181 clrow(label_str, 182 slabel(client_str) if show_client else None, 183 slabel(server_str) if show_server else None, 184 **kwargs) 185 186 def make_os_str(sys_platform, platform_release, platform_platform, platform_linux_distribution): 187 from xpra.os_util import platform_name 188 s = [platform_name(sys_platform, platform_release)] 189 if platform_linux_distribution and len(platform_linux_distribution)==3 and len(platform_linux_distribution[0])>0: 190 s.append(" ".join([str(x) for x in platform_linux_distribution])) 191 elif platform_platform: 192 s.append(platform_platform) 193 return "\n".join(s) 194 distro = get_linux_distribution() 195 LOCAL_PLATFORM_NAME = make_os_str(sys.platform, platform.release(), platform.platform(), distro) 196 def cattr(name, default_value=""): 197 return getattr(self.client, name, default_value) 198 SERVER_PLATFORM_NAME = make_os_str(cattr("_remote_platform"), 199 cattr("_remote_platform_release"), 200 cattr("_remote_platform_platform"), 201 cattr("_remote_platform_linux_distribution")) 202 csrow("Operating System", LOCAL_PLATFORM_NAME, SERVER_PLATFORM_NAME) 203 csrow("Xpra", XPRA_VERSION, cattr("_remote_version", "unknown")) 204 try: 205 from xpra.build_info import BUILD_DATE as cl_date, BUILD_TIME as cl_time 206 except ImportError: 207 cl_date = cl_time = "" 208 from xpra.version_util import revision_str, make_revision_str 209 csrow("Revision", revision_str(), 210 make_revision_str( 211 cattr("_remote_revision"), 212 cattr("_remote_modifications"), 213 cattr("_remote_branch"), 214 cattr("_remote_commit"), 215 ) 216 ) 217 csrow("Build date", make_datetime(cl_date, cl_time), 218 make_datetime(cattr("_remote_build_date"), cattr("_remote_build_time"))) 219 gtk_version_info = get_gtk_version_info() 220 def client_vinfo(prop, fallback="unknown"): 221 return make_version_str(newdictlook(gtk_version_info, (prop, "version"), fallback)) 222 def server_vinfo(lib): 223 rlv = getattr(self.client, "_remote_lib_versions", {}) 224 return make_version_str(rlv.get(lib, "")) 225 csrow("Glib", client_vinfo("glib"), server_vinfo("glib")) 226 csrow("Gobject", client_vinfo("gobject"), server_vinfo("gobject")) 227 csrow("GTK", client_vinfo("gtk"), server_vinfo("gtk")) 228 csrow("GDK", client_vinfo("gdk"), server_vinfo("gdk")) 229 csrow("Cairo", client_vinfo("cairo"), server_vinfo("cairo")) 230 csrow("Pango", client_vinfo("pango"), server_vinfo("pango")) 231 csrow("Python", platform.python_version(), server_vinfo("python")) 232 233 try: 234 from xpra.sound.wrapper import query_sound 235 props = query_sound() 236 except Exception: 237 log("cannot load sound information: %s", exc_info=True) 238 props = typedict() 239 gst_version = props.strtupleget("gst.version") 240 pygst_version = props.strtupleget("pygst.version") 241 csrow("GStreamer", make_version_str(gst_version), server_vinfo("sound.gst")) 242 csrow("pygst", make_version_str(pygst_version), server_vinfo("sound.pygst")) 243 def clientgl(prop="opengl", default_value="n/a"): 244 if not show_client: 245 return "" 246 return make_version_str(self.client.opengl_props.get(prop, default_value)) 247 def servergl(prop="opengl", default_value="n/a"): 248 if not show_server: 249 return "" 250 return make_version_str(typedict(self.client.server_opengl or {}).strget(prop, default_value)) 251 for prop in ("OpenGL", "Vendor", "PyOpenGL"): 252 key = prop.lower() 253 csrow(prop, clientgl(key), servergl(key)) 254 255 # Features Table: 256 vbox = self.vbox_tab("features.png", "Features", self.populate_features) 257 #add top table: 258 tb = TableBuilder(rows=1, columns=2, row_spacings=15) 259 table = tb.get_table() 260 al = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=1.0) 261 al.add(table) 262 vbox.pack_start(al, expand=True, fill=False, padding=10) 263 #top table contents: 264 randr_box = Gtk.HBox(False, 20) 265 self.server_randr_label = slabel() 266 self.server_randr_icon = Gtk.Image() 267 randr_box.add(self.server_randr_icon) 268 randr_box.add(self.server_randr_label) 269 tb.new_row("RandR Support", randr_box) 270 if show_client: 271 self.client_display = slabel() 272 tb.new_row("Client Display", self.client_display) 273 opengl_box = Gtk.HBox(False, 20) 274 self.client_opengl_label = slabel() 275 self.client_opengl_label.set_line_wrap(True) 276 self.client_opengl_icon = Gtk.Image() 277 opengl_box.add(self.client_opengl_icon) 278 opengl_box.add(self.client_opengl_label) 279 tb.new_row("Client OpenGL", opengl_box) 280 self.opengl_buffering = slabel() 281 tb.new_row("OpenGL Mode", self.opengl_buffering) 282 self.window_rendering = slabel() 283 tb.new_row("Window Rendering", self.window_rendering) 284 if mixin_features.mmap: 285 self.server_mmap_icon = Gtk.Image() 286 tb.new_row("Memory Mapped Transfers", self.server_mmap_icon) 287 if mixin_features.clipboard: 288 self.server_clipboard_icon = Gtk.Image() 289 tb.new_row("Clipboard", self.server_clipboard_icon) 290 if mixin_features.notifications: 291 self.server_notifications_icon = Gtk.Image() 292 tb.new_row("Notifications", self.server_notifications_icon) 293 if mixin_features.windows: 294 self.server_bell_icon = Gtk.Image() 295 tb.new_row("Bell", self.server_bell_icon) 296 self.server_cursors_icon = Gtk.Image() 297 tb.new_row("Cursors", self.server_cursors_icon) 298 299 # Codecs Table: 300 vbox = self.vbox_tab("encoding.png", "Codecs", self.populate_codecs) 301 tb = TableBuilder(rows=1, columns=2, col_spacings=0, row_spacings=10) 302 table = tb.get_table() 303 al = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=1.0) 304 al.add(table) 305 vbox.pack_start(al, expand=True, fill=False, padding=10) 306 if show_client and show_server: 307 #table headings: 308 tb.attach(title_box(""), 0, xoptions=fillexpand, xpadding=0) 309 tb.attach(title_box("Client"), 1, xoptions=fillexpand, xpadding=0) 310 tb.attach(title_box("Server"), 2, xoptions=fillexpand, xpadding=0) 311 tb.inc() 312 #table contents: 313 self.client_encodings_label = slabel() 314 self.client_encodings_label.set_line_wrap(True) 315 self.server_encodings_label = slabel() 316 self.server_encodings_label.set_line_wrap(True) 317 clrow("Picture Encodings", 318 self.client_encodings_label, self.server_encodings_label, 319 xoptions=fillexpand, yoptions=fillexpand) 320 self.client_speaker_codecs_label = slabel() 321 self.client_speaker_codecs_label.set_line_wrap(True) 322 self.server_speaker_codecs_label = slabel() 323 self.server_speaker_codecs_label.set_line_wrap(True) 324 clrow("Speaker Codecs", 325 self.client_speaker_codecs_label, self.server_speaker_codecs_label, 326 xoptions=fillexpand, yoptions=fillexpand) 327 self.client_microphone_codecs_label = slabel() 328 self.client_microphone_codecs_label.set_line_wrap(True) 329 self.server_microphone_codecs_label = slabel() 330 self.server_microphone_codecs_label.set_line_wrap(True) 331 clrow("Microphone Codecs", 332 self.client_microphone_codecs_label, self.server_microphone_codecs_label, 333 xoptions=fillexpand, yoptions=fillexpand) 334 self.client_packet_encoders_label = slabel() 335 self.client_packet_encoders_label.set_line_wrap(True) 336 self.server_packet_encoders_label = slabel() 337 self.server_packet_encoders_label.set_line_wrap(True) 338 clrow("Packet Encoders", 339 self.client_packet_encoders_label, self.server_packet_encoders_label, 340 xoptions=fillexpand, yoptions=fillexpand) 341 self.client_packet_compressors_label = slabel() 342 self.server_packet_compressors_label = slabel() 343 clrow("Packet Compressors", 344 self.client_packet_compressors_label, self.server_packet_compressors_label, 345 xoptions=fillexpand, yoptions=fillexpand) 346 347 # Connection Table: 348 tb = self.table_tab("connect.png", "Connection", self.populate_connection)[0] 349 if self.connection: 350 tb.new_row("Server Endpoint", slabel(self.connection.target)) 351 if mixin_features.display and self.client.server_display: 352 tb.new_row("Server Display", slabel(prettify_plug_name(self.client.server_display))) 353 tb.new_row("Server Hostname", slabel(cattr("_remote_hostname"))) 354 if cattr("server_platform"): 355 tb.new_row("Server Platform", slabel(cattr("server_platform"))) 356 self.server_load_label = slabel() 357 tb.new_row("Server Load", self.server_load_label, label_tooltip="Average over 1, 5 and 15 minutes") 358 self.session_started_label = slabel() 359 tb.new_row("Session Started", self.session_started_label) 360 self.session_connected_label = slabel() 361 if show_client: 362 tb.new_row("Session Connected", self.session_connected_label) 363 self.input_packets_label = slabel() 364 tb.new_row("Packets Received", self.input_packets_label) 365 self.input_bytes_label = slabel() 366 tb.new_row("Bytes Received", self.input_bytes_label) 367 self.output_packets_label = slabel() 368 tb.new_row("Packets Sent", self.output_packets_label) 369 self.output_bytes_label = slabel() 370 tb.new_row("Bytes Sent", self.output_bytes_label) 371 self.compression_label = slabel() 372 tb.new_row("Encoding + Compression", self.compression_label) 373 self.connection_type_label = slabel() 374 tb.new_row("Connection Type", self.connection_type_label) 375 self.input_encryption_label = slabel() 376 tb.new_row("Input Encryption", self.input_encryption_label) 377 self.output_encryption_label = slabel() 378 tb.new_row("Output Encryption", self.output_encryption_label) 379 380 self.speaker_label = slabel() 381 self.speaker_details = slabel(font="monospace 10") 382 tb.new_row("Speaker", self.speaker_label, self.speaker_details) 383 self.microphone_label = slabel() 384 tb.new_row("Microphone", self.microphone_label) 385 386 # Details: 387 tb, stats_box = self.table_tab("browse.png", "Statistics", self.populate_statistics) 388 tb.widget_xalign = 1.0 389 tb.attach(title_box(""), 0, xoptions=fillexpand, xpadding=0) 390 tb.attach(title_box("Latest"), 1, xoptions=fillexpand, xpadding=0) 391 tb.attach(title_box("Minimum"), 2, xoptions=fillexpand, xpadding=0) 392 tb.attach(title_box("Average"), 3, xoptions=fillexpand, xpadding=0) 393 tb.attach(title_box("90 percentile"), 4, xoptions=fillexpand, xpadding=0) 394 tb.attach(title_box("Maximum"), 5, xoptions=fillexpand, xpadding=0) 395 tb.inc() 396 397 def maths_labels(): 398 return slabel(), slabel(), slabel(), slabel(), slabel() 399 self.server_latency_labels = maths_labels() 400 tb.add_row(slabel("Server Latency (ms)", "The time it takes for the server to respond to pings"), 401 *self.server_latency_labels) 402 self.client_latency_labels = maths_labels() 403 tb.add_row(slabel("Client Latency (ms)", 404 "The time it takes for the client to respond to pings, as measured by the server"), 405 *self.client_latency_labels) 406 if mixin_features.windows and self.client.windows_enabled: 407 self.batch_labels = maths_labels() 408 tb.add_row(slabel("Batch Delay (MPixels / ms)", 409 "How long the server waits for new screen updates to accumulate before processing them"), 410 *self.batch_labels) 411 self.damage_labels = maths_labels() 412 tb.add_row(slabel("Damage Latency (ms)", 413 "The time it takes to compress a frame and pass it to the OS network layer"), 414 *self.damage_labels) 415 self.quality_labels = maths_labels() 416 tb.add_row(slabel("Encoding Quality (pct)", 417 "Automatic picture quality, average for all the windows"), 418 *self.quality_labels) 419 self.speed_labels = maths_labels() 420 tb.add_row(slabel("Encoding Speed (pct)", 421 "Automatic picture encoding speed (bandwidth vs CPU usage), average for all the windows"), 422 *self.speed_labels) 423 424 self.decoding_labels = maths_labels() 425 tb.add_row(slabel("Decoding Latency (ms)", 426 "How long it takes the client to decode a screen update"), 427 *self.decoding_labels) 428 self.regions_per_second_labels = maths_labels() 429 tb.add_row(slabel("Regions/s", 430 "The number of screen updates per second" 431 +" (includes both partial and full screen updates)"), 432 *self.regions_per_second_labels) 433 self.regions_sizes_labels = maths_labels() 434 tb.add_row(slabel("Pixels/region", "The number of pixels per screen update"), *self.regions_sizes_labels) 435 self.pixels_per_second_labels = maths_labels() 436 tb.add_row(slabel("Pixels/s", "The number of pixels processed per second"), *self.pixels_per_second_labels) 437 438 #Window count stats: 439 wtb = TableBuilder() 440 stats_box.add(wtb.get_table()) 441 #title row: 442 wtb.attach(title_box(""), 0, xoptions=fillexpand, xpadding=0) 443 wtb.attach(title_box("Regular"), 1, xoptions=fillexpand, xpadding=0) 444 wtb.attach(title_box("Transient"), 2, xoptions=fillexpand, xpadding=0) 445 wtb.attach(title_box("Trays"), 3, xoptions=fillexpand, xpadding=0) 446 if self.client.client_supports_opengl: 447 wtb.attach(title_box("OpenGL"), 4, xoptions=fillexpand, xpadding=0) 448 wtb.inc() 449 450 wtb.attach(slabel("Windows:"), 0, xoptions=fillexpand, xpadding=0) 451 self.windows_managed_label = slabel() 452 wtb.attach(self.windows_managed_label, 1) 453 self.transient_managed_label = slabel() 454 wtb.attach(self.transient_managed_label, 2) 455 self.trays_managed_label = slabel() 456 wtb.attach(self.trays_managed_label, 3) 457 if self.client.client_supports_opengl: 458 self.opengl_label = slabel() 459 wtb.attach(self.opengl_label, 4) 460 461 #add encoder info: 462 etb = TableBuilder() 463 stats_box.add(etb.get_table()) 464 self.encoder_info_box = Gtk.HBox(spacing=4) 465 etb.new_row("Window Encoders", self.encoder_info_box) 466 467 if show_client: 468 self.graph_box = Gtk.VBox(False, 10) 469 self.add_tab("statistics.png", "Graphs", self.populate_graphs, self.graph_box) 470 bandwidth_label = "Bandwidth used" 471 if SHOW_PIXEL_STATS: 472 bandwidth_label += ",\nand number of pixels rendered" 473 self.bandwidth_graph = self.add_graph_button(bandwidth_label, self.save_graph) 474 self.latency_graph = self.add_graph_button(None, self.save_graph) 475 if SHOW_SOUND_STATS: 476 self.sound_queue_graph = self.add_graph_button(None, self.save_graph) 477 else: 478 self.sound_queue_graph = None 479 self.connect("realize", self.populate_graphs) 480 self.pixel_in_data = deque(maxlen=N_SAMPLES+4) 481 self.net_in_bitcount = deque(maxlen=N_SAMPLES+4) 482 self.net_out_bitcount = deque(maxlen=N_SAMPLES+4) 483 self.sound_in_bitcount = deque(maxlen=N_SAMPLES+4) 484 self.sound_out_bitcount = deque(maxlen=N_SAMPLES+4) 485 self.sound_out_queue_min = deque(maxlen=N_SAMPLES*10+4) 486 self.sound_out_queue_max = deque(maxlen=N_SAMPLES*10+4) 487 self.sound_out_queue_cur = deque(maxlen=N_SAMPLES*10+4) 488 489 self.set_border_width(15) 490 self.add(self.tab_box) 491 def window_deleted(*_args): 492 self.is_closed = True 493 self.connect('delete_event', window_deleted) 494 self.show_tab(self.tabs[0][2]) 495 self.set_size_request(-1, -1) 496 self.init_counters() 497 self.populate() 498 self.populate_all() 499 GLib.timeout_add(1000, self.populate) 500 GLib.timeout_add(100, self.populate_tab) 501 if mixin_features.audio and SHOW_SOUND_STATS and show_client: 502 GLib.timeout_add(100, self.populate_sound_stats) 503 add_close_accel(self, self.destroy) 504 505 506 def get_window_title(self): 507 t = ["Session Info"] 508 c = self.client 509 if c: 510 if c.session_name or c.server_session_name: 511 t.append(c.session_name or c.server_session_name) 512 p = c._protocol 513 if p: 514 conn = getattr(p, "_conn", None) 515 if conn: 516 cinfo = conn.get_info() 517 t.append(cinfo.get("endpoint", bytestostr(conn.target))) 518 v = " - ".join(str(x) for x in t) 519 return v 520 521 def table_tab(self, icon_filename, title, populate_cb): 522 tb = TableBuilder() 523 table = tb.get_table() 524 vbox = self.vbox_tab(icon_filename, title, populate_cb) 525 al = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=1.0) 526 al.add(table) 527 vbox.pack_start(al, expand=True, fill=True, padding=20) 528 return tb, vbox 529 530 def vbox_tab(self, icon_filename, title, populate_cb): 531 vbox = Gtk.VBox(False, 0) 532 self.add_tab(icon_filename, title, populate_cb, contents=vbox) 533 return vbox 534 535 536 def add_tab(self, icon_filename, title, populate_cb, contents): 537 icon = get_icon_pixbuf(icon_filename) 538 def show_tab(*_args): 539 self.show_tab(contents) 540 button = imagebutton(title, icon, clicked_callback=show_tab) 541 button.connect("clicked", show_tab) 542 button.set_relief(Gtk.ReliefStyle.NONE) 543 self.tab_button_box.add(button) 544 self.tabs.append((title, button, contents, populate_cb)) 545 546 def show_tab(self, table): 547 button = None 548 for _, b, t, p_cb in self.tabs: 549 if t==table: 550 button = b 551 b.set_relief(Gtk.ReliefStyle.NORMAL) 552 b.grab_focus() 553 self.populate_cb = p_cb 554 else: 555 b.set_relief(Gtk.ReliefStyle.NONE) 556 assert button 557 for x in self.tab_box.get_children(): 558 if x!=self.tab_button_box: 559 self.tab_box.remove(x) 560 self.tab_box.pack_start(table, expand=True, fill=True, padding=0) 561 table.show_all() 562 #ensure we re-draw the whole window: 563 window = self.get_window() 564 if window: 565 alloc = self.get_allocation() 566 window.invalidate_rect(alloc, True) 567 568 def set_args(self, *args): 569 #this is a generic way for keyboard shortcuts or remote commands 570 #to pass parameters to us 571 log("set_args%s", args) 572 if not args: 573 return 574 #at the moment, we only handle the tab name as argument: 575 tab_name = args[0] 576 if tab_name.lower()!="help": 577 title = "" 578 for title, _, table, _ in self.tabs: 579 if title.lower()==tab_name.lower(): 580 self.show_tab(table) 581 return 582 log.warn("could not find session info tab named: %s", title) 583 log.warn("The options for tab names are: %s)", [x[0] for x in self.tabs]) 584 585 def populate_all(self): 586 for tab in self.tabs: 587 p_cb = tab[3] 588 if p_cb: 589 p_cb() 590 591 def add_graph_button(self, tooltip, click_cb): 592 button = Gtk.EventBox() 593 def set_cursor(widget): 594 cursor = Gdk.Cursor.new(Gdk.CursorType.BASED_ARROW_DOWN) 595 widget.get_window().set_cursor(cursor) 596 button.connect("realize", set_cursor) 597 graph = Gtk.Image() 598 graph.set_size_request(0, 0) 599 button.connect("button_press_event", click_cb, graph) 600 button.add(graph) 601 if tooltip: 602 graph.set_tooltip_text(tooltip) 603 self.graph_box.add(button) 604 return graph 605 606 def bool_icon(self, image, on_off): 607 if on_off: 608 icon = get_icon_pixbuf("ticked-small.png") 609 else: 610 icon = get_icon_pixbuf("unticked-small.png") 611 image.set_from_pixbuf(icon) 612 613 def populate_sound_stats(self, *_args): 614 #runs every 100ms 615 if self.is_closed: 616 return False 617 ss = self.client.sound_sink 618 if SHOW_SOUND_STATS and ss: 619 info = ss.get_info() 620 if info: 621 info = typedict(info) 622 def qlookup(attr): 623 return int(newdictlook(info, ("queue", attr), 0)) 624 self.sound_out_queue_cur.append(qlookup("cur")) 625 self.sound_out_queue_min.append(qlookup("min")) 626 self.sound_out_queue_max.append(qlookup("max")) 627 return not self.is_closed 628 629 def populate(self, *_args): 630 conn = self.connection 631 if self.is_closed or not conn: 632 return False 633 self.client.send_ping() 634 self.last_populate_time = monotonic() 635 636 if self.show_client: 637 self.show_opengl_state() 638 self.show_window_renderers() 639 #record bytecount every second: 640 self.net_in_bitcount.append(conn.input_bytecount*8) 641 self.net_out_bitcount.append(conn.output_bytecount*8) 642 if mixin_features.audio and SHOW_SOUND_STATS: 643 if self.client.sound_in_bytecount>0: 644 self.sound_in_bitcount.append(self.client.sound_in_bytecount * 8) 645 if self.client.sound_out_bytecount>0: 646 self.sound_out_bitcount.append(self.client.sound_out_bytecount * 8) 647 648 if self.show_client and mixin_features.windows: 649 #count pixels in the last second: 650 since = monotonic()-1 651 decoded = [0]+[pixels for _,t,pixels in self.client.pixel_counter if t>since] 652 self.pixel_in_data.append(sum(decoded)) 653 #update latency values 654 #there may be more than one record for each second 655 #so we have to average them to prevent the graph from "jumping": 656 def get_ping_latency_records(src, size=25): 657 recs = {} 658 src_list = list(src) 659 now = int(monotonic()) 660 while src_list and len(recs)<size: 661 when, value = src_list.pop() 662 if when>=(now-1): #ignore last second 663 continue 664 iwhen = int(when) 665 cv = recs.get(iwhen) 666 v = 1000.0*value 667 if cv: 668 v = (v+cv) / 2.0 #not very fair if more than 2 values... but this shouldn't happen anyway 669 recs[iwhen] = v 670 #ensure we always have a record for the last N seconds, even an empty one 671 for x in range(size): 672 i = now-2-x 673 if i not in recs: 674 recs[i] = None 675 return tuple(recs.get(x) for x in sorted(recs.keys())) 676 self.server_latency = get_ping_latency_records(self.client.server_ping_latency) 677 self.client_latency = get_ping_latency_records(self.client.client_ping_latency) 678 if self.client.server_last_info: 679 #populate running averages for graphs: 680 def getavg(*names): 681 return ( 682 newdictlook(self.client.server_last_info, list(names)+["avg"]) or 683 newdictlook(self.client.server_last_info, ["client"]+list(names)+["avg"]) 684 ) 685 def addavg(l, *names): 686 v = getavg(*names) 687 if v: 688 l.append(v) 689 addavg(self.avg_batch_delay, "batch", "delay") 690 addavg(self.avg_damage_out_latency, "damage", "out_latency") 691 spl = tuple(1000.0*x[1] for x in tuple(self.client.server_ping_latency)) 692 cpl = tuple(1000.0*x[1] for x in tuple(self.client.client_ping_latency)) 693 if spl and cpl: 694 self.avg_ping_latency.append(round(sum(spl+cpl)/len(spl+cpl))) 695 if mixin_features.windows and self.show_client: 696 pc = tuple(self.client.pixel_counter) 697 if pc: 698 tsize = 0 699 ttime = 0 700 for start_time, end_time, size in pc: 701 ttime += 1000.0 * (end_time-start_time) * size 702 tsize += size 703 self.avg_decoding_latency.append(round(ttime/tsize)) 704 #totals: ping latency is halved since we only care about sending, not sending+receiving 705 els = ( 706 (tuple(self.avg_batch_delay), 1), 707 (tuple(self.avg_damage_out_latency), 1), 708 (tuple(self.avg_ping_latency), 2), 709 (tuple(self.avg_decoding_latency), 1), 710 ) 711 if all(x[0] for x in els): 712 totals = tuple(x[-1]/r for x, r in els) 713 log("frame totals=%s", totals) 714 self.avg_total.append(round(sum(totals))) 715 return not self.is_closed 716 717 def populate_software(self): 718 #called from the "fast" timer 719 return False 720 721 def init_counters(self): 722 self.avg_batch_delay = deque(maxlen=N_SAMPLES+4) 723 self.avg_damage_out_latency = deque(maxlen=N_SAMPLES+4) 724 self.avg_ping_latency = deque(maxlen=N_SAMPLES+4) 725 self.avg_decoding_latency = deque(maxlen=N_SAMPLES+4) 726 self.avg_total = deque(maxlen=N_SAMPLES+4) 727 728 def populate_tab(self, *_args): 729 if self.is_closed: 730 return False 731 #now re-populate the tab we are seeing: 732 if self.populate_cb: 733 if not self.populate_cb(): 734 self.populate_cb = None 735 return not self.is_closed 736 737 738 def show_opengl_state(self): 739 if self.client.opengl_enabled: 740 glinfo = "%s / %s" % ( 741 self.client.opengl_props.get("vendor", ""), 742 self.client.opengl_props.get("renderer", ""), 743 ) 744 display_mode = self.client.opengl_props.get("display_mode", []) 745 bit_depth = self.client.opengl_props.get("depth", 0) 746 info = [] 747 if bit_depth: 748 info.append("%i-bit" % bit_depth) 749 if "DOUBLE" in display_mode: 750 info.append("double buffering") 751 elif "SINGLE" in display_mode: 752 info.append("single buffering") 753 else: 754 info.append("unknown buffering") 755 if "ALPHA" in display_mode: 756 info.append("with transparency") 757 else: 758 info.append("without transparency") 759 else: 760 #info could be telling us that the gl bindings are missing: 761 glinfo = self.client.opengl_props.get("info", "disabled") 762 info = ["n/a"] 763 self.client_opengl_label.set_text(glinfo) 764 self.opengl_buffering.set_text(" ".join(info)) 765 766 def show_window_renderers(self): 767 if not mixin_features.windows: 768 return 769 wr = [] 770 renderers = {} 771 for wid, window in tuple(self.client._id_to_window.items()): 772 renderers.setdefault(window.get_backing_class(), []).append(wid) 773 for bclass, windows in renderers.items(): 774 wr.append("%s (%i)" % (bclass.__name__.replace("Backing", ""), len(windows))) 775 self.window_rendering.set_text("GTK3: %s" % csv(wr)) 776 777 def populate_features(self): 778 size_info = "" 779 if mixin_features.windows: 780 if self.client.server_actual_desktop_size: 781 w,h = self.client.server_actual_desktop_size 782 size_info = "%sx%s" % (w,h) 783 if self.client.server_randr and self.client.server_max_desktop_size: 784 size_info += " (max %s)" % ("x".join([str(x) for x in self.client.server_max_desktop_size])) 785 self.bool_icon(self.server_randr_icon, self.client.server_randr) 786 else: 787 size_info = "unknown" 788 unknown = get_icon_pixbuf("unknown.png") 789 if unknown: 790 self.server_randr_icon.set_from_pixbuf(unknown) 791 self.server_randr_label.set_text("%s" % size_info) 792 if self.show_client: 793 root_w, root_h = self.client.get_root_size() 794 if mixin_features.windows and (self.client.xscale!=1 or self.client.yscale!=1): 795 sw, sh = self.client.cp(root_w, root_h) 796 display_info = "%ix%i (scaled from %ix%i)" % (sw, sh, root_w, root_h) 797 else: 798 display_info = "%ix%i" % (root_w, root_h) 799 self.client_display.set_text(display_info) 800 self.bool_icon(self.client_opengl_icon, self.client.client_supports_opengl) 801 self.show_window_renderers() 802 803 if mixin_features.mmap: 804 self.bool_icon(self.server_mmap_icon, self.client.mmap_enabled) 805 if mixin_features.clipboard: 806 self.bool_icon(self.server_clipboard_icon, self.client.server_clipboard) 807 if mixin_features.notifications: 808 self.bool_icon(self.server_notifications_icon, self.client.server_notifications) 809 if mixin_features.windows: 810 self.bool_icon(self.server_bell_icon, self.client.server_bell) 811 self.bool_icon(self.server_cursors_icon, self.client.server_cursors) 812 813 def populate_codecs(self): 814 #clamp the large labels so they will overflow vertically: 815 w = self.tab_box.get_preferred_width()[0] 816 lw = max(200, int(w//2.5)) 817 if self.show_client: 818 self.client_encodings_label.set_size_request(lw, -1) 819 self.client_speaker_codecs_label.set_size_request(lw, -1) 820 self.client_microphone_codecs_label.set_size_request(lw, -1) 821 if self.show_server: 822 self.server_encodings_label.set_size_request(lw, -1) 823 self.server_speaker_codecs_label.set_size_request(lw, -1) 824 self.server_microphone_codecs_label.set_size_request(lw, -1) 825 #sound/video codec table: 826 def codec_info(enabled, codecs): 827 if not enabled: 828 return "n/a" 829 return ", ".join(codecs or ()) 830 if mixin_features.audio: 831 c = self.client 832 if self.show_server: 833 self.server_speaker_codecs_label.set_text(codec_info(c.server_sound_send, c.server_sound_encoders)) 834 if self.show_client: 835 self.client_speaker_codecs_label.set_text(codec_info(c.speaker_allowed, c.speaker_codecs)) 836 if self.show_server: 837 self.server_microphone_codecs_label.set_text(codec_info(c.server_sound_receive, c.server_sound_decoders)) 838 if self.show_client: 839 self.client_microphone_codecs_label.set_text(codec_info(c.microphone_allowed, c.microphone_codecs)) 840 def encliststr(v): 841 v = list(v) 842 try: 843 v.remove("rgb") 844 except ValueError: 845 pass 846 return csv(sorted(v)) 847 se = () 848 if mixin_features.encoding: 849 se = self.client.server_core_encodings 850 self.server_encodings_label.set_text(encliststr(se)) 851 if self.show_client and mixin_features.encoding: 852 self.client_encodings_label.set_text(encliststr(self.client.get_core_encodings())) 853 else: 854 self.client_encodings_label.set_text("n/a") 855 856 from xpra.net.packet_encoding import get_enabled_encoders 857 if self.show_client: 858 self.client_packet_encoders_label.set_text(csv(get_enabled_encoders())) 859 if self.show_server: 860 self.server_packet_encoders_label.set_text(csv(self.client.server_packet_encoders)) 861 862 from xpra.net.compression import get_enabled_compressors 863 if self.show_client: 864 self.client_packet_compressors_label.set_text(csv(get_enabled_compressors())) 865 if self.show_server: 866 self.server_packet_compressors_label.set_text(csv(self.client.server_compressors)) 867 return False 868 869 def populate_connection(self): 870 def settimedeltastr(label, from_time): 871 import time 872 delta = datetime.timedelta(seconds=(int(time.time())-int(from_time))) 873 label.set_text(str(delta)) 874 if self.client.server_load: 875 self.server_load_label.set_text(" ".join("%.1f" % (x/1000.0) for x in self.client.server_load)) 876 if self.client.server_start_time>0: 877 settimedeltastr(self.session_started_label, self.client.server_start_time) 878 else: 879 self.session_started_label.set_text("unknown") 880 settimedeltastr(self.session_connected_label, self.client.start_time) 881 882 p = self.client._protocol 883 if not self.show_client: 884 return True 885 if p is None: 886 #no longer connected! 887 return False 888 c = p._conn 889 if c: 890 self.input_packets_label.set_text(std_unit_dec(p.input_packetcount)) 891 self.input_bytes_label.set_text(std_unit_dec(c.input_bytecount)) 892 self.output_packets_label.set_text(std_unit_dec(p.output_packetcount)) 893 self.output_bytes_label.set_text(std_unit_dec(c.output_bytecount)) 894 else: 895 for l in ( 896 self.input_packets_label, 897 self.input_bytes_label, 898 self.output_packets_label, 899 self.output_bytes_label, 900 ): 901 l.set_text("n/a") 902 903 if mixin_features.audio: 904 def get_sound_info(supported, prop): 905 if not supported: 906 return {"state" : "disabled"} 907 if prop is None: 908 return {"state" : "inactive"} 909 return prop.get_info() 910 def set_sound_info(label, details, supported, prop): 911 d = typedict(get_sound_info(supported, prop)) 912 state = d.strget("state", "") 913 codec_descr = d.strget("codec") or d.strget("codec_description") 914 container_descr = d.strget("container_description", "") 915 if state=="active" and codec_descr: 916 if codec_descr.find(container_descr)>=0: 917 descr = codec_descr 918 else: 919 descr = csv(x for x in (codec_descr, container_descr) if x) 920 state = "%s: %s" % (state, descr) 921 label.set_text(state) 922 if details: 923 s = "" 924 bitrate = d.intget("bitrate", 0) 925 if bitrate>0: 926 s = "%sbit/s" % std_unit(bitrate) 927 details.set_text(s) 928 set_sound_info(self.speaker_label, self.speaker_details, self.client.speaker_enabled, self.client.sound_sink) 929 set_sound_info(self.microphone_label, None, self.client.microphone_enabled, self.client.sound_source) 930 931 self.connection_type_label.set_text(c.socktype) 932 protocol_info = p.get_info() 933 encoder = protocol_info.get("encoder", "bug") 934 compressor = protocol_info.get("compressor", "none") 935 level = protocol_info.get("compression_level", 0) 936 compression_str = encoder + " + "+compressor 937 if level>0: 938 compression_str += " (level %s)" % level 939 self.compression_label.set_text(compression_str) 940 941 def enclabel(label_widget, cipher): 942 if not cipher: 943 info = "None" 944 else: 945 info = str(cipher) 946 if c.socktype.lower()=="ssh": 947 info += " (%s)" % c.socktype 948 ncaps = get_network_caps() 949 backend = ncaps.get("backend") 950 if backend=="python-cryptography": 951 info += " / python-cryptography" 952 label_widget.set_text(info) 953 enclabel(self.input_encryption_label, p.cipher_in_name) 954 enclabel(self.output_encryption_label, p.cipher_out_name) 955 return True 956 957 958 def getval(self, prefix, suffix, alt=""): 959 if self.client.server_last_info is None: 960 return "" 961 altv = "" 962 if alt: 963 altv = dictlook(self.client.server_last_info, (alt+"."+suffix).encode(), "") 964 return dictlook(self.client.server_last_info, (prefix+"."+suffix).encode(), altv) 965 966 def values_from_info(self, prefix, alt=None): 967 def getv(suffix): 968 return self.getval(prefix, suffix, alt) 969 return getv("cur"), getv("min"), getv("avg"), getv("90p"), getv("max") 970 971 def all_values_from_info(self, *window_props): 972 #newer (2.4 and later) servers can just give us the value directly: 973 for window_prop in window_props: 974 prop_path = "client.%s" % window_prop 975 v = dictlook(self.client.server_last_info, prop_path) 976 if v is not None: 977 try: 978 v = typedict(v) 979 except TypeError: 980 #backwards compatibility: 981 #older servers don't expose the correct value or type here 982 #so don't use this value 983 log("expected dictionary for %s", prop_path) 984 log(" got %s: %s", type(v), v) 985 else: 986 iget = v.intget 987 return iget("cur"), iget("min"), iget("avg"), iget("90p"), iget("max") 988 989 #legacy servers: sum up the values for all the windows found 990 def avg(values): 991 if not values: 992 return "" 993 return sum(values) // len(values) 994 def getv(suffix, op): 995 if self.client.server_last_info is None: 996 return "" 997 values = [] 998 for wid in self.client._window_to_id.values(): 999 for window_prop in window_props: 1000 #Warning: this is ugly... 1001 proppath = "window[%s].%s.%s" % (wid, window_prop, suffix) 1002 v = self.client.server_last_info.get(proppath) 1003 if v is None: 1004 wprop = window_prop.split(".") #ie: "encoding.speed" -> ["encoding", "speed"] 1005 newpath = ["window", wid]+wprop+[suffix] #ie: ["window", 1, "encoding", "speed", "cur"] 1006 v = newdictlook(self.client.server_last_info, newpath) 1007 if v is not None: 1008 values.append(v) 1009 break 1010 if not values: 1011 return "" 1012 try: 1013 return op(values) 1014 except (TypeError, ValueError): 1015 log("%s(%s)", op, values, exc_info=True) 1016 return "" 1017 return getv("cur", avg), getv("min", min), getv("avg", avg), getv("90p", avg), getv("max", max) 1018 1019 def populate_statistics(self): 1020 log("populate_statistics()") 1021 if monotonic()-self.last_populate_statistics<1.0: 1022 #don't repopulate more than every second 1023 return True 1024 self.last_populate_statistics = monotonic() 1025 self.client.send_info_request() 1026 def setall(labels, values): 1027 assert len(labels)==len(values), "%s labels and %s values (%s vs %s)" % ( 1028 len(labels), len(values), labels, values) 1029 for i, l in enumerate(labels): 1030 l.set_text(str(values[i])) 1031 def setlabels(labels, values, rounding=int): 1032 if not values: 1033 return 1034 avg = sum(values)/len(values) 1035 svalues = sorted(values) 1036 l = len(svalues) 1037 assert l>0 1038 if l<10: 1039 index = l-1 1040 else: 1041 index = int(l*90/100) 1042 index = max(0, min(l-1, index)) 1043 pct = svalues[index] 1044 disp = values[-1], min(values), avg, pct, max(values) 1045 rounded_values = [rounding(v) for v in disp] 1046 setall(labels, rounded_values) 1047 1048 if self.client.server_ping_latency: 1049 spl = tuple(int(1000*x[1]) for x in tuple(self.client.server_ping_latency)) 1050 setlabels(self.server_latency_labels, spl) 1051 if self.client.client_ping_latency: 1052 cpl = tuple(int(1000*x[1]) for x in tuple(self.client.client_ping_latency)) 1053 setlabels(self.client_latency_labels, cpl) 1054 if mixin_features.windows and self.client.windows_enabled: 1055 setall(self.batch_labels, self.values_from_info("batch_delay", "batch.delay")) 1056 setall(self.damage_labels, self.values_from_info("damage_out_latency", "damage.out_latency")) 1057 setall(self.quality_labels, self.all_values_from_info("quality", "encoding.quality")) 1058 setall(self.speed_labels, self.all_values_from_info("speed", "encoding.speed")) 1059 1060 region_sizes = [] 1061 rps = [] 1062 pps = [] 1063 decoding_latency = [] 1064 if self.client.pixel_counter: 1065 min_time = None 1066 max_time = None 1067 regions_per_second = {} 1068 pixels_per_second = {} 1069 for start_time, end_time, size in self.client.pixel_counter: 1070 decoding_latency.append(int(1000.0*(end_time-start_time))) 1071 region_sizes.append(size) 1072 if min_time is None or min_time>end_time: 1073 min_time = end_time 1074 if max_time is None or max_time<end_time: 1075 max_time = end_time 1076 time_in_seconds = int(end_time) 1077 regions = regions_per_second.get(time_in_seconds, 0) 1078 regions_per_second[time_in_seconds] = regions+1 1079 pixels = pixels_per_second.get(time_in_seconds, 0) 1080 pixels_per_second[time_in_seconds] = pixels + size 1081 if int(min_time)+1 < int(max_time): 1082 for t in range(int(min_time)+1, int(max_time)): 1083 rps.append(regions_per_second.get(t, 0)) 1084 pps.append(pixels_per_second.get(t, 0)) 1085 setlabels(self.decoding_labels, decoding_latency) 1086 setlabels(self.regions_per_second_labels, rps) 1087 setlabels(self.regions_sizes_labels, region_sizes, rounding=std_unit_dec) 1088 setlabels(self.pixels_per_second_labels, pps, rounding=std_unit_dec) 1089 1090 windows, gl, transient, trays = 0, 0, 0, 0 1091 for w in self.client._window_to_id.keys(): 1092 if w.is_tray(): 1093 trays += 1 1094 elif w.is_OR(): 1095 transient +=1 1096 else: 1097 windows += 1 1098 if w.is_GL(): 1099 gl += 1 1100 self.windows_managed_label.set_text(str(windows)) 1101 self.transient_managed_label.set_text(str(transient)) 1102 self.trays_managed_label.set_text(str(trays)) 1103 if self.client.client_supports_opengl: 1104 self.opengl_label.set_text(str(gl)) 1105 1106 #remove all the current labels: 1107 for x in self.encoder_info_box.get_children(): 1108 self.encoder_info_box.remove(x) 1109 if self.client.server_last_info: 1110 window_encoder_stats = self.get_window_encoder_stats() 1111 #log("window_encoder_stats=%s", window_encoder_stats) 1112 for wid, props in window_encoder_stats.items(): 1113 l = slabel("%s (%s)" % (wid, bytestostr(props.get("")))) 1114 l.show() 1115 info = ("%s=%s" % (k,v) for k,v in props.items() if k!="") 1116 l.set_tooltip_text(" ".join(info)) 1117 self.encoder_info_box.add(l) 1118 return True 1119 1120 def get_window_encoder_stats(self): 1121 window_encoder_stats = {} 1122 #new-style server with namespace (easier): 1123 window_dict = self.client.server_last_info.get("window") 1124 if window_dict and isinstance(window_dict, dict): 1125 for k,v in window_dict.items(): 1126 try: 1127 wid = int(k) 1128 encoder_stats = v.get("encoder") 1129 if encoder_stats: 1130 window_encoder_stats[wid] = encoder_stats 1131 except Exception: 1132 log.error("Error: cannot lookup window dict", exc_info=True) 1133 return window_encoder_stats 1134 1135 1136 def set_graph_surface(self, graph, surface): 1137 w = surface.get_width() 1138 h = surface.get_height() 1139 graph.set_size_request(w, h) 1140 graph.surface = surface 1141 graph.set_from_surface(surface) 1142 1143 def populate_graphs(self, *_args): 1144 #older servers have 'batch' at top level, 1145 #newer servers store it under client 1146 self.client.send_info_request("network", "damage", "state", "batch", "client") 1147 box = self.tab_box 1148 h = box.get_preferred_height()[0] 1149 bh = self.tab_button_box.get_preferred_height()[0] 1150 if h<=0: 1151 return True 1152 start_x_offset = min(1.0, (monotonic()-self.last_populate_time)*0.95) 1153 rect = box.get_allocation() 1154 maxw, maxh = self.client.get_root_size() 1155 ngraphs = 2+int(SHOW_SOUND_STATS) 1156 #the preferred size (which does not cause the window to grow too big): 1157 W = 360 1158 H = 160*3//ngraphs 1159 w = min(maxw, max(W, rect.width-20)) 1160 #need some padding to be able to shrink the window again: 1161 pad = 50 1162 h = min(maxh-pad//ngraphs, max(H, (h-bh-pad)//ngraphs, (rect.height-bh-pad)//ngraphs)) 1163 #bandwidth graph: 1164 labels, datasets = [], [] 1165 if self.net_in_bitcount and self.net_out_bitcount: 1166 def unit(scale): 1167 if scale==1: 1168 return "" 1169 unit, value = to_std_unit(scale) 1170 if value==1: 1171 return str(unit) 1172 return "x%s%s" % (int(value), unit) 1173 net_in_scale, net_in_data = values_to_diff_scaled_values(tuple(self.net_in_bitcount)[1:N_SAMPLES+3], scale_unit=1000, min_scaled_value=50) 1174 net_out_scale, net_out_data = values_to_diff_scaled_values(tuple(self.net_out_bitcount)[1:N_SAMPLES+3], scale_unit=1000, min_scaled_value=50) 1175 if SHOW_RECV: 1176 labels += ["recv %sb/s" % unit(net_in_scale), "sent %sb/s" % unit(net_out_scale)] 1177 datasets += [net_in_data, net_out_data] 1178 else: 1179 labels += ["recv %sb/s" % unit(net_in_scale)] 1180 datasets += [net_in_data] 1181 if mixin_features.windows and SHOW_PIXEL_STATS and self.client.windows_enabled: 1182 pixel_scale, in_pixels = values_to_scaled_values(tuple(self.pixel_in_data)[3:N_SAMPLES+4], min_scaled_value=100) 1183 datasets.append(in_pixels) 1184 labels.append("%s pixels/s" % unit(pixel_scale)) 1185 if mixin_features.audio and SHOW_SOUND_STATS and self.sound_in_bitcount: 1186 sound_in_scale, sound_in_data = values_to_diff_scaled_values(tuple(self.sound_in_bitcount)[1:N_SAMPLES+3], scale_unit=1000, min_scaled_value=50) 1187 datasets.append(sound_in_data) 1188 labels.append("Speaker %sb/s" % unit(sound_in_scale)) 1189 if mixin_features.audio and SHOW_SOUND_STATS and self.sound_out_bitcount: 1190 sound_out_scale, sound_out_data = values_to_diff_scaled_values(tuple(self.sound_out_bitcount)[1:N_SAMPLES+3], scale_unit=1000, min_scaled_value=50) 1191 datasets.append(sound_out_data) 1192 labels.append("Mic %sb/s" % unit(sound_out_scale)) 1193 1194 if labels and datasets: 1195 surface = make_graph_imagesurface(datasets, labels=labels, 1196 width=w, height=h, 1197 title="Bandwidth", min_y_scale=10, rounding=10, 1198 start_x_offset=start_x_offset) 1199 self.set_graph_surface(self.bandwidth_graph, surface) 1200 1201 def norm_lists(items, size=N_SAMPLES): 1202 #ensures we always have exactly 20 values, 1203 #(and skip if we don't have any) 1204 values, labels = [], [] 1205 for l, name in items: 1206 if not l: 1207 continue 1208 l = list(l) 1209 if len(l)<size: 1210 for _ in range(size-len(l)): 1211 l.insert(0, None) 1212 else: 1213 l = l[:size] 1214 values.append(l) 1215 labels.append(name) 1216 return values, labels 1217 1218 #latency graph: 1219 latency_values, latency_labels = norm_lists( 1220 ( 1221 (self.avg_ping_latency, "network"), 1222 (self.avg_batch_delay, "batch delay"), 1223 (self.avg_damage_out_latency, "encode&send"), 1224 (self.avg_decoding_latency, "decoding"), 1225 (self.avg_total, "frame total"), 1226 )) 1227 #debug: 1228 #for i, v in enumerate(latency_values): 1229 # log.warn("%20s = %s", latency_labels[i], v) 1230 surface = make_graph_imagesurface(latency_values, labels=latency_labels, 1231 width=w, height=h, 1232 title="Latency (ms)", min_y_scale=10, rounding=25, 1233 start_x_offset=start_x_offset) 1234 self.set_graph_surface(self.latency_graph, surface) 1235 1236 if mixin_features.audio and SHOW_SOUND_STATS and self.client.sound_sink: 1237 #sound queue graph: 1238 queue_values, queue_labels = norm_lists( 1239 ( 1240 (self.sound_out_queue_max, "Max"), 1241 (self.sound_out_queue_cur, "Level"), 1242 (self.sound_out_queue_min, "Min"), 1243 ), N_SAMPLES*10) 1244 surface = make_graph_imagesurface(queue_values, labels=queue_labels, 1245 width=w, height=h, 1246 title="Sound Buffer (ms)", min_y_scale=10, rounding=25, 1247 start_x_offset=start_x_offset) 1248 self.set_graph_surface(self.sound_queue_graph, surface) 1249 return True 1250 1251 def save_graph(self, _ebox, btn, graph): 1252 log("save_graph%s", (btn, graph)) 1253 chooser = Gtk.FileChooserDialog("Save graph as a PNG image", 1254 parent=self, action=Gtk.FileChooserAction.SAVE, 1255 buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK)) 1256 chooser.set_select_multiple(False) 1257 chooser.set_default_response(Gtk.ResponseType.OK) 1258 file_filter = Gtk.FileFilter() 1259 file_filter.set_name("PNG") 1260 file_filter.add_pattern("*.png") 1261 chooser.add_filter(file_filter) 1262 response = chooser.run() 1263 filenames = chooser.get_filenames() 1264 chooser.hide() 1265 chooser.destroy() 1266 if response == Gtk.ResponseType.OK: 1267 if len(filenames)==1: 1268 filename = filenames[0] 1269 if not filename.lower().endswith(".png"): 1270 filename += ".png" 1271 surface = graph.surface 1272 log("saving surface %s to %s", surface, filename) 1273 from io import BytesIO 1274 b = BytesIO() 1275 surface.write_to_png(b) 1276 def save_file(): 1277 with open(filename, "wb") as f: 1278 f.write(b.getvalue()) 1279 from xpra.make_thread import start_thread 1280 start_thread(save_file, "save-graph") 1281 elif response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.CLOSE, Gtk.ResponseType.DELETE_EVENT): 1282 log("closed/cancelled") 1283 else: 1284 log.warn("unknown chooser response: %d" % response) 1285 1286 def destroy(self, *args): 1287 log("SessionInfo.destroy(%s) is_closed=%s", args, self.is_closed) 1288 self.is_closed = True 1289 super().destroy() 1290 log("SessionInfo.destroy(%s) done", args) 1291 1292 1293class SessionInfoClient(InfoTimerClient): 1294 1295 REFRESH_RATE = envint("XPRA_INFO_REFRESH_RATE", 2) 1296 1297 def setup_connection(self, conn): 1298 self.session_name = self.server_session_name = "session-info" 1299 self.mmap_enabled = False 1300 self.windows_enabled = False 1301 def noop(*_args): 1302 pass 1303 self.send_ping = noop 1304 self.server_sound_send = self.server_sound_receive = True 1305 self.server_sound_encoders = self.server_sound_decoders = [] 1306 self.server_ping_latency = self.client_ping_latency = [] 1307 self.server_start_time = 0 1308 protocol = super().setup_connection(conn) 1309 self.window = None 1310 return protocol 1311 1312 def run_loop(self): 1313 Gtk.main() 1314 1315 def exit_loop(self): 1316 Gtk.main_quit() 1317 1318 def client_type(self): 1319 #overriden in subclasses! 1320 return "session-info" 1321 1322 def update_screen(self): 1323 #this is called every time we get the server info back 1324 #log.info("server_last_info=%s", self.server_last_info) 1325 if not self.server_last_info: 1326 return 1327 td = typedict(self.server_last_info) 1328 def rtdict(*keys): 1329 d = td 1330 for prop in keys: 1331 v = d.dictget(prop) 1332 #log.info("%s.get(%s)=%s", repr_ellipsized(d), prop, repr_ellipsized(v)) 1333 d = typedict(v or {}) 1334 return d 1335 from xpra.client.mixins.serverinfo_mixin import get_remote_lib_versions 1336 features = rtdict("features") 1337 self.server_clipboard = features.boolget("clipboard") 1338 self.server_notifications = features.boolget("notifications") 1339 display = rtdict("display") 1340 self.server_opengl = rtdict("display", "opengl") 1341 self.server_bell = display.boolget("bell") 1342 self.server_cursors = display.boolget("cursors") 1343 self.server_randr = display.boolget("randr") 1344 server = rtdict("server") 1345 self.server_actual_desktop_size = server.inttupleget("root_window_size") 1346 self.server_max_desktop_size = server.inttupleget("max_desktop_size") 1347 self._remote_lib_versions = get_remote_lib_versions(rtdict("server")) 1348 encodings = rtdict("encodings") 1349 self.server_core_encodings = encodings.strtupleget("core") 1350 network = rtdict("network") 1351 self.server_packet_encoders = network.strtupleget("encoders") 1352 self.server_compressors = network.strtupleget("compressors") 1353 self.server_load = server.inttupleget("load") 1354 self._remote_hostname = server.strget("hostname") 1355 build = rtdict("server", "build") 1356 self._remote_version = build.strget("version") 1357 self._remote_build_date = build.strget("date") 1358 self._remote_build_time = build.strget("time") 1359 self._remote_revision = build.intget("revision") 1360 self._remote_modifications = build.intget("local_modifications") 1361 self._remote_branch = build.strget("branch") 1362 self._remote_commit = build.strget("commit") 1363 platform_info = rtdict("server", "platform") 1364 self._remote_platform = platform_info.strget("") 1365 self._remote_platform_release = platform_info.strget("release") 1366 self._remote_platform_platform = platform_info.strget("platform") 1367 self._remote_platform_linux_distribution = platform_info.strget("linux_distribution") 1368 self.server_display = server.strget("display") 1369 self.idle_add(self.do_update_screen) 1370 1371 def do_update_screen(self): 1372 if not self.window: 1373 self.window = SessionInfo(self, "session-info", self._protocol._conn, show_client=False) 1374 self.window.show_all() 1375 def destroy(*_args): 1376 self.exit_loop() 1377 self.window.connect("destroy", destroy) 1378 self.window.populate() 1379 return False 1380