diff --git a/benchmarks/benchmark-runner.js b/benchmarks/benchmark-runner.js new file mode 100644 index 000000000..a3a4a3f4e --- /dev/null +++ b/benchmarks/benchmark-runner.js @@ -0,0 +1,60 @@ +/** @babel */ + +import Chart from 'chart.js' +import glob from 'glob' +import fs from 'fs-plus' +import path from 'path' + +export default async function (benchmarkPaths) { + let paths = [] + for (const benchmarkPath of benchmarkPaths) { + if (fs.isDirectorySync(benchmarkPath)) { + paths = paths.concat(glob.sync(path.join(benchmarkPath, '**', '*.bench.js'))) + } else { + paths.push(benchmarkPath) + } + } + + document.body.style.backgroundColor = '#ffffff' + while (paths.length > 0) { + const benchmark = require(paths.shift())() + let results + if (benchmark instanceof Promise) { + results = await benchmark + } else { + results = benchmark + } + + const dataByBenchmarkName = {} + for (const {name, duration, x} of results) { + dataByBenchmarkName[name] = dataByBenchmarkName[name] || {points: []} + dataByBenchmarkName[name].points.push({x, y: duration}) + } + + const benchmarkContainer = document.createElement('div') + document.body.appendChild(benchmarkContainer) + for (const key in dataByBenchmarkName) { + const data = dataByBenchmarkName[key] + if (data.points.length > 1) { + const canvas = document.createElement('canvas') + benchmarkContainer.appendChild(canvas) + const chart = new Chart(canvas, { + type: 'line', + data: { + labels: data.points.map((p) => p.x), + datasets: [{label: key, data: data.points}] + } + }) + } else { + const title = document.createElement('h2') + title.textContent = key + benchmarkContainer.appendChild(title) + const duration = document.createElement('p') + duration.textContent = `${data.points[0].y}ms` + benchmarkContainer.appendChild(duration) + } + + global.atom.reset() + } + } +} diff --git a/package.json b/package.json index 108db5904..6b1357157 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "babel-core": "5.8.38", "cached-run-in-this-context": "0.4.1", "chai": "3.5.0", + "chart.js": "^2.3.0", "clear-cut": "^2.0.1", "coffee-script": "1.11.1", "color": "^0.7.3", @@ -32,6 +33,7 @@ "fstream": "0.1.24", "fuzzaldrin": "^2.1", "git-utils": "^4.1.2", + "glob": "^7.1.1", "grim": "1.5.0", "jasmine-json": "~0.0", "jasmine-tagged": "^1.1.4", diff --git a/src/initialize-benchmark-window.js b/src/initialize-benchmark-window.js new file mode 100644 index 000000000..493caab15 --- /dev/null +++ b/src/initialize-benchmark-window.js @@ -0,0 +1,86 @@ +/** @babel */ + +import {ipcRenderer, remote} from 'electron' +import path from 'path' +import ipcHelpers from './ipc-helpers' + +export default function () { + const {getWindowLoadSettings} = require('./window-load-settings-helpers') + const {headless, resourcePath, benchmarkPaths} = getWindowLoadSettings() + try { + const Clipboard = require('../src/clipboard') + const ApplicationDelegate = require('../src/application-delegate') + const AtomEnvironment = require('../src/atom-environment') + const TextEditor = require('../src/text-editor') + + const exportsPath = path.join(resourcePath, 'exports') + require('module').globalPaths.push(exportsPath) // Add 'exports' to module search path. + process.env.NODE_PATH = exportsPath // Set NODE_PATH env variable since tasks may need it. + + document.title = 'Benchmarks' + window.addEventListener('keydown', (event) => { + // Quit: cmd-q / ctrl-q + if ((event.metaKey || event.ctrlKey) && event.keyCode === 81) { + ipcRenderer.send('command', 'application:quit') + } + + // Reload: cmd-r / ctrl-r + if ((event.metaKey || event.ctrlKey) && event.keyCode === 82) { + ipcHelpers.call('window-method', 'reload') + } + + // Toggle Dev Tools: cmd-alt-i (Mac) / ctrl-shift-i (Linux/Windows) + if (event.keyCode === 73) { + const isDarwin = process.platform === 'darwin' + if ((isDarwin && event.metaKey && event.altKey) || (!isDarwin && event.ctrlKey && event.shiftKey)) { + ipcHelpers.call('window-method', 'toggleDevTools') + } + } + + // Close: cmd-w / ctrl-w + if ((event.metaKey || event.ctrlKey) && event.keyCode === 87) { + ipcHelpers.call('window-method', 'close') + } + + // Copy: cmd-c / ctrl-c + if ((event.metaKey || event.ctrlKey) && event.keyCode === 67) { + ipcHelpers.call('window-method', 'copy') + } + }, true) + + const clipboard = new Clipboard() + TextEditor.setClipboard(clipboard) + + const applicationDelegate = new ApplicationDelegate() + global.atom = new AtomEnvironment({ + applicationDelegate, window, document, clipboard, + configDirPath: process.env.ATOM_HOME, enablePersistence: false + }) + + // Prevent benchmarks from modifying application menus + global.atom.menu.sendToBrowserProcess = function () { } + + if (!headless) { + remote.getCurrentWindow().show() + } + + const benchmarkRunner = require('../benchmarks/benchmark-runner') + return benchmarkRunner(benchmarkPaths).then((code) => { + if (headless) { + exitWithStatusCode(code) + } + }) + } catch (e) { + if (headless) { + console.error(error.stack || error) + exitWithStatusCode(1) + } else { + throw e + } + } +} + +function exitWithStatusCode (code) { + remote.app.emit('will-quit') + remote.process.exit(status) +} diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 21f138eed..b8caa23ba 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -42,7 +42,7 @@ class AtomApplication # take a few seconds to trigger 'error' event, it could be a bug of node # or atom-shell, before it's fixed we check the existence of socketPath to # speedup startup. - if (process.platform isnt 'win32' and not fs.existsSync options.socketPath) or options.test + if (process.platform isnt 'win32' and not fs.existsSync options.socketPath) or options.test or options.benchmark new AtomApplication(options).initialize(options) return @@ -86,7 +86,7 @@ class AtomApplication @config.onDidChange 'core.useCustomTitleBar', @promptForRestart.bind(this) - @autoUpdateManager = new AutoUpdateManager(@version, options.test, @resourcePath, @config) + @autoUpdateManager = new AutoUpdateManager(@version, options.test or options.benchmark, @resourcePath, @config) @applicationMenu = new ApplicationMenu(@version, @autoUpdateManager) @atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode) @@ -103,23 +103,39 @@ class AtomApplication Promise.all(windowsClosePromises).then(=> @disposable.dispose()) launch: (options) -> - if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 or options.test + if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 or options.test or options.benchmark @openWithOptions(options) else @loadState(options) or @openPath(options) - openWithOptions: ({initialPaths, pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout, clearWindowState, addToLastWindow, env}) -> + openWithOptions: (options) -> + {initialPaths, pathsToOpen, executedFrom, urlsToOpen, benchmark, test, + pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, + timeout, clearWindowState, addToLastWindow, env} = options + app.focus() if test - @runTests({headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, logFile, timeout, env}) + @runTests({ + headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, + logFile, timeout, env + }) + else if benchmark + @runBenchmarks({headless: false, @resourcePath, executedFrom, pathsToOpen, timeout, env}) else if pathsToOpen.length > 0 - @openPaths({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env}) + @openPaths({ + initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, + devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env + }) else if urlsToOpen.length > 0 - @openUrl({urlToOpen, devMode, safeMode, env}) for urlToOpen in urlsToOpen + for urlToOpen in urlsToOpen + @openUrl({urlToOpen, devMode, safeMode, env}) else # Always open a editor window if this is the first instance of Atom. - @openPath({initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env}) + @openPath({ + initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, + clearWindowState, addToLastWindow, env + }) # Public: Removes the {AtomWindow} from the global window list. removeWindow: (window) -> @@ -651,6 +667,29 @@ class AtomApplication safeMode ?= false new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env}) + runBenchmarks: ({headless, resourcePath, executedFrom, pathsToOpen, env}) -> + if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath) + resourcePath = @resourcePath + + try + windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-benchmark-window')) + catch error + windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-benchmark-window')) + + benchmarkPaths = [] + if pathsToOpen? + for pathToOpen in pathsToOpen + benchmarkPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen))) + + if benchmarkPaths.length is 0 + process.stderr.write 'Error: Specify at least one benchmark path.\n\n' + process.exit(1) + + devMode = true + isSpec = true + safeMode = false + new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, isSpec, devMode, benchmarkPaths, safeMode, env}) + resolveTestRunnerPath: (testPath) -> FindParentDir ?= require 'find-parent-dir' diff --git a/src/main-process/main.js b/src/main-process/main.js index 28871b661..5823b2e27 100644 --- a/src/main-process/main.js +++ b/src/main-process/main.js @@ -19,7 +19,7 @@ if (args.resourcePath) { const stableResourcePath = path.dirname(path.dirname(__dirname)) const defaultRepositoryPath = path.join(electron.app.getPath('home'), 'github', 'atom') - if (args.dev || args.test) { + if (args.dev || args.test || args.benchmark) { if (process.env.ATOM_DEV_RESOURCE_PATH) { resourcePath = process.env.ATOM_DEV_RESOURCE_PATH } else if (fs.statSyncNoException(defaultRepositoryPath)) { diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 07b2b49f8..db6991a1e 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -45,6 +45,7 @@ module.exports = function parseCommandLine (processArgs) { 'portable', 'Set portable mode. Copies the ~/.atom folder to be a sibling of the installed Atom location if a .atom folder is not already there.' ) + options.boolean('benchmark').describe('benchmark', 'Run the specified benchmarks.') options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.') options.alias('m', 'main-process').boolean('m').describe('m', 'Run the specified specs in the main process.') options.string('timeout').describe( @@ -78,6 +79,7 @@ module.exports = function parseCommandLine (processArgs) { const addToLastWindow = args['add'] const safeMode = args['safe'] const pathsToOpen = args._ + const benchmark = args['benchmark'] const test = args['test'] const mainProcess = args['main-process'] const timeout = args['timeout'] @@ -135,7 +137,7 @@ module.exports = function parseCommandLine (processArgs) { resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test, version, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, socketPath, userDataDir, profileStartup, timeout, setPortable, clearWindowState, - addToLastWindow, mainProcess, env: process.env + addToLastWindow, mainProcess, benchmark, env: process.env } } diff --git a/src/main-process/start.js b/src/main-process/start.js index 125f3ed54..bae9a9927 100644 --- a/src/main-process/start.js +++ b/src/main-process/start.js @@ -57,7 +57,7 @@ module.exports = function start (resourcePath, startTime) { if (args.userDataDir != null) { app.setPath('userData', args.userDataDir) - } else if (args.test) { + } else if (args.test || args.benchmark) { app.setPath('userData', temp.mkdirSync('atom-test-data')) }