52a68eb24a
Add --perf-output=<filename> option to gnome-shell that combines the reports written for each run by the C/Javascript code into a complete report. If this option is not specified, a brief human-readable summary is printed to stdout instead. https://bugzilla.gnome.org/show_bug.cgi?id=618189
589 lines
20 KiB
Plaintext
589 lines
20 KiB
Plaintext
#!@PYTHON@
|
|
# -*- mode: Python; indent-tabs-mode: nil; -*-
|
|
|
|
import atexit
|
|
import datetime
|
|
import json
|
|
import optparse
|
|
import os
|
|
import random
|
|
import re
|
|
import shutil
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import termios
|
|
import time
|
|
import errno
|
|
|
|
def show_version(option, opt_str, value, parser):
|
|
print "GNOME Shell @VERSION@"
|
|
sys.exit()
|
|
|
|
def get_running_session_environs():
|
|
wanted_environment = ['DBUS_SESSION_BUS_ADDRESS', 'DISPLAY', 'XDG_DATA_DIRS',
|
|
'XAUTHORITY', 'XDG_SESSION_COOKIE', 'ORBIT_SOCKETDIR',
|
|
'SESSION_MANAGER']
|
|
num_re = re.compile('^[0-9]+$')
|
|
myuid = os.getuid()
|
|
if not os.path.isdir('/proc'):
|
|
return {}
|
|
for filename in os.listdir('/proc'):
|
|
if not num_re.match(filename):
|
|
continue
|
|
piddir = '/proc/' + filename
|
|
try:
|
|
stat = os.stat(piddir)
|
|
except OSError, e:
|
|
continue
|
|
if not stat.st_uid == myuid:
|
|
continue
|
|
try:
|
|
f = open(piddir + "/cmdline")
|
|
command = f.read()
|
|
f.close()
|
|
except IOError, e:
|
|
continue
|
|
# /proc/cmdline is separated and terminated by NULs
|
|
command = command.split("\x00")[0]
|
|
command = os.path.basename(command)
|
|
if command != 'gnome-session':
|
|
continue
|
|
try:
|
|
f = open(os.path.join(piddir, 'environ'))
|
|
except OSError, e:
|
|
continue
|
|
environ_data = f.read()
|
|
f.close()
|
|
# There's a trailing null at the last one, so remove the
|
|
# empty string
|
|
environs = environ_data.split('\0')[:-1]
|
|
# Rumor has it the presence of just FOO (instead of FOO=bar)
|
|
# represents a deleted environment variable
|
|
environs = filter(lambda x: '=' in x, environs)
|
|
# Turn it into a dictionary
|
|
environs = dict(map(lambda x: x.split('=', 1), environs))
|
|
result = {}
|
|
for key in wanted_environment:
|
|
if key in environs:
|
|
result[key] = environs[key]
|
|
return result
|
|
|
|
def start_xephyr():
|
|
tmpdir = tempfile.mkdtemp("", "gnome-shell.")
|
|
atexit.register(shutil.rmtree, tmpdir)
|
|
|
|
display = ":" + str(random.randint(10, 99))
|
|
xauth_file = os.path.join(tmpdir, "database")
|
|
|
|
# Create a random 128-bit key and format it as hex
|
|
f = open("/dev/urandom", "r")
|
|
key = f.read(16)
|
|
f.close()
|
|
hexkey = "".join(("%02x" % ord(byte) for byte in key))
|
|
|
|
# Store that in an xauthority file as the key for connecting to our Xephyr
|
|
retcode = subprocess.call(["xauth",
|
|
"-f", xauth_file,
|
|
"add", display, "MIT-MAGIC-COOKIE-1", hexkey])
|
|
if retcode != 0:
|
|
raise RuntimeError("xauth failed")
|
|
|
|
# Launch Xephyr
|
|
try:
|
|
xephyr = subprocess.Popen(["Xephyr", display,
|
|
"-auth", xauth_file,
|
|
"-screen", options.geometry,
|
|
"-host-cursor"])
|
|
except OSError, e:
|
|
if e.errno == errno.ENOENT:
|
|
print "Could not find Xephyr."
|
|
sys.exit(1)
|
|
else:
|
|
raise
|
|
os.environ['DISPLAY'] = display
|
|
os.environ['XAUTHORITY'] = xauth_file
|
|
|
|
# Wait for server to get going: LAME
|
|
time.sleep(1)
|
|
|
|
# Start some windows in our session.
|
|
subprocess.Popen(["gnome-terminal"])
|
|
|
|
return xephyr;
|
|
|
|
GLXINFO_RE = re.compile(r"^(\S.*):\s*\n((?:^\s+.*\n)*)", re.MULTILINE)
|
|
|
|
def _get_glx_extensions():
|
|
"""Return a tuple of server, client, and effective GLX extensions"""
|
|
|
|
glxinfo = subprocess.Popen(["glxinfo"], stdout=subprocess.PIPE)
|
|
glxinfo_output = glxinfo.communicate()[0]
|
|
glxinfo.wait()
|
|
|
|
glxinfo_map = {}
|
|
for m in GLXINFO_RE.finditer(glxinfo_output):
|
|
glxinfo_map[m.group(1)] = m.group(2)
|
|
|
|
server_glx_extensions = set(re.split("\s*,\s*", glxinfo_map['server glx extensions'].strip()))
|
|
client_glx_extensions = set(re.split("\s*,\s*", glxinfo_map['client glx extensions'].strip()))
|
|
glx_extensions = set(re.split("\s*,\s*", glxinfo_map['GLX extensions'].strip()))
|
|
|
|
return (server_glx_extensions, client_glx_extensions, glx_extensions)
|
|
|
|
def start_shell(perf_output=None):
|
|
bin_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
if os.path.exists(os.path.join(bin_dir, 'gnome-shell.in')):
|
|
running_from_source_tree = True
|
|
top_dir = os.path.dirname(bin_dir)
|
|
plugin = os.path.join(top_dir, 'src', 'libgnome-shell.la')
|
|
typelib_dir = os.path.join(top_dir, "src")
|
|
js_dir = os.path.join(top_dir, "js")
|
|
data_dir = os.path.join(top_dir, "data")
|
|
else:
|
|
running_from_source_tree = False
|
|
plugin = 'libgnome-shell'
|
|
js_dir = os.path.join('@pkgdatadir@', 'js')
|
|
|
|
# Set up environment
|
|
env = dict(os.environ)
|
|
env.update({'GNOME_SHELL_JS' : '@GJS_JS_DIR@:@GJS_JS_NATIVE_DIR@:' + js_dir,
|
|
'PATH' : '@MUTTER_BIN_DIR@:' + os.environ.get('PATH', ''),
|
|
'GNOME_DISABLE_CRASH_DIALOG' : '1'})
|
|
|
|
if running_from_source_tree:
|
|
env.update({'GNOME_SHELL_DATADIR' : data_dir,
|
|
'GI_TYPELIB_PATH' : typelib_dir})
|
|
|
|
jhbuild_gconf_source = os.path.join('@sysconfdir@', 'gconf/2/path.jhbuild')
|
|
if os.path.exists(jhbuild_gconf_source):
|
|
env['GCONF_DEFAULT_SOURCE_PATH'] = jhbuild_gconf_source
|
|
|
|
# Work around Ubuntu xulrunner bug,
|
|
# http://bugzilla.gnome.org/show_bug.cgi?id=573413
|
|
pkgconfig = subprocess.Popen(['pkg-config', '--variable=sdkdir', 'mozilla-js'],
|
|
stdout=subprocess.PIPE)
|
|
mozjs_sdkdir = pkgconfig.communicate()[0].strip()
|
|
pkgconfig.wait()
|
|
if pkgconfig.returncode == 0:
|
|
mozjs_libdir = re.sub('-(sdk|devel)', '', mozjs_sdkdir)
|
|
if os.path.exists(mozjs_libdir + '/libmozjs.so'):
|
|
env['LD_LIBRARY_PATH'] = os.environ.get('LD_LIBRARY_PATH', '') + ':' + mozjs_libdir
|
|
|
|
# Log everything to stderr (make stderr our "log file")
|
|
env['GJS_DEBUG_OUTPUT'] = 'stderr'
|
|
|
|
if not options.verbose:
|
|
# Unless verbose() is specified, only let gjs show errors and
|
|
# things that are explicitly logged via log() from javascript
|
|
env['GJS_DEBUG_TOPICS'] = 'JS ERROR;JS LOG'
|
|
|
|
if use_tfp:
|
|
# Decide if we need to set LIBGL_ALWAYS_INDIRECT=1 to get the
|
|
# texture_from_pixmap extension; we take having the extension
|
|
# be supported on both the client and server but not in the
|
|
# list of effective extensions as a signal of needing to force
|
|
# indirect rendering.
|
|
#
|
|
# Note that this check would give the wrong answer for Xephyr,
|
|
# but since we force !use_tfp there anyway, it doesn't matter.
|
|
(server_glx_extensions, client_glx_extensions, glx_extensions) = _get_glx_extensions()
|
|
|
|
if ("GLX_EXT_texture_from_pixmap" in server_glx_extensions and
|
|
"GLX_EXT_texture_from_pixmap" in client_glx_extensions and
|
|
(not "GLX_EXT_texture_from_pixmap" in glx_extensions)):
|
|
if options.verbose:
|
|
print "Forcing indirect GL"
|
|
# This is Mesa specific; the NVIDIA proprietary drivers
|
|
# drivers use __GL_FORCE_INDIRECT=1 instead. But we don't
|
|
# need to force indirect rendering for NVIDIA.
|
|
env['LIBGL_ALWAYS_INDIRECT'] = '1'
|
|
|
|
if options.perf is not None:
|
|
env['SHELL_PERF_MODULE'] = options.perf
|
|
|
|
if perf_output is not None:
|
|
env['SHELL_PERF_OUTPUT'] = perf_output
|
|
|
|
if options.debug:
|
|
debug_command = options.debug_command.split()
|
|
args = list(debug_command)
|
|
else:
|
|
args = []
|
|
|
|
args.extend(['mutter', '--mutter-plugins=' + plugin])
|
|
if options.replace:
|
|
args.append('--replace')
|
|
if options.sync:
|
|
args.append('--sync')
|
|
return subprocess.Popen(args, env=env)
|
|
|
|
def run_shell(perf_output=None):
|
|
if options.debug:
|
|
# Record initial terminal state so we can reset it to that
|
|
# later, in case we kill gdb at a bad time
|
|
termattrs = termios.tcgetattr(0);
|
|
|
|
normal_exit = False
|
|
|
|
if options.verbose:
|
|
print "Starting shell"
|
|
|
|
try:
|
|
shell = None
|
|
if options.xephyr:
|
|
xephyr = start_xephyr()
|
|
# This makes us not grab the org.gnome.Panel or
|
|
# org.freedesktop.Notifications D-Bus names
|
|
os.environ['GNOME_SHELL_NO_REPLACE'] = '1'
|
|
shell = start_shell()
|
|
else:
|
|
xephyr = None
|
|
shell = start_shell(perf_output=perf_output)
|
|
|
|
# Wait for shell to exit
|
|
if options.verbose:
|
|
print "Waiting for shell to exit"
|
|
shell.wait()
|
|
|
|
except KeyboardInterrupt, e:
|
|
try:
|
|
os.kill(shell.pid, signal.SIGKILL)
|
|
except:
|
|
pass
|
|
shell.wait()
|
|
finally:
|
|
# Clean up Xephyr if it outlived the shell
|
|
if xephyr:
|
|
try:
|
|
os.kill(xephyr.pid, signal.SIGKILL)
|
|
except OSError:
|
|
pass
|
|
|
|
if shell is None:
|
|
print "Failed to start shell"
|
|
elif shell.returncode == 0:
|
|
normal_exit = True
|
|
if options.verbose:
|
|
print "Shell exited normally"
|
|
elif shell.returncode < 0:
|
|
# Python has no mapping for strsignal; not worth using
|
|
# ctypes for this.
|
|
print "Shell killed with signal %d" % - shell.returncode
|
|
else:
|
|
# Normal reason here would be losing connection the X server
|
|
if options.verbose:
|
|
print "Shell exited with return code %d" % shell.returncode
|
|
|
|
if options.debug:
|
|
termios.tcsetattr(0, termios.TCSANOW, termattrs);
|
|
|
|
return normal_exit
|
|
|
|
def run_performance_test():
|
|
iters = options.perf_iters
|
|
if options.perf_warmup:
|
|
iters += 1
|
|
|
|
logs = []
|
|
metric_summaries = {}
|
|
|
|
for i in xrange(0, iters):
|
|
# We create an empty temporary file that the shell will overwrite
|
|
# with the contents.
|
|
handle, output_file = tempfile.mkstemp(".json", "gnome-shell-perf.")
|
|
os.close(handle)
|
|
|
|
# Run the performance test and collect the output as JSON
|
|
normal_exit = False
|
|
try:
|
|
normal_exit = run_shell(perf_output=output_file)
|
|
finally:
|
|
if not normal_exit:
|
|
os.remove(output_file)
|
|
|
|
if not normal_exit:
|
|
return False
|
|
|
|
try:
|
|
f = open(output_file)
|
|
output = json.load(f)
|
|
f.close()
|
|
finally:
|
|
os.remove(output_file)
|
|
|
|
# Grab the event definitions the first time around
|
|
if i == 0:
|
|
events = output['events']
|
|
|
|
if options.perf_warmup and i == 0:
|
|
continue
|
|
|
|
for metric in output['metrics']:
|
|
name = metric['name']
|
|
if not name in metric_summaries:
|
|
summary = {}
|
|
summary['description'] = metric['description']
|
|
summary['values'] = []
|
|
metric_summaries[name] = summary
|
|
else:
|
|
summary = metric_summaries[name]
|
|
|
|
summary['values'].append(metric['value'])
|
|
|
|
logs.append(output['log'])
|
|
|
|
if options.perf_output:
|
|
# Write a complete report, formatted as JSON. The Javascript/C code that
|
|
# generates the individual reports we are summarizing here is very careful
|
|
# to format them nicely, but we just dump out a compressed no-whitespace
|
|
# version here for simplicity. Using json.dump(indent=0) doesn't real
|
|
# improve the readability of the output much.
|
|
report = {
|
|
'date': datetime.datetime.now().isoformat(),
|
|
'events': events,
|
|
'metrics': metric_summaries,
|
|
'logs': logs
|
|
}
|
|
|
|
# Add the Git revision if available
|
|
bin_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
if os.path.exists(os.path.join(bin_dir, 'gnome-shell.in')):
|
|
top_dir = os.path.dirname(bin_dir)
|
|
git_dir = os.path.join(top_dir, '.git')
|
|
if os.path.exists(git_dir):
|
|
env = dict(os.environ)
|
|
env['GIT_DIR'] = git_dir
|
|
revision = subprocess.Popen(['git', 'rev-parse', 'HEAD'],
|
|
env=env,
|
|
stdout=subprocess.PIPE).communicate()[0].strip()
|
|
report['revision'] = revision
|
|
|
|
f = open(options.perf_output, 'w')
|
|
json.dump(report, f)
|
|
f.close()
|
|
else:
|
|
# Write a human readable summary
|
|
print '------------------------------------------------------------';
|
|
for metric in sorted(metric_summaries.keys()):
|
|
summary = metric_summaries[metric]
|
|
print "#", summary['description']
|
|
print metric, ", ".join((str(x) for x in summary['values']))
|
|
print '------------------------------------------------------------';
|
|
|
|
return True
|
|
|
|
def restore_gnome():
|
|
# Do imports lazily to save time and memory
|
|
import gio
|
|
import gconf
|
|
|
|
# We don't want to start the new gnome-panel in the current
|
|
# directory; $HOME is better for stuff launched from it
|
|
os.chdir(os.path.expanduser("~"))
|
|
|
|
def launch_component(gconf_path):
|
|
client = gconf.client_get_default()
|
|
component = client.get_string(gconf_path)
|
|
|
|
if component == None or component == "":
|
|
return
|
|
|
|
# See gnome-session/gsm-util.c:gsm_util_find_desktop_file_for_app_name()
|
|
# The one difference is that we don't search the autostart directories,
|
|
# and just search normal application search path. (Gio doesnt' know
|
|
# how to search the autostart dirs, so we'd have to do that ourselves.)
|
|
appinfo = None
|
|
try:
|
|
appinfo = gio.unix.DesktopAppInfo(component + ".desktop")
|
|
except:
|
|
try:
|
|
appinfo = gio.unix.DesktopAppInfo("gnome-" + component + ".desktop")
|
|
except:
|
|
pass
|
|
|
|
if appinfo:
|
|
appinfo.launch()
|
|
|
|
launch_component("/desktop/gnome/session/required_components/windowmanager")
|
|
launch_component("/desktop/gnome/session/required_components/panel")
|
|
|
|
# Main program
|
|
|
|
parser = optparse.OptionParser()
|
|
parser.add_option("-r", "--replace", action="store_true",
|
|
help="Replace the running metacity/gnome-panel")
|
|
parser.add_option("-g", "--debug", action="store_true",
|
|
help="Run under a debugger")
|
|
parser.add_option("", "--debug-command", metavar="COMMAND",
|
|
help="Command to use for debugging (defaults to 'gdb --args')")
|
|
parser.add_option("-v", "--verbose", action="store_true")
|
|
parser.add_option("", "--sync", action="store_true")
|
|
parser.add_option("", "--perf", metavar="PERF_MODULE",
|
|
help="Specify the name of a performance module to run")
|
|
parser.add_option("", "--perf-iters", type="int", metavar="ITERS",
|
|
help="Numbers of iterations of performance module to run",
|
|
default=1)
|
|
parser.add_option("", "--perf-warmup", action="store_true",
|
|
help="Run a dry run before performance tests")
|
|
parser.add_option("", "--perf-output",
|
|
help="Output file to write performance report")
|
|
parser.add_option("", "--xephyr", action="store_true",
|
|
help="Run a debugging instance inside Xephyr")
|
|
parser.add_option("", "--geometry", metavar="GEOMETRY",
|
|
help="Specify Xephyr screen geometry",
|
|
default="1024x768");
|
|
parser.add_option("-w", "--wide", action="store_true",
|
|
help="Use widescreen (1280x800) with Xephyr")
|
|
parser.add_option("", "--eval-file", metavar="EVAL_FILE",
|
|
help="Evaluate the contents of the given JavaScript file")
|
|
parser.add_option("", "--create-extension", action="store_true",
|
|
help="Create a new GNOME Shell extension")
|
|
parser.add_option("", "--version", action="callback", callback=show_version,
|
|
help="Display version and exit")
|
|
|
|
options, args = parser.parse_args()
|
|
|
|
if args:
|
|
parser.print_usage()
|
|
sys.exit(1)
|
|
|
|
if options.create_extension:
|
|
import json
|
|
|
|
print
|
|
print '''Name should be a very short (ideally descriptive) string.
|
|
Examples are: "Click To Focus", "Adblock", "Shell Window Shrinker".
|
|
'''
|
|
name = raw_input('Name: ').strip()
|
|
print
|
|
print '''Description is a single-sentence explanation of what your extension does.
|
|
Examples are: "Make windows visible on click", "Block advertisement popups"
|
|
"Animate windows shrinking on minimize"
|
|
'''
|
|
description = raw_input('Description: ').strip()
|
|
underifier = re.compile('[^A-Za-z]')
|
|
sample_uuid = underifier.sub('_', name)
|
|
# TODO use evolution data server
|
|
hostname = subprocess.Popen(['hostname'], stdout=subprocess.PIPE).communicate()[0].strip()
|
|
sample_uuid = sample_uuid + '@' + hostname
|
|
|
|
print
|
|
print '''Uuid is a globally-unique identifier for your extension.
|
|
This should be in the format of an email address (foo.bar@extensions.example.com), but
|
|
need not be an actual email address, though it's a good idea to base the uuid on your
|
|
email address. For example, if your email address is janedoe@example.com, you might
|
|
use an extension title clicktofocus@janedoe.example.com.'''
|
|
uuid = raw_input('Uuid [%s]: ' % (sample_uuid, )).strip()
|
|
if uuid == '':
|
|
uuid = sample_uuid
|
|
|
|
extension_path = os.path.join(os.path.expanduser('~/.local'), 'share', 'gnome-shell', 'extensions', uuid)
|
|
if os.path.exists(extension_path):
|
|
print "Extension path %r already exists" % (extension_path, )
|
|
sys.exit(0)
|
|
os.makedirs(extension_path)
|
|
meta = { 'name': name,
|
|
'description': description,
|
|
'uuid': uuid }
|
|
f = open(os.path.join(extension_path, 'metadata.json'), 'w')
|
|
try:
|
|
json.dump(meta, f)
|
|
except AttributeError:
|
|
# For Python versions older than 2.6, try using the json-py module
|
|
f.write(json.write(meta) + '\n')
|
|
f.close()
|
|
|
|
extensionjs_path = os.path.join(extension_path, 'extension.js')
|
|
f = open(extensionjs_path, 'w')
|
|
f.write('''// Sample extension code, makes clicking on the panel show a message
|
|
const St = imports.gi.St;
|
|
const Mainloop = imports.mainloop;
|
|
|
|
const Main = imports.ui.main;
|
|
|
|
function _showHello() {
|
|
let text = new St.Label({ style_class: 'helloworld-label', text: "Hello, world!" });
|
|
let monitor = global.get_primary_monitor();
|
|
global.stage.add_actor(text);
|
|
text.set_position(Math.floor (monitor.width / 2 - text.width / 2), Math.floor(monitor.height / 2 - text.height / 2));
|
|
Mainloop.timeout_add(3000, function () { text.destroy(); });
|
|
}
|
|
|
|
// Put your extension initialization code here
|
|
function main() {
|
|
Main.panel.actor.reactive = true;
|
|
Main.panel.actor.connect('button-release-event', _showHello);
|
|
}
|
|
''')
|
|
f.close()
|
|
|
|
f = open(os.path.join(extension_path, 'stylesheet.css'), 'w')
|
|
f.write('''/* Example stylesheet */
|
|
.helloworld-label {
|
|
font-size: 36px;
|
|
font-weight: bold;
|
|
color: #ffffff;
|
|
background-color: rgba(10,10,10,0.7);
|
|
border-radius: 5px;
|
|
}
|
|
''')
|
|
f.close()
|
|
|
|
subprocess.Popen(['gnome-open', extensionjs_path])
|
|
sys.exit(0)
|
|
|
|
# Handle ssh logins
|
|
if 'DISPLAY' not in os.environ:
|
|
running_env = get_running_session_environs()
|
|
os.environ.update(running_env)
|
|
|
|
if options.eval_file:
|
|
import dbus
|
|
|
|
f = open(options.eval_file)
|
|
contents = f.read()
|
|
f.close()
|
|
|
|
session = dbus.SessionBus()
|
|
shell = session.get_object('org.gnome.Shell', '/org/gnome/Shell')
|
|
shell = dbus.Interface(shell, 'org.gnome.Shell')
|
|
result = shell.Eval(contents)
|
|
print result
|
|
sys.exit(0)
|
|
|
|
if options.debug_command:
|
|
options.debug = True
|
|
elif options.debug:
|
|
options.debug_command = "gdb --args"
|
|
|
|
if options.wide:
|
|
options.geometry = "1280x800"
|
|
|
|
# Figure out whether or not to use GL_EXT_texture_from_pixmap. By default
|
|
# we use it iff we aren't running Xephyr, but we allow the user to
|
|
# explicitly disable it.
|
|
# FIXME: Move this to ClutterGlxPixmap like
|
|
# CLUTTER_PIXMAP_TEXTURE_RECTANGLE=disable.
|
|
if 'GNOME_SHELL_DISABLE_TFP' in os.environ and \
|
|
os.environ['GNOME_SHELL_DISABLE_TFP'] != '':
|
|
use_tfp = False
|
|
else:
|
|
# tfp does not work correctly in Xephyr
|
|
use_tfp = not options.xephyr
|
|
|
|
# We only respawn the previous environment on abnormal exit;
|
|
# for a clean exit, we assume that gnome-shell was replaced with
|
|
# something else.
|
|
normal_exit = False
|
|
|
|
try:
|
|
if options.perf:
|
|
normal_exit = run_performance_test()
|
|
else:
|
|
normal_exit = run_shell()
|
|
finally:
|
|
if not options.xephyr and options.replace and (options.perf or not normal_exit):
|
|
restore_gnome()
|