diff --git a/.gitignore b/.gitignore index 7d105c26b11..18d662507f2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ logs *.log npm-debug.log.* +cpu-profiles +*.cpuprofile # Runtime data pids diff --git a/.travis.yml b/.travis.yml index 6b0f8f6dd41..259e79ce5f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ before_install: - sh -e /etc/init.d/xvfb start before_script: - npm run download-sync-client + - curl -sL https://raw.githubusercontent.com/travis-ci/artifacts/master/install | bash script: - npm run testsuite branches: @@ -30,6 +31,7 @@ env: - CXX=g++-4.8 NODE_ENV=test TEST_DIR=misc-components - CXX=g++-4.8 NODE_ENV=test TEST_DIR=navbar-components - CXX=g++-4.8 NODE_ENV=test TEST_DIR=tab-components + - CXX=g++-4.8 NODE_ENV=test TEST_DIR=performance ARTIFACTS_REGION=us-east-1 addons: apt: sources: diff --git a/package-lock.json b/package-lock.json index 9fe69f4eb29..3fdec93b151 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2632,6 +2632,39 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" }, + "chrome-remote-interface": { + "version": "0.24.5", + "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.24.5.tgz", + "integrity": "sha512-+RixJXes45Y4XnpAegjaWtDixdS6580aUpK8pzojRHHL89qXXvv0VBEPCoDaB9j0yBS+gMbpZKj2ZSH/45HABw==", + "dev": true, + "requires": { + "commander": "2.1.0", + "ws": "2.0.3" + }, + "dependencies": { + "commander": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", + "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=", + "dev": true + }, + "ultron": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz", + "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ=", + "dev": true + }, + "ws": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-2.0.3.tgz", + "integrity": "sha1-Uy/UmcP319cg5UPx+AcQbPxX2cs=", + "dev": true, + "requires": { + "ultron": "1.1.0" + } + } + } + }, "chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", diff --git a/package.json b/package.json index cec9eab958e..82df05e3d46 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "base64-js": "^1.2.0", "chai": "^3.4.1", "chai-as-promised": "^5.1.0", + "chrome-remote-interface": "^0.24.3", "co-mocha": "^1.1.2", "cross-env": "^3.1.4", "css-loader": "~0.28.7", diff --git a/test/lib/brave.js b/test/lib/brave.js index 31f02bc9a3f..ad3a1301e0e 100644 --- a/test/lib/brave.js +++ b/test/lib/brave.js @@ -345,7 +345,7 @@ var exports = { .waitForVisible(urlInput) }) - this.app.client.addCommand('waitForUrl', function (url) { + this.app.client.addCommand('waitForUrl', function (url, timeout = 5000, interval = 100) { logVerbose('waitForUrl("' + url + '")') return this.waitUntil(function () { return this.tabByUrl(url).then((response) => { @@ -355,7 +355,7 @@ var exports = { logVerbose('tabByUrl("' + url + '") => false') return false }) - }, 5000, null, 100) + }, timeout, null, interval) }) this.app.client.addCommand('waitForSelectedText', function (text) { @@ -666,7 +666,7 @@ var exports = { }).then((response) => response.value) }) - this.app.client.addCommand('newTab', function (createProperties = {}) { + this.app.client.addCommand('newTab', function (createProperties = {}, activateIfOpen = false, isRestore = false) { return this .execute(function (createProperties) { return devTools('appActions').createTabRequested(createProperties) @@ -696,20 +696,18 @@ var exports = { return this.execute(function (siteDetail) { return devTools('appActions').addBookmark(siteDetail) }, siteDetail).then((response) => response.value) - .waitForBookmarkEntry(waitUrl, false) + .waitForBookmarkEntry(waitUrl) }) - this.app.client.addCommand('waitForBookmarkEntry', function (location, waitForTitle = true) { - logVerbose('waitForBookmarkEntry("' + location + '", "' + waitForTitle + '")') + this.app.client.addCommand('waitForBookmarkEntry', function (location) { + logVerbose('waitForBookmarkEntry("' + location + '")') return this.waitUntil(function () { return this.getAppState().then((val) => { - const ret = val.value && val.value.bookmarks && Array.from(Object.values(val.value.bookmarks)).find( - (bookmark) => bookmark.location === location && - (!waitForTitle || (waitForTitle && bookmark.title))) - logVerbose('waitForBookmarkEntry("' + location + ', ' + waitForTitle + '") => ' + ret) + const ret = val.value.cache.bookmarkLocation.hasOwnProperty(location) + logVerbose('waitForBookmarkEntry("' + location + '") => ' + ret) return ret }) - }, 5000, null, 100) + }, 10000, null, 100) }) /** @@ -1112,7 +1110,10 @@ var exports = { }) }, - startApp: function () { + /** + * @param {Array=} extraArgs + */ + startApp: function (extraArgs) { if (process.env.KEEP_BRAVE_USER_DATA_DIR) { console.log('BRAVE_USER_DATA_DIR=' + userDataDir) } @@ -1121,6 +1122,8 @@ var exports = { BRAVE_USER_DATA_DIR: userDataDir, SPECTRON: true } + let args = ['./', '--enable-logging', '--v=1'] + if (extraArgs) { args = args.concat(extraArgs) } this.app = new Application({ quitTimeout: 0, waitTimeout: exports.defaultTimeout, @@ -1130,7 +1133,7 @@ var exports = { ? 'node_modules/electron-prebuilt/dist/brave.exe' : './node_modules/.bin/electron', env, - args: ['./', '--enable-logging', '--v=1'], + args, requireName: 'devTools' }) return this.app.start() diff --git a/test/lib/profilerUtil.js b/test/lib/profilerUtil.js new file mode 100644 index 00000000000..2b641b5518e --- /dev/null +++ b/test/lib/profilerUtil.js @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict' + +const Brave = require('./brave') +const CDP = require('chrome-remote-interface') +const fs = require('fs') + +const LOG_FOLDER = './cpu-profiles' + +// Private +// === + +/** + * Derive main process remote debug port based on webdriver args. + * @param {Spectron.Application} app e.g. Brave.app + * @returns {number} + */ +const getDebugPort = function (app) { + if (!app.args) { return } + for (let arg of app.args) { + if (arg.indexOf('--debug') === -1 && arg.indexOf('--inspect') === -1) { + continue + } + const regex = /([0-9]+)$/ + const result = arg.match(regex) + if (!result[1]) { continue } + return parseInt(result[1]) + } +} + +// Public +// === + +/** + * Connect to a remote instance using Chrome Debugging Protocol. + * See https://github.com/cyrus-and/chrome-remote-interface#cdpoptions-callback + */ +const initCDP = function * () { + const port = getDebugPort(Brave.app) + if (!port) { + throw new Error("Could not determine RDP port from webdriver app args. Did you start with Brave.startApp(['--debug={inspectPort}'] ?") + } + const cdp = yield CDP({port}) + Brave.cdp = cdp +} + +const startProfiler = function * () { + yield initCDP() + yield Brave.cdp.Profiler.enable() + yield Brave.cdp.Profiler.setSamplingInterval({interval: 100}) + yield Brave.cdp.Profiler.start() +} + +/** + * @param logTag {string=} Optional file prefix + * @returns filename to which CPU profile was written + */ +const stopProfiler = function * (logTag = '') { + const cdpProfilerResult = yield Brave.cdp.Profiler.stop() + if (!fs.existsSync(LOG_FOLDER)) { + console.log(`Creating directory ${LOG_FOLDER}`) + fs.mkdirSync(LOG_FOLDER) + } + const fileContent = JSON.stringify(cdpProfilerResult.profile, null, 2) + const filename = `${logTag}-${new Date().toISOString()}.cpuprofile` + const path = `${LOG_FOLDER}/${filename}` + fs.writeFile(path, fileContent) + console.log(`Wrote CPU profile data to: ${path}`) + return filename +} + +/** + * Profile a function. + * @param {function} fn + */ +const profile = function * (fn, logTag) { + yield startProfiler() + yield fn() + yield stopProfiler(logTag) +} + +const uploadTravisArtifacts = function * () { + if (!process.env.TRAVIS) { return } + console.log('Uploading Travis artifacts...') + const execute = require('../../tools/lib/execute') + const command = `artifacts upload ${LOG_FOLDER} --target-paths "$TRAVIS_REPO_SLUG/$TRAVIS_BUILD_NUMBER:$TRAVIS_REPO_SLUG/$TRAVIS_COMMIT"` + yield new Promise((resolve, reject) => { + execute(command, process.env, (err) => { + if (err) { + console.error('Failed to upload artifacts', err) + process.exit(1) + return reject(err) + } + resolve() + }) + }) +} + +module.exports = { + initCDP, + startProfiler, + stopProfiler, + profile, + uploadTravisArtifacts +} diff --git a/test/lib/userProfiles.js b/test/lib/userProfiles.js new file mode 100644 index 00000000000..ecf124ed388 --- /dev/null +++ b/test/lib/userProfiles.js @@ -0,0 +1,53 @@ +const Immutable = require('immutable') +const niceware = require('niceware') + +const addBookmarksN = function (total) { + if (!total || total > 65536) { + throw new Error('Can only add up to 65536 bookmarks.') + } + return function * (client) { + const data = [] + const buffer = new Buffer(2) + for (let n = 0; n < total; n++) { + buffer.writeUInt16BE(n) + const string = niceware.bytesToPassphrase(buffer)[0] + data.push({ + location: `https://www.${string}.com`, + title: string, + parentFolderId: 0 + }) + } + const lastBookmark = data.pop() + const immutableData = Immutable.fromJS(data) + yield client.waitForBrowserWindow() + .addBookmarks(immutableData) + .addBookmark(lastBookmark) + } +} +const addBookmarks4000 = addBookmarksN(4000) + +const addTabsN = function (total) { + return function * (client) { + const data = [] + const buffer = new Buffer(2) + for (let n = 0; n < total; n++) { + buffer.writeUInt16BE(n) + const string = niceware.bytesToPassphrase(buffer)[0] + data.push({ + active: false, + discarded: true, + url: `https://www.${string}.com` + }) + } + yield client.waitForBrowserWindow() + for (let datum of data) { + yield client.newTab(datum, false, true) // isRestore + } + } +} +const addTabs100 = addTabsN(100) + +module.exports = { + addBookmarks4000, + addTabs100 +} diff --git a/test/performance/bookmarksTest.js b/test/performance/bookmarksTest.js new file mode 100644 index 00000000000..74b75b5ec73 --- /dev/null +++ b/test/performance/bookmarksTest.js @@ -0,0 +1,86 @@ +/* global describe, it, beforeEach, afterEach */ + +const Brave = require('../lib/brave') +const {navigatorBookmarked, navigatorNotBookmarked, doneButton} = require('../lib/selectors') +const profilerUtil = require('../lib/profilerUtil') +const userProfiles = require('../lib/userProfiles') + +describe('Performance bookmarks', function () { + Brave.beforeAllServerSetup(this) + let inspectPort = 9223 + + function * setup () { + Brave.addCommands() + } + + function * setupBrave (enableInspector) { + const extraArgs = [] + if (enableInspector) { + extraArgs.push(`--debug=${inspectPort}`) + inspectPort += 1 + } + yield Brave.startApp(extraArgs) + yield setup(Brave.app.client) + } + + function * restart (timeout = 3000) { + yield Brave.stopApp(false, timeout) + yield setupBrave(true) + } + + beforeEach(function * () { + this.page1Url = Brave.server.url('page1.html') + yield setupBrave() + }) + + afterEach(function * () { + yield Brave.stopApp() + }) + + this.afterAll(function * () { + yield profilerUtil.uploadTravisArtifacts() + }) + + describe('adding a bookmark', function () { + function * runPreStory () { + yield Brave.app.client + .waitForUrl(Brave.newTabUrl, 10000, 250) + .loadUrl(this.page1Url) + .windowParentByUrl(this.page1Url) + .activateURLMode() + .waitForVisible(navigatorNotBookmarked) + } + + function * runStory () { + yield Brave.app.client + .click(navigatorNotBookmarked) + .waitForVisible(doneButton) + .waitForBookmarkDetail(this.page1Url, 'Page 1') + .waitForEnabled(doneButton) + .click(doneButton) + .activateURLMode() + .waitForVisible(navigatorBookmarked) + } + + it('fresh', function * () { + // restart to keep it consistent with other tests + yield restart() + yield runPreStory.call(this) + yield profilerUtil.profile(runStory.bind(this), 'bookmarks--add-bookmark--fresh') + }) + + it('4000 bookmarks', function * () { + yield userProfiles.addBookmarks4000(Brave.app.client) + yield restart() + yield runPreStory.call(this) + yield profilerUtil.profile(runStory.bind(this), 'bookmarks--add-bookmark--4000-bookmarks') + }) + + it('100 tabs', function * () { + yield userProfiles.addTabs100(Brave.app.client) + yield restart() + yield runPreStory.call(this) + yield profilerUtil.profile(runStory.bind(this), 'bookmarks--add-bookmark--100-tabs') + }) + }) +}) diff --git a/test/performance/startupTest.js b/test/performance/startupTest.js new file mode 100644 index 00000000000..9672705a75d --- /dev/null +++ b/test/performance/startupTest.js @@ -0,0 +1,83 @@ +/* global describe, it, beforeEach, afterEach */ + +const Brave = require('../lib/brave') +const profilerUtil = require('../lib/profilerUtil') +const {urlInput} = require('../lib/selectors') +const userProfiles = require('../lib/userProfiles') + +describe('Performance startup', function () { + Brave.beforeAllServerSetup(this) + let inspectPort = 9223 + + function * setup () { + Brave.addCommands() + } + + function * setupBrave (enableInspector) { + const extraArgs = [] + if (enableInspector) { + extraArgs.push(`--debug=${inspectPort}`) + inspectPort += 1 + } + yield Brave.startApp(extraArgs) + yield setup(Brave.app.client) + } + + function * restart (timeout = 3000) { + yield Brave.stopApp(false, timeout) + yield setupBrave(true) + } + + beforeEach(function * () { + this.url = Brave.server.url('page1.html') + yield setupBrave() + }) + + afterEach(function * () { + yield Brave.stopApp() + }) + + this.afterAll(function * () { + yield profilerUtil.uploadTravisArtifacts() + }) + + describe('type a URL and navigate', function () { + function * runStory () { + yield Brave.app.client + .waitForBrowserWindow() + .waitForUrl(Brave.newTabUrl, 10000, 250) + .waitForBrowserWindow() + .windowByUrl(Brave.browserWindowUrl) + .ipcSend('shortcut-focus-url') + .waitForVisible(urlInput) + .waitForElementFocus(urlInput) + .pause(500) + for (let i = 0; i < this.url.length; i++) { + yield Brave.app.client + .keys(this.url[i]) + .pause(30) + } + yield Brave.app.client + .keys(Brave.keys.ENTER) + .waitForUrl(this.url) + } + + it('fresh', function * () { + // restart to keep it consistent with other tests + yield restart() + yield profilerUtil.profile(runStory.bind(this), 'startup--navigate-manually--fresh') + }) + + it('4000 bookmarks', function * () { + yield userProfiles.addBookmarks4000(Brave.app.client) + yield restart() + yield profilerUtil.profile(runStory.bind(this), 'startup--navigate-manually--4000-bookmarks') + }) + + it('100 tabs', function * () { + yield userProfiles.addTabs100(Brave.app.client) + yield restart() + yield profilerUtil.profile(runStory.bind(this), 'startup--navigate-manually--100-tabs') + }) + }) +}) diff --git a/tools/downloadMuonDebugBuild.js b/tools/downloadMuonDebugBuild.js new file mode 100644 index 00000000000..548a9ee4f35 --- /dev/null +++ b/tools/downloadMuonDebugBuild.js @@ -0,0 +1,32 @@ +'use strict' + +const path = require('path') +const fs = require('fs') +const request = require('request') +const execute = require('./lib/execute') + +const muonFolder = path.join(__dirname, '..', 'node_modules', 'electron-prebuilt') +const muonFolderDist = path.join(muonFolder, 'dist') +const muonVersion = 'latest' // XXX +const downloadUrl = `https://brave-test-builds.s3.amazonaws.com/muon/brave-debug-${muonVersion}-linux-x64.zip` +const localPath = path.join(muonFolder, `brave-debug-${muonVersion}-linux-x64.zip`) + +console.log(`downloading muon debug build: ${downloadUrl}`) +request + .get(downloadUrl) + .on('error', function (error) { + console.log('could not get', downloadUrl, error) + process.exit(1) + }) + .on('end', function (response) { + console.log('muon download finished') + execute(`unzip -o -q -d ${muonFolderDist} ${localPath}`, process.env, (err) => { + if (err) { + console.error('failed', err) + process.exit(1) + return + } + console.log('done') + }) + }) + .pipe(fs.createWriteStream(localPath)) diff --git a/tools/test.js b/tools/test.js index 4c72cb826fa..ea1721e1d49 100644 --- a/tools/test.js +++ b/tools/test.js @@ -15,7 +15,14 @@ switch (TEST_DIR) { case 'codecov': cmd.push('bash tools/codecov.sh') break - default: + case 'performance': + // 2017-09-28 Use debug builds for tests which require muon to run with + // --debug, currently broken in Linux on prod muon builds. + if (process.platform === 'linux') { + cmd.push('node tools/downloadMuonDebugBuild.js') + } + // Intentionally no break, because perf tests also run the below + default: // eslint-disable-line cmd.push(`mocha "test/${TEST_DIR}/**/*Test.js"`) }