xpra icon
Bug tracker and wiki

This bug tracker and wiki are being discontinued
please use https://github.com/Xpra-org/xpra instead.


Ticket #1655: test_measure_perf_xvfb.py

File test_measure_perf_xvfb.py, 46.7 KB (added by J. Max Mena, 4 years ago)

modified test_measure_perf

Line 
1#!/usr/bin/env python
2# This file is part of Xpra.
3# Copyright (C) 2012, 2013 Antoine Martin <antoine@devloop.org.uk>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
6#
7# To create multiple output files which can be used to generate charts (using test_measure_perf_charts.py)
8# build a config class (copy from perf_config_default.py -- make changes as necessary).
9#
10# Then determine the values of the following variables:
11#   prefix: a string to identify the data set
12#   id: a string to identify the variable that the data set is testing (for example '14' because we're testing xpra v14 in this data set)
13#   repetitions: decide how many times you want to run the tests
14#
15# The data file names you will produce will then be in the format:
16#   prefix_id_rep#.csv
17#
18# With this information in hand you can now create a script that will run the tests, containing commands like:
19#   ./test_measure_perf.py as an example:
20
21# For example:
22#
23# ./test_measure_perf.py all_tests_40 ./data/all_tests_40_14_1.csv 1 14 > ./data//all_tests_40_14_1.log
24# ./test_measure_perf.py all_tests_40 ./data/all_tests_40_14_2.csv 2 14 > ./data//all_tests_40_14_2.log
25#
26# In this example script, I'm running test_measure_perf 2 times, using a config class named "all_tests_40.py",
27# and outputting the results to data files using the prefix "all_tests_40", for version 14.
28#
29# The additional arguments "1 14", "2 14" are custom paramaters which will be written to the "Custom Params" column
30# in the corresponding data files.
31#
32# Where you see "1", "2" in the file names or params, that's referring to the corresponding repetition of the tests.
33#
34# Once this script has run, you can open up test_measure_perf_charts.py and take a look at the
35# instructions there for generating the charts.
36#
37
38import re
39import sys
40import subprocess
41import os.path
42import time
43
44from xpra.log import Logger
45log = Logger()
46
47def getoutput(cmd, env=None):
48    try:
49        process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, close_fds=True)
50    except Exception as e:
51        print("error running %s: %s" % (cmd, e))
52        raise e
53    (out,err) = process.communicate()
54    code = process.poll()
55    if code!=0:
56        raise Exception("command '%s' returned error code %s, out=%s, err=%s" % (cmd, code, out, err))
57    return out
58
59def get_config(config_name):
60    try:
61        mod = __import__(config_name)
62    except (ImportError, SyntaxError) as e:
63        sys.stderr.write("Error loading module %s (%s)\n" % (config_name, e))
64        return None
65    return mod.Config()
66
67if (len(sys.argv) > 1):
68    config_name = sys.argv[1]
69else:
70    config_name = 'perf_config_default'
71config = get_config(config_name)
72if (config==None):
73    raise Exception("Could not load config file")
74
75XPRA_BIN = "/usr/bin/xpra"
76XPRA_VERSION_OUTPUT = getoutput([XPRA_BIN, "--version"])
77XPRA_VERSION = ""
78for x in XPRA_VERSION_OUTPUT.splitlines():
79    if x.startswith("xpra v"):
80        XPRA_VERSION = x[len("xpra v"):].replace("\n", "").replace("\r", "")
81XPRA_VERSION = XPRA_VERSION.split("-")[0]
82XPRA_VERSION_NO = [int(x) for x in XPRA_VERSION.split(".")]
83XPRA_SERVER_STOP_COMMANDS = [
84                             [XPRA_BIN, "stop", ":%s" % config.DISPLAY_NO],
85                             "ps -ef | grep -i [X]vfb-for-Xpra-:%s | awk '{print $2}' | xargs kill" % config.DISPLAY_NO
86                             ]
87XPRA_INFO_COMMAND = [XPRA_BIN, "info", "tcp:%s:%s" % (config.IP, config.PORT)]
88print ("XPRA_VERSION_NO=%s" % XPRA_VERSION_NO)
89
90STRICT_ENCODINGS = False
91if STRICT_ENCODINGS:
92    #beware: only enable this flag if the version being tested
93    # also supports the same environment overrides,
94    # or the comparison will not be fair.
95    os.environ["XPRA_ENCODING_STRICT_MODE"] = "1"
96    os.environ["XPRA_MAX_PIXELS_PREFER_RGB"] = "0"
97    os.environ["XPRA_MAX_NONVIDEO_PIXELS"] = "0"
98
99XPRA_SPEAKER_OPTIONS = [None]
100XPRA_MICROPHONE_OPTIONS = [None]
101if config.TEST_SOUND:
102    from xpra.sound.gstreamer_util import CODEC_ORDER, has_codec
103    if XPRA_VERSION_NO>=[0, 9]:
104        #0.9 onwards supports all codecs defined:
105        XPRA_SPEAKER_OPTIONS = [x for x in CODEC_ORDER if has_codec(x)]
106    elif XPRA_VERSION_NO==[0, 8]:
107        #only mp3 works in 0.8:
108        XPRA_SPEAKER_OPTIONS = ["mp3"]
109    else:
110        #none before that
111        XPRA_SPEAKER_OPTIONS = [None]
112
113if (config.XPRA_USE_PASSWORD):
114    password_filename = "./test-password.txt"
115    import uuid
116    with open(password_filename, 'wb') as f:
117        f.write(uuid.uuid4().hex)
118
119check = [config.TRICKLE_BIN]
120if config.TEST_XPRA:
121    check.append(XPRA_BIN)
122if config.TEST_VNC:
123    check.append(config.XVNC_BIN)
124    check.append(config.VNCVIEWER_BIN)
125for x in check:
126    if not os.path.exists(x):
127        raise Exception("cannot run tests: %s is missing!" % x)
128
129HEADERS = ["Test Name", "Remoting Tech", "Server Version", "Client Version", "Custom Params", "SVN Version",
130           "Encoding", "Quality", "Speed","OpenGL", "Test Command", "Sample Duration (s)", "Sample Time (epoch)",
131           "CPU info", "Platform", "Kernel Version", "Xorg version", "OpenGL", "Client Window Manager", "Screen Size",
132           "Compression", "Encryption", "Connect via", "download limit (KB)", "upload limit (KB)", "latency (ms)",
133           "packets in/s", "packets in: bytes/s", "packets out/s", "packets out: bytes/s",
134           "Regions/s", "Pixels/s Sent", "Encoding Pixels/s", "Decoding Pixels/s",
135           "Application packets in/s", "Application bytes in/s",
136           "Application packets out/s", "Application bytes out/s", "mmap bytes/s",
137           "Video Encoder", "CSC", "CSC Mode", "Scaling",
138           ]
139for x in ("client", "server"):
140    HEADERS += [x+" user cpu_pct", x+" system cpu pct", x+" number of threads", x+" vsize (MB)", x+" rss (MB)"]
141#all these headers have min/max/avg:
142for h in ("Batch Delay (ms)", "Actual Batch Delay (ms)",
143          "Client Latency (ms)", "Client Ping Latency (ms)", "Server Ping Latency (ms)",
144          "Damage Latency (ms)",
145          "Quality", "Speed"):
146    for x in ("Min", "Avg", "Max"):
147        HEADERS.append(x+" "+h)
148
149def is_process_alive(process, grace=0):
150    i = 0
151    while i<grace:
152        if not process or process.poll() is not None:
153            return  False
154        time.sleep(1)
155        i += 1
156    return process and process.poll() is None
157
158def try_to_stop(process, grace=0):
159    if is_process_alive(process, grace):
160        try:
161            process.terminate()
162        except Exception as e:
163            print("could not stop process %s: %s" % (process, e))
164def try_to_kill(process, grace=0):
165    if is_process_alive(process, grace):
166        try:
167            process.kill()
168        except Exception as e:
169            print("could not stop process %s: %s" % (process, e))
170
171def find_matching_lines(out, pattern):
172    lines = []
173    for line in out.splitlines():
174        if line.find(pattern)>=0:
175            lines.append(line)
176    return  lines
177
178def getoutput_lines(cmd, pattern, setup_info):
179    out = getoutput(cmd)
180    return  find_matching_lines(out, pattern)
181
182def getoutput_line(cmd, pattern, setup_info):
183    lines = getoutput_lines(cmd, pattern, setup_info)
184    if len(lines)!=1:
185        print("WARNING: expected 1 line matching '%s' from %s but found %s" % (pattern, cmd, len(lines)))
186        return "not found"
187    return  lines[0]
188
189def get_cpu_info():
190    lines = getoutput_lines(["cat", "/proc/cpuinfo"], "model name", "cannot find cpu info")
191    assert len(lines)>0, "coult not find 'model name' in '/proc/cpuinfo'"
192    cpu0 = lines[0]
193    n = len(lines)
194    for x in lines[1:]:
195        if x!=cpu0:
196            return " - ".join(lines), n
197    cpu_name = cpu0.split(":")[1]
198    for o,r in [("Processor", ""), ("(R)", ""), ("(TM)", ""), ("(tm)", ""), ("  ", " ")]:
199        while cpu_name.find(o)>=0:
200            cpu_name = cpu_name.replace(o, r)
201    cpu_info = "%sx %s" % (len(lines), cpu_name.strip())
202    print("CPU_INFO=%s" % cpu_info)
203    return  cpu_info, n
204
205XORG_VERSION = getoutput_line([config.XORG_BIN, "-version"], "X.Org X Server", "Cannot detect Xorg server version")
206print("XORG_VERSION=%s" % XORG_VERSION)
207CPU_INFO, N_CPUS = get_cpu_info()
208KERNEL_VERSION = getoutput(["uname", "-r"]).replace("\n", "").replace("\r", "")
209PAGE_SIZE = int(getoutput(["getconf", "PAGESIZE"]).replace("\n", "").replace("\r", ""))
210PLATFORM = getoutput(["uname", "-p"]).replace("\n", "").replace("\r", "")
211OPENGL_INFO = getoutput_line(["glxinfo"], "OpenGL renderer string", "Cannot detect OpenGL renderer string").split("OpenGL renderer string:")[1].strip()
212
213import pygtk
214pygtk.require("2.0")
215import gtk                                      #@UnusedImport
216from gtk import gdk                             #@UnusedImport
217SCREEN_SIZE = gdk.get_default_root_window().get_size()
218print("screen size=%s" % str(SCREEN_SIZE))
219
220#detect Xvnc version:
221XVNC_VERSION = ""
222VNCVIEWER_VERSION = ""
223DETECT_XVNC_VERSION_CMD = [config.XVNC_BIN, "--help"]
224DETECT_VNCVIEWER_VERSION_CMD = [config.VNCVIEWER_BIN, "--help"]
225def get_stderr(command):
226    try:
227        process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
228        _,err = process.communicate()
229        return err
230    except Exception as e:
231        print("error running %s: %s" % (DETECT_XVNC_VERSION_CMD, e))
232
233err = get_stderr(DETECT_XVNC_VERSION_CMD)
234if err:
235    v_lines = find_matching_lines(err, "Xvnc TigerVNC")
236    if len(v_lines)==1:
237        XVNC_VERSION = " ".join(v_lines[0].split()[:3])
238print ("XVNC_VERSION=%s" % XVNC_VERSION)
239err = get_stderr(DETECT_VNCVIEWER_VERSION_CMD)
240if err:
241    v_lines = find_matching_lines(err, "TigerVNC Viewer for X version")
242    if len(v_lines)==1:
243        VNCVIEWER_VERSION = "TigerVNC Viewer %s" % (v_lines[0].split()[5])
244print ("VNCVIEWER_VERSION=%s" % VNCVIEWER_VERSION)
245
246#get svnversion, prefer directly from svn:
247try:
248    SVN_VERSION = getoutput(["svnversion", "-n"]).split(":")[-1].strip()
249except:
250    SVN_VERSION = ""
251if not SVN_VERSION:
252    #fallback to getting it from xpra's src_info:
253    try:
254        from xpra.src_info import REVISION, LOCAL_MODIFICATIONS
255        SVN_VERSION = 'r%s' % REVISION
256        if LOCAL_MODIFICATIONS:
257            SVN_VERSION += "M"
258    except:
259        pass
260if not SVN_VERSION:
261    #fallback to running python:
262    SVN_VERSION = getoutput(["python", "-c", "from xpra.src_info import REVISION,LOCAL_MODIFICATIONS;print(('r%s%s' % (REVISION, ' M'[int(bool(LOCAL_MODIFICATIONS))])).strip())"])
263print("Found xpra revision: '%s'" % str(SVN_VERSION))
264
265WINDOW_MANAGER = os.environ.get("DESKTOP_SESSION", "unknown")
266
267def clean_sys_state():
268    #clear the caches
269    cmd = ["echo", "3", ">", "/proc/sys/vm/drop_caches"]
270    process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
271    assert process.wait()==0, "failed to run %s" % str(cmd)
272
273def zero_iptables():
274    if not config.USE_IPTABLES:
275        return
276    cmds = [config.IPTABLES_CMD+['-Z', 'INPUT'], config.IPTABLES_CMD+['-Z', 'OUTPUT']]
277    for cmd in cmds:
278        getoutput(cmd)
279        #out = getoutput(cmd)
280        #print("output(%s)=%s" % (cmd, out))
281
282def update_proc_stat():
283    with open("/proc/stat", "rU") as proc_stat:
284        time_total = 0
285        for line in proc_stat:
286            values = line.split()
287            if values[0]=="cpu":
288                time_total = sum([int(x) for x in values[1:]])
289                #print("time_total=%s" % time_total)
290                break
291    return time_total
292
293def update_pidstat(pid):
294    with open("/proc/%s/stat" % pid, "rU") as stat_file:
295        data = stat_file.read()
296    pid_stat = data.split()
297    #print("update_pidstat(%s): %s" % (pid, pid_stat))
298    return pid_stat
299
300def compute_stat(prefix, time_total_diff, old_pid_stat, new_pid_stat):
301    #found help here:
302    #http://stackoverflow.com/questions/1420426/calculating-cpu-usage-of-a-process-in-linux
303    old_utime = int(old_pid_stat[13])
304    old_stime = int(old_pid_stat[14])
305    new_utime = int(new_pid_stat[13])
306    new_stime = int(new_pid_stat[14])
307    #normalize to 100% (single process) by multiplying by number of CPUs:
308    user_pct = int(N_CPUS * 1000 * (new_utime - old_utime) / time_total_diff)/10.0
309    sys_pct = int(N_CPUS * 1000 * (new_stime - old_stime) / time_total_diff)/10.0
310    nthreads = int((int(old_pid_stat[19])+int(new_pid_stat[19]))/2)
311    vsize = int(max(int(old_pid_stat[22]), int(new_pid_stat[22]))/1024/1024)
312    rss = int(max(int(old_pid_stat[23]), int(new_pid_stat[23]))*PAGE_SIZE/1024/1024)
313    return {prefix+" user cpu_pct"       : user_pct,
314            prefix+" system cpu pct"     : sys_pct,
315            prefix+" number of threads"  : nthreads,
316            prefix+" vsize (MB)"         : vsize,
317            prefix+" rss (MB)"           : rss,
318            }
319
320def getiptables_line(chain, pattern, setup_info):
321    cmd = config.IPTABLES_CMD + ["-vnL", chain]
322    line = getoutput_line(cmd, pattern, setup_info)
323    if not line:
324        raise Exception("no line found matching %s, make sure you have a rule like: %s" % (pattern, setup_info))
325    return line
326
327def parse_ipt(chain, pattern, setup_info):
328    if not config.USE_IPTABLES:
329        return  0, 0
330    line = getiptables_line(chain, pattern, setup_info)
331    parts = line.split()
332    assert len(parts)>2
333    def parse_num(part):
334        U = 1024
335        m = {"K":U, "M":U**2, "G":U**3}.get(part[-1], 1)
336        num = "".join([x for x in part if x in "0123456789"])
337        return int(num)*m/config.MEASURE_TIME
338    return parse_num(parts[0]), parse_num(parts[1])
339
340def get_iptables_INPUT_count():
341    setup = "iptables -I INPUT -p tcp --dport %s -j ACCEPT" % config.PORT
342    return  parse_ipt("INPUT", "tcp dpt:%s" % config.PORT, setup)
343
344def get_iptables_OUTPUT_count():
345    setup = "iptables -I OUTPUT -p tcp --sport %s -j ACCEPT" % config.PORT
346    return  parse_ipt("OUTPUT", "tcp spt:%s" % config.PORT, setup)
347
348def measure_client(server_pid, name, cmd, get_stats_cb):
349    print("starting client: %s" % cmd)
350    try:
351        client_process = subprocess.Popen(cmd)
352        #give it time to settle down:
353        time.sleep(config.SETTLE_TIME)
354        code = client_process.poll()
355        assert code is None, "client failed to start, return code is %s" % code
356        #clear counters
357        initial_stats = get_stats_cb()
358        zero_iptables()
359        old_time_total = update_proc_stat()
360        old_pid_stat = update_pidstat(client_process.pid)
361        if server_pid>0:
362            old_server_pid_stat = update_pidstat(server_pid)
363        #we start measuring
364        t = 0
365        all_stats = [initial_stats]
366        while t<config.MEASURE_TIME:
367            time.sleep(config.COLLECT_STATS_TIME)
368            t += config.COLLECT_STATS_TIME
369
370            code = client_process.poll()
371            assert code is None, "client crashed, return code is %s" % code
372
373            stats = get_stats_cb(initial_stats, all_stats)
374
375        #stop the counters
376        new_time_total = update_proc_stat()
377        new_pid_stat = update_pidstat(client_process.pid)
378        if server_pid>0:
379            new_server_pid_stat = update_pidstat(server_pid)
380        ni,isize = get_iptables_INPUT_count()
381        no,osize = get_iptables_OUTPUT_count()
382        #[ni, isize, no, osize]
383        iptables_stat = {"packets in/s"         : ni,
384                         "packets in: bytes/s"  : isize,
385                         "packets out/s"        : no,
386                         "packets out: bytes/s" : osize}
387        #now collect the data
388        client_process_data = compute_stat("client", new_time_total-old_time_total, old_pid_stat, new_pid_stat)
389        if server_pid>0:
390            server_process_data = compute_stat("server", new_time_total-old_time_total, old_server_pid_stat, new_server_pid_stat)
391        else:
392            server_process_data = []
393        print("process_data (client/server): %s / %s" % (client_process_data, server_process_data))
394        print("input/output on tcp PORT %s: %s / %s packets, %s / %s KBytes" % (config.PORT, ni, no, isize, osize))
395        data = {}
396        data.update(iptables_stat)
397        data.update(stats)
398        data.update(client_process_data)
399        data.update(server_process_data)
400        return data
401    finally:
402        #stop the process
403        if client_process and client_process.poll() is None:
404            try_to_stop(client_process)
405            try_to_kill(client_process, 5)
406            code = client_process.poll()
407            assert code is not None, "failed to stop client!"
408
409def with_server(start_server_command, stop_server_commands, in_tests, get_stats_cb):
410    tests = in_tests[config.STARTING_TEST:config.LIMIT_TESTS]
411    print("going to run %s tests: %s" % (len(tests), [x[0] for x in tests]))
412    print("*******************************************")
413    print("ETA: %s minutes" % int((config.SERVER_SETTLE_TIME+config.DEFAULT_TEST_COMMAND_SETTLE_TIME+config.SETTLE_TIME+config.MEASURE_TIME+1)*len(tests)/60))
414    print("*******************************************")
415
416    server_process = None
417    test_command_process = None
418    env = {}
419    for k,v in os.environ.items():
420    #whitelist what we want to keep:
421        if k.startswith("XPRA") or k in ("LOGNAME", "XDG_RUNTIME_DIR", "USER", "HOME", "PATH", "LD_LIBRARY_PATH", "XAUTHORITY", "SHELL", "TERM", "USERNAME", "HOSTNAME", "PWD"):
422            env[k] = v
423    env["DISPLAY"] = ":%s" % config.DISPLAY_NO
424    errors = 0
425    results = []
426    count = 0
427    for name, tech_name, server_version, client_version, encoding, quality, speed, \
428        opengl, compression, encryption, ssh, (down,up,latency), test_command, client_cmd in tests:
429        try:
430            print("**************************************************************")
431            count += 1
432            test_command_settle_time = config.TEST_COMMAND_SETTLE_TIME.get(test_command[0], config.DEFAULT_TEST_COMMAND_SETTLE_TIME)
433            eta = int((config.SERVER_SETTLE_TIME+test_command_settle_time+config.SETTLE_TIME+config.MEASURE_TIME+1)*(len(tests)-count)/60)
434            print("%s/%s: %s            ETA=%s minutes" % (count, len(tests), name, eta))
435            test_command_process = None
436            try:
437                clean_sys_state()
438                #start the server:
439                if config.START_SERVER:
440                    print("starting server: %s" % str(start_server_command))
441                    server_process = subprocess.Popen(start_server_command, stdin=None)
442                    #give it time to settle down:
443                    t = config.SERVER_SETTLE_TIME
444                    if count==1:
445                        #first run, give it enough time to cleanup the socket
446                        t += 5
447                    time.sleep(t)
448                    server_pid = server_process.pid
449                    code = server_process.poll()
450                    assert code is None, "server failed to start, return code is %s, please ensure that you can run the server command line above and that a server does not already exist on that port or DISPLAY" % code
451                else:
452                    server_pid = 0
453
454                try:
455                    #start the test command:
456                    if config.USE_VIRTUALGL:
457                        if type(test_command)==str:
458                            cmd = config.VGLRUN_BIN + "-d "+os.environ.get("DISPLAY")+" -- "+ test_command
459                        elif type(test_command) in (list, tuple):
460                            cmd = [config.VGLRUN_BIN, "-d", os.environ.get("DISPLAY"), "--"] + list(test_command)
461                        else:
462                            raise Exception("invalid test command type: %s for %s" % (type(test_command), test_command))
463                    else:
464                        cmd = test_command
465
466                    print("starting test command: %s with env=%s, settle time=%s" % (cmd, env, test_command_settle_time))
467                    shell = type(cmd)==str
468                    test_command_process = subprocess.Popen(cmd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, shell=shell)
469
470                    if config.PREVENT_SLEEP:
471                        subprocess.Popen(config.PREVENT_SLEEP_COMMAND)
472
473                    time.sleep(test_command_settle_time)
474                    code = test_command_process.poll()
475                    assert code is None, "test command %s failed to start: exit code is %s" % (cmd, code)
476                    print("test command %s is running with pid=%s" % (cmd, test_command_process.pid))
477
478                    #run the client test
479                    data = {"Test Name"      : name,
480                            "Remoting Tech"  : tech_name,
481                            "Server Version" : server_version,
482                            "Client Version" : client_version,
483                            "Custom Params"  : config.CUSTOM_PARAMS,
484                            "SVN Version"    : SVN_VERSION,
485                            "Encoding"       : encoding,
486                            "Quality"        : quality,
487                            "Speed"          : speed,
488                            "OpenGL"         : opengl,
489                            "Test Command"   : get_command_name(test_command),
490                            "Sample Duration (s)"    : config.MEASURE_TIME,
491                            "Sample Time (epoch)"    : time.time(),
492                            "CPU info"       : CPU_INFO,
493                            "Platform"       : PLATFORM,
494                            "Kernel Version" : KERNEL_VERSION,
495                            "Xorg version"   : XORG_VERSION,
496                            "OpenGL"         : OPENGL_INFO,
497                            "Client Window Manager"  : WINDOW_MANAGER,
498                            "Screen Size"    : "%sx%s" % gdk.get_default_root_window().get_size(),
499                            "Compression"    : compression,
500                            "Encryption"     : encryption,
501                            "Connect via"    : ssh,
502                            "download limit (KB)"    : down,
503                            "upload limit (KB)"      : up,
504                            "latency (ms)"           : latency,
505                            }
506                    data.update(measure_client(server_pid, name, client_cmd, get_stats_cb))
507                    results.append([data.get(x, "") for x in HEADERS])
508                except Exception as e:
509                    import traceback
510                    traceback.print_exc()
511                    errors += 1
512                    print("error during client command run for %s: %s" % (name, e))
513                    if errors>config.MAX_ERRORS:
514                        print("too many errors, aborting tests")
515                        break
516            finally:
517                if test_command_process:
518                    print("stopping '%s' with pid=%s" % (test_command, test_command_process.pid))
519                    try_to_stop(test_command_process)
520                    try_to_kill(test_command_process, 2)
521                if config.START_SERVER:
522                    try_to_stop(server_process)
523                    time.sleep(2)
524                    for s in stop_server_commands:
525                        print("stopping server with: %s" % (s))
526                        try:
527                            stop_process = subprocess.Popen(s, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
528                            stop_process.wait()
529                        except Exception as e:
530                            print("error: %s" % e)
531                    try_to_kill(server_process, 5)
532                time.sleep(1)
533        except KeyboardInterrupt as e:
534            print("caught %s: stopping this series of tests" % e)
535            break
536    return results
537
538def trickle_command(down, up, latency):
539    if down<=0 and up<=0 and latency<=0:
540        return  []
541    cmd = [config.TRICKLE_BIN, "-s"]
542    if down>0:
543        cmd += ["-d", str(down)]
544    if up>0:
545        cmd += ["-u", str(up)]
546    if latency>0:
547        cmd += ["-L", str(latency)]
548    return cmd
549
550def trickle_str(down, up, latency):
551    if down<=0 and up<=0 and latency<=0:
552        return  "unthrottled"
553    s = "/".join(str(x) for x in [down,up,latency])
554    return "throttled:%s" % s
555
556def get_command_name(command_arg):
557    try:
558        name = config.TEST_NAMES.get(command_arg)
559        if name:
560            return  name
561    except:
562        pass
563    if type(command_arg)==list:
564        c = command_arg[0]              #["/usr/bin/xterm", "blah"] -> "/usr/bin/xterm"
565    else:
566        c = command_arg.split(" ")[0]   #"/usr/bin/xterm -e blah" -> "/usr/bin/xterm"
567    assert type(c)==str
568    return c.split("/")[-1]             #/usr/bin/xterm -> xterm
569
570def get_auth_args():
571    cmd = []
572    if config.XPRA_USE_PASSWORD:
573        cmd.append("--password-file=%s" % password_filename)
574        if XPRA_VERSION_NO>=[0, 14]:
575            cmd.append("--auth=file")
576        if XPRA_VERSION_NO>=[0, 15]:
577            cmd.append("--auth=none")
578            cmd.append("--tcp-auth=file")
579    return cmd
580
581def xpra_get_stats(initial_stats=None, all_stats=[]):
582    if XPRA_VERSION_NO<[0, 3]:
583        return  {}
584    info_cmd = XPRA_INFO_COMMAND[:] + get_auth_args()
585    out = getoutput(info_cmd)
586    if not out:
587        return  {}
588    #parse output:
589    d = {}
590    for line in out.splitlines():
591        parts = line.split("=")
592        if len(parts)==2:
593            d[parts[0]] = parts[1]
594    #functions for accessing the data:
595    def iget(names, default_value=""):
596        """ some of the fields got renamed, try both old and new names """
597        for n in names:
598            v = d.get(n)
599            if v is not None:
600                return int(v)
601        return default_value
602    #values always based on initial data only:
603    #(difference from initial value)
604    lookup = initial_stats or {}
605    initial_input_packetcount  = lookup.get("Application packets in/s", 0)
606    initial_input_bytecount    = lookup.get("Application bytes in/s", 0)
607    initial_output_packetcount = lookup.get("Application packets out/s", 0)
608    initial_output_bytecount   = lookup.get("Application bytes out/s", 0)
609    initial_mmap_bytes         = lookup.get("mmap bytes/s", 0)
610    data = {
611            "Application packets in/s"      : (iget(["client.connection.input.packetcount", "input_packetcount"], 0)-initial_input_packetcount)/config.MEASURE_TIME,
612            "Application bytes in/s"        : (iget(["client.connection.input.bytecount", "input_bytecount"], 0)-initial_input_bytecount)/config.MEASURE_TIME,
613            "Application packets out/s"     : (iget(["client.connection.output.packetcount", "output_packetcount"], 0)-initial_output_packetcount)/config.MEASURE_TIME,
614            "Application bytes out/s"       : (iget(["client.connection.output.bytecount", "output_bytecount"], 0)-initial_output_bytecount)/config.MEASURE_TIME,
615            "mmap bytes/s"                  : (iget(["client.connection.output.mmap_bytecount", "output_mmap_bytecount"], 0)-initial_mmap_bytes)/config.MEASURE_TIME,
616            }
617
618    #values that are averages or min/max:
619    def add(prefix, op, name, prop_names):
620        values = []
621        #cook the property names using the lowercase prefix if needed
622        #(all xpra info properties are lowercase):
623        actual_prop_names = []
624        full_search = []
625        for prop_name in prop_names:
626            if prop_name.find("%s")>=0:
627                prop_name = prop_name % prefix.lower()
628            actual_prop_names.append(prop_name)
629            if prop_name.find("*")>=0 or prop_name.find("+")>=0:        #ie: "window\[\d+\].encoding.quality.avg"
630                #make it a proper python regex:
631                full_search.append(prop_name)
632        if len(full_search)>0:
633            for s in full_search:
634                regex = re.compile(s)
635                matches = [d.get(x) for x in d.keys() if regex.match(x)]
636                for v in matches:
637                    values.append(int(v))
638            #print("add(%s, %s, %s, %s) values from full_search=%s: %s" % (prefix, op, name, prop_names, full_search, values))
639        else:
640            #match just one record:
641            values.append(iget(actual_prop_names))
642            #print("add(%s, %s, %s, %s) values from iget: %s" % (prefix, op, name, prop_names, values))
643        #this is the stat property name:
644        full_name = name                            #ie: "Application packets in/s"
645        if prefix:
646            full_name = prefix+" "+name             #ie: "Min" + " " + "Batch Delay"
647        for s in all_stats:                         #add all previously found values to list
648            values.append(s.get(full_name))
649        #strip missing values:
650        values = [x for x in values if x is not None and x!=""]
651        if len(values)>0:
652            v = op(values)                          #ie: avg([4,5,4]) or max([4,5,4])
653            #print("%s: %s(%s)=%s" % (full_name, op, values, v))
654            data[full_name] = v
655
656    def avg(l):
657        return sum(l)/len(l)
658
659    add("", avg, "Regions/s",                       ["encoding.regions_per_second", "regions_per_second"])
660    add("", avg, "Pixels/s Sent",                   ["encoding.pixels_per_second", "pixels_per_second"])
661    add("", avg, "Encoding Pixels/s",               ["encoding.pixels_encoded_per_second", "pixels_encoded_per_second"])
662    add("", avg, "Decoding Pixels/s",               ["encoding.pixels_decoded_per_second", "pixels_decoded_per_second"])
663
664    for prefix, op in (("Min", min), ("Max", max), ("Avg", avg)):
665        add(prefix, op, "Batch Delay (ms)",         ["batch.delay.%s", "batch_delay.%s", "%s_batch_delay"])
666        add(prefix, op, "Actual Batch Delay (ms)",  ["batch.actual_delay.%s"])
667        add(prefix, op, "Client Latency (ms)",      ["client.latency.%s", "client_latency.%s", "%s_client_latency"])
668        add(prefix, op, "Client Ping Latency (ms)", ["client.ping_latency.%s", "client_ping_latency.%s"])
669        add(prefix, op, "Server Ping Latency (ms)", ["server.ping_latency.%s", "server_ping_latency.%s", "server_latency.%s", "%s_server_latency"])
670        add(prefix, op, "Damage Latency (ms)",      ["damage.in_latency.%s", "damage_in_latency.%s"])
671
672        add(prefix, op, "Quality",                  ["^window\[\d+\].encoding.quality.%s$"])
673        add(prefix, op, "Speed",                    ["^window\[\d+\].encoding.speed.%s$"])
674
675    def addset(name, prop_name):
676        regex = re.compile(prop_name)
677        def getdictvalues(from_dict):
678            return [from_dict.get(x) for x in from_dict.keys() if regex.match(x)]
679        values = getdictvalues(d)
680        for s in all_stats:                         #add all previously found values to list
681            values += getdictvalues(s)
682        data[name] = list(set(values))
683
684    #video encoder
685    addset("Video Encoder", "^window\[\d+\].encoder$")
686    #record CSC:
687    addset("CSC", "^window\[\d+\].csc$")
688    addset("CSC Mode", "^window\[\d+\].csc.dst_format$")
689    addset("Scaling", "^window\[\d+\].scaling$")
690    #packet layer:
691    addset("Compressors", "connection.compression$")
692    addset("Packet Encoders", "connection.encoder$")
693    #add this record to the list:
694    all_stats.append(data)
695    return data
696
697def get_xpra_start_server_command():
698    cmd = [XPRA_BIN, "--no-daemon", "--bind-tcp=0.0.0.0:%s" % config.PORT]
699    if config.XPRA_FORCE_XDUMMY:
700        cmd.append("--xvfb=%s -nolisten tcp +extension GLX +extension RANDR +extension RENDER -logfile %s -config %s" % (config.XORG_BIN, config.XORG_LOG, config.XORG_CONFIG))
701    if XPRA_VERSION_NO>=[0, 5]:
702        cmd.append("--no-notifications")
703    cmd += get_auth_args()
704    cmd.append("--no-pulseaudio")
705    # NOTE: This is added for testing
706    cmd.append("-d dbus")
707    cmd += ["start", ":%s" % config.DISPLAY_NO]
708    return cmd
709
710def test_xpra():
711    print("")
712    print("*********************************************************")
713    print("                Xpra tests")
714    print("")
715    tests = []
716    for connect_option, encryption in config.XPRA_CONNECT_OPTIONS:
717        shaping_options = config.TRICKLE_SHAPING_OPTIONS
718        if connect_option=="unix-domain":
719            shaping_options = [config.NO_SHAPING]
720        for down,up,latency in shaping_options:
721            for x11_test_command in config.X11_TEST_COMMANDS:
722                for encoding in config.XPRA_TEST_ENCODINGS:
723                    if XPRA_VERSION_NO>=[0, 10]:
724                        opengl_options = config.XPRA_OPENGL_OPTIONS.get(encoding, [True])
725                    elif XPRA_VERSION_NO>=[0, 9]:
726                        opengl_options = config.XPRA_OPENGL_OPTIONS.get(encoding, [False])
727                    else:
728                        opengl_options = [False]
729                    for opengl in opengl_options:
730                        quality_options = config.XPRA_ENCODING_QUALITY_OPTIONS.get(encoding, [-1])
731                        for quality in quality_options:
732                            speed_options = config.XPRA_ENCODING_SPEED_OPTIONS.get(encoding, [-1])
733                            for speed in speed_options:
734                                for speaker in XPRA_SPEAKER_OPTIONS:
735                                    for mic in XPRA_MICROPHONE_OPTIONS:
736                                        comp_options = [None]
737                                        if XPRA_VERSION_NO>=[0, 13]:
738                                            comp_options = config.XPRA_COMPRESSORS_OPTIONS
739                                        for comp in comp_options:
740                                            comp_level_options = config.XPRA_COMPRESSION_LEVEL_OPTIONS
741                                            for compression in comp_level_options:
742                                                packet_encoders_options = [None]
743                                                if XPRA_VERSION_NO>=[0, 14]:
744                                                    packet_encoders_options = config.XPRA_PACKET_ENCODERS_OPTIONS
745                                                for packet_encoders in packet_encoders_options:
746                                                    cmd = trickle_command(down, up, latency)
747                                                    cmd += [XPRA_BIN, "attach"]
748                                                    if connect_option=="ssh":
749                                                        cmd.append("ssh:%s:%s" % (config.IP, config.DISPLAY_NO))
750                                                    elif connect_option=="tcp":
751                                                        cmd.append("tcp:%s:%s" % (config.IP, config.PORT))
752                                                    else:
753                                                        cmd.append(":%s" % (config.DISPLAY_NO))
754                                                    if XPRA_VERSION_NO>=[0, 15]:
755                                                        cmd.append("--readonly=yes")
756                                                    else:
757                                                        cmd.append("--readonly")
758                                                    cmd += get_auth_args()
759                                                    if packet_encoders:
760                                                        cmd += ["--packet-encoders=%s" % packet_encoders]
761                                                    if comp:
762                                                        cmd += ["--compressors=%s" % comp]
763                                                    if compression is not None:
764                                                        cmd += ["-z", str(compression)]
765                                                    if XPRA_VERSION_NO>=[0, 3]:
766                                                        cmd.append("--enable-pings")
767                                                        cmd.append("--no-clipboard")
768                                                    if XPRA_VERSION_NO>=[0, 5]:
769                                                        cmd.append("--no-bell")
770                                                        cmd.append("--no-cursors")
771                                                        cmd.append("--no-notifications")
772                                                    if XPRA_VERSION_NO>=[0, 12]:
773                                                        if config.XPRA_MDNS:
774                                                            cmd.append("--mdns")
775                                                        else:
776                                                            cmd.append("--no-mdns")
777                                                    if XPRA_VERSION_NO>=[0, 8] and encryption:
778                                                        cmd.append("--encryption=%s" % encryption)
779                                                    if speed>=0:
780                                                        cmd.append("--speed=%s" % speed)
781                                                    if quality>=0:
782                                                        if XPRA_VERSION_NO>=[0, 7]:
783                                                            cmd.append("--quality=%s" % quality)
784                                                        else:
785                                                            cmd.append("--jpeg-quality=%s" % quality)
786                                                        name = "%s-%s" % (encoding, quality)
787                                                    else:
788                                                        name = encoding
789                                                    if speaker is None:
790                                                        if XPRA_VERSION_NO>=[0, 8]:
791                                                            cmd.append("--no-speaker")
792                                                    else:
793                                                        cmd.append("--speaker-codec=%s" % speaker)
794                                                    if mic is None:
795                                                        if XPRA_VERSION_NO>=[0, 8]:
796                                                            cmd.append("--no-microphone")
797                                                    else:
798                                                        cmd.append("--microphone-codec=%s" % mic)
799                                                    if encoding!="mmap":
800                                                        cmd.append("--no-mmap")
801                                                        cmd.append("--encoding=%s" % encoding)
802                                                    if XPRA_VERSION_NO>=[0, 9]:
803                                                        cmd.append("--opengl=%s" % opengl)
804                                                    command_name = get_command_name(x11_test_command)
805                                                    test_name = "%s (%s - %s - %s - %s - via %s)" % \
806                                                        (name, command_name, compression, encryption, trickle_str(down, up, latency), connect_option)
807                                                    tests.append((test_name, "xpra", XPRA_VERSION, XPRA_VERSION, \
808                                                                  encoding, quality, speed,
809                                                                  opengl, compression, encryption, connect_option, \
810                                                                  (down,up,latency), x11_test_command, cmd))
811    return with_server(get_xpra_start_server_command(), XPRA_SERVER_STOP_COMMANDS, tests, xpra_get_stats)
812
813def get_x11_client_window_info(display, *app_name_strings):
814    env = os.environ.copy()
815    if display:
816        env["DISPLAY"] = display
817    wininfo = getoutput(["xwininfo", "-root", "-tree"], env)
818    for line in wininfo.splitlines():
819        if not line:
820            continue
821        found = True
822        for x in app_name_strings:
823            if not line.find(x)>=0:
824                found = False
825                break
826        if not found:
827            continue
828        parts = line.split()
829        if not parts[0].startswith("0x"):
830            continue
831        #found a window which matches the name we are looking for!
832        wid = parts[0]
833        x, y, w, h = 0, 0, 0, 0
834        dims = parts[-2]        #ie: 400x300+20+10
835        dp = dims.split("+")    #["400x300", "20", "10"]
836        if len(dp)==3:
837            d = dp[0]           #"400x300"
838            x = int(dp[1])      #20
839            y = int(dp[2])      #10
840            wh = d.split("x")   #["400", "300"]
841            if len(wh)==2:
842                w = int(wh[0])  #400
843                h = int(wh[1])  #300
844        print("Found window for '%s': %s - %sx%s" % (app_name_strings, wid, w, h))
845        return  wid, x, y, w, h
846    return  None
847
848def get_vnc_stats(initial_stats=None, all_stats=[]):
849    #print("get_vnc_stats(%s)" % last_record)
850    if initial_stats==None:
851        #this is the initial call,
852        #start the thread to watch the output of tcbench
853        #we first need to figure out the dimensions of the client window
854        #within the Xvnc server, the use those dimensions to tell tcbench
855        #where to look in the vncviewer client window
856        test_window_info = get_x11_client_window_info(":%s" % config.DISPLAY_NO)
857        print("info for client test window: %s" % str(test_window_info))
858        info = get_x11_client_window_info(None, "TigerVNC: x11", "Vncviewer")
859        if not info:
860            return  {}
861        print("info for TigerVNC: %s" % str(info))
862        wid, _, _, w, h = info
863        if not wid:
864            return  {}
865        if test_window_info:
866            _, _, _, w, h = test_window_info
867        command = [config.TCBENCH, "-wh%s" % wid, "-t%s" % (config.MEASURE_TIME-5)]
868        if w>0 and h>0:
869            command.append("-x%s" % int(w/2))
870            command.append("-y%s" % int(h/2))
871        if os.path.exists(config.TCBENCH_LOG):
872            os.unlink(config.TCBENCH_LOG)
873        tcbench_log  = open(config.TCBENCH_LOG, 'w')
874        try:
875            print("tcbench starting: %s, logging to %s" % (command, config.TCBENCH_LOG))
876            proc = subprocess.Popen(command, stdin=None, stdout=tcbench_log, stderr=tcbench_log)
877            return {"tcbench" : proc}
878        except Exception as e:
879            import traceback
880            traceback.print_exc()
881            print("error running %s: %s" % (command, e))
882        return  {}           #we failed...
883    regions_s = ""
884    if "tcbench" in initial_stats:
885        #found the process watcher,
886        #parse the tcbench output and look for frames/sec:
887        process = initial_stats.get("tcbench")
888        assert type(process)==subprocess.Popen
889        #print("get_vnc_stats(%s) process.poll()=%s" % (last_record, process.poll()))
890        if process.poll() is None:
891            try_to_stop(process)
892            try_to_kill(process, 2)
893        else:
894            with open(config.TCBENCH_LOG, mode='rb') as f:
895                out = f.read()
896            #print("get_vnc_stats(%s) tcbench output=%s" % (last_record, out))
897            for line in out.splitlines():
898                if not line.find("Frames/sec:")>=0:
899                    continue
900                parts = line.split()
901                regions_s = parts[-1]
902                print("Frames/sec=%s" % regions_s)
903    return {
904            "Regions/s"                     : regions_s,
905           }
906
907def test_vnc():
908    print("")
909    print("*********************************************************")
910    print("                VNC tests")
911    print("")
912    tests = []
913    for down,up,latency in config.TRICKLE_SHAPING_OPTIONS:
914        for x11_test_command in config.X11_TEST_COMMANDS:
915            for encoding in config.VNC_ENCODINGS:
916                for zlib in config.VNC_ZLIB_OPTIONS:
917                    for compression in config.VNC_COMPRESSION_OPTIONS:
918                        jpeg_quality = [8]
919                        if encoding=="Tight":
920                            jpeg_quality = config.VNC_JPEG_OPTIONS
921                        for jpegq in jpeg_quality:
922                            cmd = trickle_command(down, up, latency)
923                            cmd += [config.VNCVIEWER_BIN, "%s::%s" % (config.IP, config.PORT),
924                                   "--ViewOnly",
925                                   "--ZlibLevel=%s" % str(zlib),
926                                   "--CompressLevel=%s" % str(compression),
927                                   ]
928                            if encoding=="auto":
929                                cmd.append("--AutoSelect=1")
930                            else:
931                                cmd.append("--AutoSelect=0")
932                                cmd.append("--PreferredEncoding=%s" % encoding)
933                            if jpegq<0:
934                                cmd.append("--NoJPEG=1")
935                                jpegtxt = "nojpeg"
936                            else:
937                                cmd.append("--NoJPEG=0")
938                                cmd.append("--QualityLevel=%s" % jpegq)
939                                jpegtxt = "jpeg=%s" % jpegq
940                            #make a descriptive title:
941                            if zlib==-1:
942                                zlibtxt = "nozlib"
943                            else:
944                                zlibtxt = "zlib=%s" % zlib
945                            command_name = get_command_name(x11_test_command)
946                            test_name = "vnc (%s - %s - %s - compression=%s - %s - %s)" % \
947                                        (command_name, encoding, zlibtxt, compression, jpegtxt, trickle_str(down, up, latency))
948                            tests.append((test_name, "vnc", XVNC_VERSION, VNCVIEWER_VERSION, \
949                                          encoding, False, compression, None, False, \
950                                          (down,up,latency), x11_test_command, cmd))
951    return with_server(config.XVNC_SERVER_START_COMMAND, config.XVNC_SERVER_STOP_COMMANDS, tests, get_vnc_stats)
952
953def main():
954    #before doing anything, check that the firewall is setup correctly:
955    get_iptables_INPUT_count()
956    get_iptables_OUTPUT_count()
957
958    #If CUSTOM_PARAMS are supplied on the command line, they override what's in config
959    if (len(sys.argv) > 3):
960        config.CUSTOM_PARAMS = " ".join(sys.argv[3:])
961    config.print_options()
962
963    xpra_results = []
964    if config.TEST_XPRA:
965        xpra_results = test_xpra()
966    vnc_results = []
967    if config.TEST_VNC:
968        vnc_results = test_vnc()
969
970    if (len(sys.argv) > 2):
971        csv_name = sys.argv[2]
972    else:
973        csv_name = None
974
975    print("*"*80)
976    print("RESULTS:")
977    print("")
978
979    out_lines = []
980    out_line = ", ".join(HEADERS)
981    print out_line
982    out_lines.append(out_line)
983
984    def s(x):
985        if x is None:
986            return ""
987        elif type(x) in (list, tuple, set):
988            return '"' + (", ".join(list(x))) + '"'
989        elif type(x) in (unicode, str):
990            if len(x)==0:
991                return ""
992            return '"%s"' % x
993        elif type(x) in (float, long, int):
994            return str(x)
995        else:
996            return "unhandled-type: %s" % type(x)
997
998    for result in xpra_results+vnc_results:
999        out_line = ", ".join([s(x) for x in result])
1000        print(out_line)
1001        out_lines.append(out_line)
1002
1003    if (csv_name != None):
1004        with open(csv_name, "w") as csv:
1005            for line in out_lines:
1006                csv.write(line+"\n")
1007
1008if __name__ == "__main__":
1009    main()