diff options
Diffstat (limited to '')
-rw-r--r-- | js/ui/scripting.js | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/js/ui/scripting.js b/js/ui/scripting.js new file mode 100644 index 0000000..e4b29a4 --- /dev/null +++ b/js/ui/scripting.js @@ -0,0 +1,351 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported sleep, waitLeisure, createTestWindow, waitTestWindows, + destroyTestWindows, defineScriptEvent, scriptEvent, + collectStatistics, runPerfScript */ + +const { Gio, GLib, Meta, Shell } = imports.gi; + +const Config = imports.misc.config; +const Main = imports.ui.main; +const Params = imports.misc.params; +const Util = imports.misc.util; + +const { loadInterfaceXML } = imports.misc.fileUtils; + +// This module provides functionality for driving the shell user interface +// in an automated fashion. The primary current use case for this is +// automated performance testing (see runPerfScript()), but it could +// be applied to other forms of automation, such as testing for +// correctness as well. +// +// When scripting an automated test we want to make a series of calls +// in a linear fashion, but we also want to be able to let the main +// loop run so actions can finish. For this reason we write the script +// as an async function that uses await when it wants to let the main +// loop run. +// +// await Scripting.sleep(1000); +// main.overview.show(); +// await Scripting.waitLeisure(); +// + +/** + * sleep: + * @param {number} milliseconds - number of milliseconds to wait + * @returns {Promise} that resolves after @milliseconds ms + * + * Used within an automation script to pause the the execution of the + * current script for the specified amount of time. Use as + * 'yield Scripting.sleep(500);' + */ +function sleep(milliseconds) { + return new Promise(resolve => { + let id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, milliseconds, () => { + resolve(); + return GLib.SOURCE_REMOVE; + }); + GLib.Source.set_name_by_id(id, '[gnome-shell] sleep'); + }); +} + +/** + * waitLeisure: + * @returns {Promise} that resolves when the shell is idle + * + * Used within an automation script to pause the the execution of the + * current script until the shell is completely idle. Use as + * 'yield Scripting.waitLeisure();' + */ +function waitLeisure() { + return new Promise(resolve => { + global.run_at_leisure(resolve); + }); +} + +const PerfHelperIface = loadInterfaceXML('org.gnome.Shell.PerfHelper'); +var PerfHelperProxy = Gio.DBusProxy.makeProxyWrapper(PerfHelperIface); +function PerfHelper() { + return new PerfHelperProxy(Gio.DBus.session, 'org.gnome.Shell.PerfHelper', '/org/gnome/Shell/PerfHelper'); +} + +let _perfHelper = null; +function _getPerfHelper() { + if (_perfHelper == null) + _perfHelper = new PerfHelper(); + + return _perfHelper; +} + +function _spawnPerfHelper() { + let path = Config.LIBEXECDIR; + let command = `${path}/gnome-shell-perf-helper`; + Util.trySpawnCommandLine(command); +} + +function _callRemote(obj, method, ...args) { + return new Promise((resolve, reject) => { + args.push((result, excp) => { + if (excp) + reject(excp); + else + resolve(); + }); + + method.apply(obj, args); + }); +} + +/** + * createTestWindow: + * @param {Object} params: options for window creation. + * {number} [params.width=640] - width of window, in pixels + * {number} [params.height=480] - height of window, in pixels + * {bool} [params.alpha=false] - whether the window should have an alpha channel + * {bool} [params.maximized=false] - whether the window should be created maximized + * {bool} [params.redraws=false] - whether the window should continually redraw itself + * @returns {Promise} + * + * Creates a window using gnome-shell-perf-helper for testing purposes. + * While this function can be used with yield in an automation + * script to pause until the D-Bus call to the helper process returns, + * because of the normal X asynchronous mapping process, to actually wait + * until the window has been mapped and exposed, use waitTestWindows(). + */ +function createTestWindow(params) { + params = Params.parse(params, { width: 640, + height: 480, + alpha: false, + maximized: false, + redraws: false }); + + let perfHelper = _getPerfHelper(); + return _callRemote(perfHelper, perfHelper.CreateWindowRemote, + params.width, params.height, + params.alpha, params.maximized, params.redraws); +} + +/** + * waitTestWindows: + * @returns {Promise} + * + * Used within an automation script to pause until all windows previously + * created with createTestWindow have been mapped and exposed. + */ +function waitTestWindows() { + let perfHelper = _getPerfHelper(); + return _callRemote(perfHelper, perfHelper.WaitWindowsRemote); +} + +/** + * destroyTestWindows: + * @returns {Promise} + * + * Destroys all windows previously created with createTestWindow(). + * While this function can be used with yield in an automation + * script to pause until the D-Bus call to the helper process returns, + * this doesn't guarantee that Mutter has actually finished the destroy + * process because of normal X asynchronicity. + */ +function destroyTestWindows() { + let perfHelper = _getPerfHelper(); + return _callRemote(perfHelper, perfHelper.DestroyWindowsRemote); +} + +/** + * defineScriptEvent + * @param {string} name: The event will be called script.<name> + * @param {string} description: Short human-readable description of the event + * + * Convenience function to define a zero-argument performance event + * within the 'script' namespace that is reserved for events defined locally + * within a performance automation script + */ +function defineScriptEvent(name, description) { + Shell.PerfLog.get_default().define_event(`script.${name}`, + description, + ""); +} + +/** + * scriptEvent + * @param {string} name: Name registered with defineScriptEvent() + * + * Convenience function to record a script-local performance event + * previously defined with defineScriptEvent + */ +function scriptEvent(name) { + Shell.PerfLog.get_default().event(`script.${name}`); +} + +/** + * collectStatistics + * + * Convenience function to trigger statistics collection + */ +function collectStatistics() { + Shell.PerfLog.get_default().collect_statistics(); +} + +function _collect(scriptModule, outputFile) { + let eventHandlers = {}; + + for (let f in scriptModule) { + let m = /([A-Za-z]+)_([A-Za-z]+)/.exec(f); + if (m) + eventHandlers[`${m[1]}.${m[2]}`] = scriptModule[f]; + } + + Shell.PerfLog.get_default().replay( + (time, eventName, signature, arg) => { + if (eventName in eventHandlers) + eventHandlers[eventName](time, arg); + }); + + if ('finish' in scriptModule) + scriptModule.finish(); + + if (outputFile) { + let f = Gio.file_new_for_path(outputFile); + let raw = f.replace(null, false, + Gio.FileCreateFlags.NONE, + null); + let out = Gio.BufferedOutputStream.new_sized(raw, 4096); + Shell.write_string_to_stream(out, "{\n"); + + Shell.write_string_to_stream(out, '"events":\n'); + Shell.PerfLog.get_default().dump_events(out); + + let monitors = Main.layoutManager.monitors; + let primary = Main.layoutManager.primaryIndex; + Shell.write_string_to_stream(out, ',\n"monitors":\n['); + for (let i = 0; i < monitors.length; i++) { + let monitor = monitors[i]; + if (i != 0) + Shell.write_string_to_stream(out, ', '); + Shell.write_string_to_stream(out, '"%s%dx%d+%d+%d"'.format(i == primary ? "*" : "", + monitor.width, monitor.height, + monitor.x, monitor.y)); + } + Shell.write_string_to_stream(out, ' ]'); + + Shell.write_string_to_stream(out, ',\n"metrics":\n[ '); + let first = true; + for (let name in scriptModule.METRICS) { + let metric = scriptModule.METRICS[name]; + // Extra checks here because JSON.stringify generates + // invalid JSON for undefined values + if (metric.description == null) { + log(`Error: No description found for metric ${name}`); + continue; + } + if (metric.units == null) { + log(`Error: No units found for metric ${name}`); + continue; + } + if (metric.value == null) { + log(`Error: No value found for metric ${name}`); + continue; + } + + if (!first) + Shell.write_string_to_stream(out, ',\n '); + first = false; + + Shell.write_string_to_stream(out, + `{ "name": ${JSON.stringify(name)},\n` + + ` "description": ${JSON.stringify(metric.description)},\n` + + ` "units": ${JSON.stringify(metric.units)},\n` + + ` "value": ${JSON.stringify(metric.value)} }`); + } + Shell.write_string_to_stream(out, ' ]'); + + Shell.write_string_to_stream(out, ',\n"log":\n'); + Shell.PerfLog.get_default().dump_log(out); + + Shell.write_string_to_stream(out, '\n}\n'); + out.close(null); + } else { + let metrics = []; + for (let metric in scriptModule.METRICS) + metrics.push(metric); + + metrics.sort(); + + print('------------------------------------------------------------'); + for (let i = 0; i < metrics.length; i++) { + let metric = metrics[i]; + print(`# ${scriptModule.METRICS[metric].description}`); + print(`${metric}: ${scriptModule.METRICS[metric].value}${scriptModule.METRICS[metric].units}`); + } + print('------------------------------------------------------------'); + } +} + +async function _runPerfScript(scriptModule, outputFile) { + try { + await scriptModule.run(); + } catch (err) { + log(`Script failed: ${err}\n${err.stack}`); + Meta.exit(Meta.ExitCode.ERROR); + } + + try { + _collect(scriptModule, outputFile); + } catch (err) { + log(`Script failed: ${err}\n${err.stack}`); + Meta.exit(Meta.ExitCode.ERROR); + } + Meta.exit(Meta.ExitCode.SUCCESS); +} + +/** + * runPerfScript + * @param {Object} scriptModule: module object with run and finish + * functions and event handlers + * @param {string} outputFile: path to write output to + * + * Runs a script for automated collection of performance data. The + * script is defined as a Javascript module with specified contents. + * + * First the run() function within the module will be called as a + * generator to automate a series of actions. These actions will + * trigger performance events and the script can also record its + * own performance events. + * + * Then the recorded event log is replayed using handler functions + * within the module. The handler for the event 'foo.bar' is called + * foo_bar(). + * + * Finally if the module has a function called finish(), that will + * be called. + * + * The event handler and finish functions are expected to fill in + * metrics to an object within the module called METRICS. Each + * property of this object represents an individual metric. The + * name of the property is the name of the metric, the value + * of the property is an object with the following properties: + * + * description: human readable description of the metric + * units: a string representing the units of the metric. It has + * the form '<unit> <unit> ... / <unit> / <unit> ...'. Certain + * unit values are recognized: s, ms, us, B, KiB, MiB. Other + * values can appear but are uninterpreted. Examples 's', + * '/ s', 'frames', 'frames / s', 'MiB / s / frame' + * value: computed value of the metric + * + * The resulting metrics will be written to @outputFile as JSON, or, + * if @outputFile is not provided, logged. + * + * After running the script and collecting statistics from the + * event log, GNOME Shell will exit. + **/ +function runPerfScript(scriptModule, outputFile) { + Shell.PerfLog.get_default().set_enabled(true); + _spawnPerfHelper(); + + Gio.bus_watch_name(Gio.BusType.SESSION, + 'org.gnome.Shell.PerfHelper', + Gio.BusNameWatcherFlags.NONE, + () => _runPerfScript(scriptModule, outputFile), + null); +} |