diff options
Diffstat (limited to 'js/perf')
-rw-r--r-- | js/perf/basic.js | 146 | ||||
-rw-r--r-- | js/perf/core.js | 271 | ||||
-rw-r--r-- | js/perf/hwtest.js | 319 |
3 files changed, 736 insertions, 0 deletions
diff --git a/js/perf/basic.js b/js/perf/basic.js new file mode 100644 index 0000000..6d0ef53 --- /dev/null +++ b/js/perf/basic.js @@ -0,0 +1,146 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported run, finish, script_topBarNavDone, script_notificationShowDone, + script_notificationCloseDone, script_overviewShowDone, + script_applicationsShowStart, script_applicationsShowDone, METRICS, +*/ +/* eslint camelcase: ["error", { properties: "never", allow: ["^script_"] }] */ + +const { St } = imports.gi; + +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; +const Scripting = imports.ui.scripting; + +// This script tests the most important (basic) functionality of the shell. + +var METRICS = {}; + +async function run() { + /* eslint-disable no-await-in-loop */ + Scripting.defineScriptEvent('topBarNavStart', 'Starting to navigate the top bar'); + Scripting.defineScriptEvent('topBarNavDone', 'Done navigating the top bar'); + Scripting.defineScriptEvent('notificationShowStart', 'Showing a notification'); + Scripting.defineScriptEvent('notificationShowDone', 'Done showing a notification'); + Scripting.defineScriptEvent('notificationCloseStart', 'Closing a notification'); + Scripting.defineScriptEvent('notificationCloseDone', 'Done closing a notification'); + Scripting.defineScriptEvent('overviewShowStart', 'Starting to show the overview'); + Scripting.defineScriptEvent('overviewShowDone', 'Overview finished showing'); + Scripting.defineScriptEvent('applicationsShowStart', 'Starting to switch to applications view'); + Scripting.defineScriptEvent('applicationsShowDone', 'Done switching to applications view'); + + Main.overview.connect('shown', + () => Scripting.scriptEvent('overviewShowDone')); + + await Scripting.sleep(1000); + + // navigate through top bar + Scripting.scriptEvent('topBarNavStart'); + Main.panel.statusArea.quickSettings.menu.open(); + await Scripting.sleep(400); + + const { menuManager } = Main.panel; + while (menuManager.activeMenu && + Main.panel.navigate_focus(menuManager.activeMenu.sourceActor, + St.DirectionType.TAB_BACKWARD, false)) + await Scripting.sleep(400); + Scripting.scriptEvent('topBarNavDone'); + + await Scripting.sleep(1000); + + // notification + const source = new MessageTray.SystemNotificationSource(); + Main.messageTray.add(source); + + Scripting.scriptEvent('notificationShowStart'); + source.connect('notification-show', + () => Scripting.scriptEvent('notificationShowDone')); + + const notification = new MessageTray.Notification(source, + 'A test notification'); + source.showNotification(notification); + await Scripting.sleep(400); + + Main.panel.statusArea.dateMenu.menu.open(); + await Scripting.sleep(400); + + Scripting.scriptEvent('notificationCloseStart'); + notification.connect('destroy', + () => Scripting.scriptEvent('notificationCloseDone')); + + notification.destroy(); + await Scripting.sleep(400); + + Main.panel.statusArea.dateMenu.menu.close(); + await Scripting.waitLeisure(); + + await Scripting.sleep(1000); + + // overview (window picker) + Scripting.scriptEvent('overviewShowStart'); + Main.overview.show(); + await Scripting.waitLeisure(); + Main.overview.hide(); + await Scripting.waitLeisure(); + + await Scripting.sleep(1000); + + // overview (app picker) + Main.overview.show(); + await Scripting.waitLeisure(); + + Scripting.scriptEvent('applicationsShowStart'); + // eslint-disable-next-line require-atomic-updates + Main.overview.dash.showAppsButton.checked = true; + await Scripting.waitLeisure(); + Scripting.scriptEvent('applicationsShowDone'); + // eslint-disable-next-line require-atomic-updates + Main.overview.dash.showAppsButton.checked = false; + await Scripting.waitLeisure(); + + Main.overview.hide(); + await Scripting.waitLeisure(); + /* eslint-enable no-await-in-loop */ +} + +let topBarNav = false; +let notificationShown = false; +let notificationClosed = false; +let windowPickerShown = false; +let appPickerShown = false; + +function script_topBarNavDone() { + topBarNav = true; +} + +function script_notificationShowDone() { + notificationShown = true; +} + +function script_notificationCloseDone() { + notificationClosed = true; +} + +function script_overviewShowDone() { + windowPickerShown = true; +} + +function script_applicationsShowDone() { + appPickerShown = true; +} + +function finish() { + if (!topBarNav) + throw new Error('Failed to navigate top bar'); + + if (!notificationShown) + throw new Error('Failed to show notification'); + + if (!notificationClosed) + throw new Error('Failed to close notification'); + + if (!windowPickerShown) + throw new Error('Failed to show window picker'); + + if (!appPickerShown) + throw new Error('Failed to show app picker'); +} diff --git a/js/perf/core.js b/js/perf/core.js new file mode 100644 index 0000000..bd7e220 --- /dev/null +++ b/js/perf/core.js @@ -0,0 +1,271 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported run, script_overviewShowStart, script_overviewShowDone, + script_applicationsShowStart, script_applicationsShowDone, + script_afterShowHide, malloc_usedSize, glx_swapComplete, + clutter_stagePaintDone */ +/* eslint camelcase: ["error", { properties: "never", allow: ["^script_", "^malloc", "^glx", "^clutter"] }] */ + +const System = imports.system; + +const Main = imports.ui.main; +const Scripting = imports.ui.scripting; + +// This performance script measure the most important (core) performance +// metrics for the shell. By looking at the output metrics of this script +// someone should be able to get an idea of how well the shell is performing +// on a particular system. + +var METRICS = { + overviewLatencyFirst: { + description: 'Time to first frame after triggering overview, first time', + units: 'us', + }, + overviewFpsFirst: { + description: 'Frame rate when going to the overview, first time', + units: 'frames / s', + }, + overviewLatencySubsequent: { + description: 'Time to first frame after triggering overview, second time', + units: 'us', + }, + overviewFpsSubsequent: { + description: 'Frames rate when going to the overview, second time', + units: 'frames / s', + }, + overviewFps5Windows: { + description: 'Frames rate when going to the overview, 5 windows open', + units: 'frames / s', + }, + overviewFps10Windows: { + description: 'Frames rate when going to the overview, 10 windows open', + units: 'frames / s', + }, + overviewFps5Maximized: { + description: 'Frames rate when going to the overview, 5 maximized windows open', + units: 'frames / s', + }, + overviewFps10Maximized: { + description: 'Frames rate when going to the overview, 10 maximized windows open', + units: 'frames / s', + }, + overviewFps5Alpha: { + description: 'Frames rate when going to the overview, 5 alpha-transparent windows open', + units: 'frames / s', + }, + overviewFps10Alpha: { + description: 'Frames rate when going to the overview, 10 alpha-transparent windows open', + units: 'frames / s', + }, + usedAfterOverview: { + description: "Malloc'ed bytes after the overview is shown once", + units: 'B', + }, + leakedAfterOverview: { + description: "Additional malloc'ed bytes the second time the overview is shown", + units: 'B', + }, + applicationsShowTimeFirst: { + description: 'Time to switch to applications view, first time', + units: 'us', + }, + applicationsShowTimeSubsequent: { + description: 'Time to switch to applications view, second time', + units: 'us', + }, +}; + +const WINDOW_CONFIGS = [{ + width: 640, height: 480, + alpha: false, maximized: false, count: 1, metric: 'overviewFpsSubsequent', +}, { + width: 640, height: 480, + alpha: false, maximized: false, count: 5, metric: 'overviewFps5Windows', +}, { + width: 640, height: 480, + alpha: false, maximized: false, count: 10, metric: 'overviewFps10Windows', +}, { + width: 640, height: 480, + alpha: false, maximized: true, count: 5, metric: 'overviewFps5Maximized', +}, { + width: 640, height: 480, + alpha: false, maximized: true, count: 10, metric: 'overviewFps10Maximized', +}, { + width: 640, height: 480, + alpha: true, maximized: false, count: 5, metric: 'overviewFps5Alpha', +}, { + width: 640, height: 480, + alpha: true, maximized: false, count: 10, metric: 'overviewFps10Alpha', +}]; + +async function run() { + /* eslint-disable no-await-in-loop */ + Scripting.defineScriptEvent("overviewShowStart", "Starting to show the overview"); + Scripting.defineScriptEvent("overviewShowDone", "Overview finished showing"); + Scripting.defineScriptEvent("afterShowHide", "After a show/hide cycle for the overview"); + Scripting.defineScriptEvent("applicationsShowStart", "Starting to switch to applications view"); + Scripting.defineScriptEvent("applicationsShowDone", "Done switching to applications view"); + + // Enable recording of timestamps for different points in the frame cycle + global.frame_timestamps = true; + + Main.overview.connect('shown', () => { + Scripting.scriptEvent('overviewShowDone'); + }); + + await Scripting.sleep(1000); + + for (let i = 0; i < 2 * WINDOW_CONFIGS.length; i++) { + // We go to the overview twice for each configuration; the first time + // to calculate the mipmaps for the windows, the second time to get + // a clean set of numbers. + if ((i % 2) == 0) { + let config = WINDOW_CONFIGS[i / 2]; + await Scripting.destroyTestWindows(); + + for (let k = 0; k < config.count; k++) { + await Scripting.createTestWindow({ + width: config.width, + height: config.height, + alpha: config.alpha, + maximized: config.maximized, + }); + } + + await Scripting.waitTestWindows(); + await Scripting.sleep(1000); + await Scripting.waitLeisure(); + } + + Scripting.scriptEvent('overviewShowStart'); + Main.overview.show(); + + await Scripting.waitLeisure(); + Main.overview.hide(); + await Scripting.waitLeisure(); + + System.gc(); + await Scripting.sleep(1000); + Scripting.collectStatistics(); + Scripting.scriptEvent('afterShowHide'); + } + + await Scripting.destroyTestWindows(); + await Scripting.sleep(1000); + + Main.overview.show(); + await Scripting.waitLeisure(); + + for (let i = 0; i < 2; i++) { + Scripting.scriptEvent('applicationsShowStart'); + // eslint-disable-next-line require-atomic-updates + Main.overview.dash.showAppsButton.checked = true; + await Scripting.waitLeisure(); + Scripting.scriptEvent('applicationsShowDone'); + // eslint-disable-next-line require-atomic-updates + Main.overview.dash.showAppsButton.checked = false; + await Scripting.waitLeisure(); + } + /* eslint-enable no-await-in-loop */ +} + +let showingOverview = false; +let finishedShowingOverview = false; +let overviewShowStart; +let overviewFrames; +let overviewLatency; +let mallocUsedSize = 0; +let overviewShowCount = 0; +let haveSwapComplete = false; +let applicationsShowStart; +let applicationsShowCount = 0; + +function script_overviewShowStart(time) { + showingOverview = true; + finishedShowingOverview = false; + overviewShowStart = time; + overviewFrames = 0; +} + +function script_overviewShowDone(_time) { + // We've set up the state at the end of the zoom out, but we + // need to wait for one more frame to paint before we count + // ourselves as done. + finishedShowingOverview = true; +} + +function script_applicationsShowStart(time) { + applicationsShowStart = time; +} + +function script_applicationsShowDone(time) { + applicationsShowCount++; + if (applicationsShowCount == 1) + METRICS.applicationsShowTimeFirst.value = time - applicationsShowStart; + else + METRICS.applicationsShowTimeSubsequent.value = time - applicationsShowStart; +} + +function script_afterShowHide(_time) { + if (overviewShowCount == 1) + METRICS.usedAfterOverview.value = mallocUsedSize; + else + METRICS.leakedAfterOverview.value = mallocUsedSize - METRICS.usedAfterOverview.value; +} + +function malloc_usedSize(time, bytes) { + mallocUsedSize = bytes; +} + +function _frameDone(time) { + if (showingOverview) { + if (overviewFrames == 0) + overviewLatency = time - overviewShowStart; + + overviewFrames++; + } + + if (finishedShowingOverview) { + showingOverview = false; + finishedShowingOverview = false; + overviewShowCount++; + + let dt = (time - (overviewShowStart + overviewLatency)) / 1000000; + + // If we see a start frame and an end frame, that would + // be 1 frame for a FPS computation, hence the '- 1' + let fps = (overviewFrames - 1) / dt; + + if (overviewShowCount == 1) { + METRICS.overviewLatencyFirst.value = overviewLatency; + METRICS.overviewFpsFirst.value = fps; + } else if (overviewShowCount == 2) { + METRICS.overviewLatencySubsequent.value = overviewLatency; + } + + // Other than overviewFpsFirst, we collect FPS metrics the second + // we show each window configuration. overviewShowCount is 1,2,3... + if (overviewShowCount % 2 == 0) { + let config = WINDOW_CONFIGS[(overviewShowCount / 2) - 1]; + METRICS[config.metric].value = fps; + } + } +} + +function glx_swapComplete(time, swapTime) { + haveSwapComplete = true; + + _frameDone(swapTime); +} + +function clutter_stagePaintDone(time) { + // If we aren't receiving GLXBufferSwapComplete events, then we approximate + // the time the user sees a frame with the time we finished doing drawing + // commands for the frame. This doesn't take into account the time for + // the GPU to finish painting, and the time for waiting for the buffer + // swap, but if this are uniform - every frame takes the same time to draw - + // then it won't upset our FPS calculation, though the latency value + // will be slightly too low. + + if (!haveSwapComplete) + _frameDone(time); +} diff --git a/js/perf/hwtest.js b/js/perf/hwtest.js new file mode 100644 index 0000000..63777bd --- /dev/null +++ b/js/perf/hwtest.js @@ -0,0 +1,319 @@ +/* exported run, script_desktopShown, script_overviewShowStart, + script_overviewShowDone, script_applicationsShowStart, + script_applicationsShowDone, script_mainViewDrawStart, + script_mainViewDrawDone, script_overviewDrawStart, + script_overviewDrawDone, script_redrawTestStart, + script_redrawTestDone, script_collectTimings, + script_geditLaunch, script_geditFirstFrame, + clutter_stagePaintStart, clutter_paintCompletedTimestamp */ +/* eslint camelcase: ["error", { properties: "never", allow: ["^script_", "^clutter"] }] */ +const { Clutter, Gio, Shell } = imports.gi; +const Main = imports.ui.main; +const Scripting = imports.ui.scripting; + +var METRICS = { + timeToDesktop: { + description: 'Time from starting graphical.target to desktop showing', + units: 'us', + }, + + overviewShowTime: { + description: 'Time to switch to overview view, first time', + units: 'us', + }, + + applicationsShowTime: { + description: 'Time to switch to applications view, first time', + units: 'us', + }, + + mainViewRedrawTime: { + description: 'Time to redraw the main view, full screen', + units: 'us', + }, + + overviewRedrawTime: { + description: 'Time to redraw the overview, full screen, 5 windows', + units: 'us', + }, + + applicationRedrawTime: { + description: 'Time to redraw frame with a maximized application update', + units: 'us', + }, + + geditStartTime: { + description: 'Time from gedit launch to window drawn', + units: 'us', + }, +}; + +function waitAndDraw(milliseconds) { + let cb; + + let timeline = new Clutter.Timeline({ duration: milliseconds }); + timeline.start(); + + timeline.connect('new-frame', (_timeline, _frame) => { + global.stage.queue_redraw(); + }); + + timeline.connect('completed', () => { + timeline.stop(); + if (cb) + cb(); + }); + + return callback => (cb = callback); +} + +function waitSignal(object, signal) { + let cb; + + let id = object.connect(signal, () => { + object.disconnect(id); + if (cb) + cb(); + }); + + return callback => (cb = callback); +} + +function extractBootTimestamp() { + const sp = Gio.Subprocess.new([ + 'journalctl', '-b', + 'MESSAGE_ID=7d4958e842da4a758f6c1cdc7b36dcc5', + 'UNIT=graphical.target', + '-o', + 'json', + ], Gio.SubprocessFlags.STDOUT_PIPE); + let result = null; + + let datastream = Gio.DataInputStream.new(sp.get_stdout_pipe()); + while (true) { // eslint-disable-line no-constant-condition + let [line, length_] = datastream.read_line_utf8(null); + if (line === null) + break; + + let fields = JSON.parse(line); + result = Number(fields['__MONOTONIC_TIMESTAMP']); + } + datastream.close(null); + return result; +} + +async function run() { + /* eslint-disable no-await-in-loop */ + Scripting.defineScriptEvent("desktopShown", "Finished initial animation"); + Scripting.defineScriptEvent("overviewShowStart", "Starting to show the overview"); + Scripting.defineScriptEvent("overviewShowDone", "Overview finished showing"); + Scripting.defineScriptEvent("applicationsShowStart", "Starting to switch to applications view"); + Scripting.defineScriptEvent("applicationsShowDone", "Done switching to applications view"); + Scripting.defineScriptEvent("mainViewDrawStart", "Drawing main view"); + Scripting.defineScriptEvent("mainViewDrawDone", "Ending timing main view drawing"); + Scripting.defineScriptEvent("overviewDrawStart", "Drawing overview"); + Scripting.defineScriptEvent("overviewDrawDone", "Ending timing overview drawing"); + Scripting.defineScriptEvent("redrawTestStart", "Drawing application window"); + Scripting.defineScriptEvent("redrawTestDone", "Ending timing application window drawing"); + Scripting.defineScriptEvent("collectTimings", "Accumulate frame timings from redraw tests"); + Scripting.defineScriptEvent("geditLaunch", "gedit application launch"); + Scripting.defineScriptEvent("geditFirstFrame", "first frame of gedit window drawn"); + + await Scripting.waitLeisure(); + Scripting.scriptEvent('desktopShown'); + + let interfaceSettings = new Gio.Settings({ + schema_id: 'org.gnome.desktop.interface', + }); + interfaceSettings.set_boolean('enable-animations', false); + + Scripting.scriptEvent('overviewShowStart'); + Main.overview.show(); + await Scripting.waitLeisure(); + Scripting.scriptEvent('overviewShowDone'); + + await Scripting.sleep(1000); + + Scripting.scriptEvent('applicationsShowStart'); + // eslint-disable-next-line require-atomic-updates + Main.overview.dash.showAppsButton.checked = true; + + await Scripting.waitLeisure(); + Scripting.scriptEvent('applicationsShowDone'); + + await Scripting.sleep(1000); + + Main.overview.hide(); + await Scripting.waitLeisure(); + + // --------------------- // + // Tests of redraw speed // + // --------------------- // + + global.frame_timestamps = true; + global.frame_finish_timestamp = true; + + for (let k = 0; k < 5; k++) + await Scripting.createTestWindow({ maximized: true }); + await Scripting.waitTestWindows(); + + await Scripting.sleep(1000); + + Scripting.scriptEvent('mainViewDrawStart'); + await waitAndDraw(1000); + Scripting.scriptEvent('mainViewDrawDone'); + + Main.overview.show(); + Scripting.waitLeisure(); + + await Scripting.sleep(1500); + + Scripting.scriptEvent('overviewDrawStart'); + await waitAndDraw(1000); + Scripting.scriptEvent('overviewDrawDone'); + + await Scripting.destroyTestWindows(); + Main.overview.hide(); + + await Scripting.createTestWindow({ + maximized: true, + redraws: true, + }); + await Scripting.waitTestWindows(); + + await Scripting.sleep(1000); + + Scripting.scriptEvent('redrawTestStart'); + await Scripting.sleep(1000); + Scripting.scriptEvent('redrawTestDone'); + + await Scripting.sleep(1000); + Scripting.scriptEvent('collectTimings'); + + await Scripting.destroyTestWindows(); + + global.frame_timestamps = false; + global.frame_finish_timestamp = false; + + await Scripting.sleep(1000); + + let appSys = Shell.AppSystem.get_default(); + let app = appSys.lookup_app('org.gnome.gedit.desktop'); + + Scripting.scriptEvent('geditLaunch'); + app.activate(); + + let windows = app.get_windows(); + if (windows.length > 0) + throw new Error('gedit was already running'); + + while (windows.length == 0) { + await waitSignal(global.display, 'window-created'); + windows = app.get_windows(); + } + + let actor = windows[0].get_compositor_private(); + await waitSignal(actor, 'first-frame'); + Scripting.scriptEvent('geditFirstFrame'); + + await Scripting.sleep(1000); + + windows[0].delete(global.get_current_time()); + + await Scripting.sleep(1000); + + interfaceSettings.set_boolean('enable-animations', true); + /* eslint-enable no-await-in-loop */ +} + +let overviewShowStart; +let applicationsShowStart; +let stagePaintStart; +let redrawTiming; +let redrawTimes = {}; +let geditLaunchTime; + +function script_desktopShown(time) { + let bootTimestamp = extractBootTimestamp(); + METRICS.timeToDesktop.value = time - bootTimestamp; +} + +function script_overviewShowStart(time) { + overviewShowStart = time; +} + +function script_overviewShowDone(time) { + METRICS.overviewShowTime.value = time - overviewShowStart; +} + +function script_applicationsShowStart(time) { + applicationsShowStart = time; +} + +function script_applicationsShowDone(time) { + METRICS.applicationsShowTime.value = time - applicationsShowStart; +} + +function script_mainViewDrawStart(_time) { + redrawTiming = 'mainView'; +} + +function script_mainViewDrawDone(_time) { + redrawTiming = null; +} + +function script_overviewDrawStart(_time) { + redrawTiming = 'overview'; +} + +function script_overviewDrawDone(_time) { + redrawTiming = null; +} + +function script_redrawTestStart(_time) { + redrawTiming = 'application'; +} + +function script_redrawTestDone(_time) { + redrawTiming = null; +} + +function script_collectTimings(_time) { + for (let timing in redrawTimes) { + let times = redrawTimes[timing]; + times.sort((a, b) => a - b); + + let len = times.length; + let median; + + if (len == 0) + median = -1; + else if (len % 2 == 1) + median = times[(len - 1) / 2]; + else + median = Math.round((times[len / 2 - 1] + times[len / 2]) / 2); + + METRICS[`${timing}RedrawTime`].value = median; + } +} + +function script_geditLaunch(time) { + geditLaunchTime = time; +} + +function script_geditFirstFrame(time) { + METRICS.geditStartTime.value = time - geditLaunchTime; +} + +function clutter_stagePaintStart(time) { + stagePaintStart = time; +} + +function clutter_paintCompletedTimestamp(time) { + if (redrawTiming != null && stagePaintStart != null) { + if (!(redrawTiming in redrawTimes)) + redrawTimes[redrawTiming] = []; + redrawTimes[redrawTiming].push(time - stagePaintStart); + } + stagePaintStart = null; +} |