From 4dd8743e7e533d8cc0fe9629e3c1eb916c55c6e9 Mon Sep 17 00:00:00 2001 From: amit3200 Date: Mon, 5 Jun 2023 02:22:58 +0530 Subject: [PATCH] Adding Optimizations for Screenshot and all --- packages/client/src/client.js | 3 +- packages/core/src/api.js | 17 ++- packages/webdriver-utils/src/index.js | 7 +- .../src/metadata/desktopMetaData.js | 13 +- .../src/providers/automateProvider.js | 111 ++++++++++++++++++ .../src/providers/genericProvider.js | 33 ++++-- .../src/providers/providerResolver.js | 4 +- packages/webdriver-utils/src/util/timing.js | 54 +++++++++ 8 files changed, 222 insertions(+), 20 deletions(-) create mode 100644 packages/webdriver-utils/src/util/timing.js diff --git a/packages/client/src/client.js b/packages/client/src/client.js index 19cfcdf83..7f6e107da 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -368,7 +368,8 @@ export class PercyClient { async createComparison(snapshotId, { tag, tiles = [], externalDebugUrl, ignoredElementsData } = {}) { validateId('snapshot', snapshotId); - + // Remove post percy api deploy + externalDebugUrl = externalDebugUrl.replace('automate', 'app-automate'); this.log.debug(`Creating comparision: ${tag.name}...`); for (let tile of tiles) { diff --git a/packages/core/src/api.js b/packages/core/src/api.js index 848e836f3..82cf2f22d 100644 --- a/packages/core/src/api.js +++ b/packages/core/src/api.js @@ -117,9 +117,20 @@ export function createPercyServer(percy, port) { .route('post', '/percy/flush', async (req, res) => res.json(200, { success: await percy.flush(req.body).then(() => true) })) - .route('post', '/percy/automateScreenshot', async (req, res) => res.json(200, { - success: await (percy.upload(await new WebdriverUtils(req.body).automateScreenshot())).then(() => true) - })) + .route('post', '/percy/automateScreenshot', async (req, res) => { + if (req.body.client_info) { + req.body.clientInfo = req.body.client_info; + } + if (req.body.environment_info) { + req.body.environmentInfo = req.body.environment_info; + } + if (!req.body.options) { + req.body.options = {}; + } + res.json(200, { + success: await (percy.upload(await new WebdriverUtils(req.body).automateScreenshot())).then(() => true) + }); + }) // stops percy at the end of the current event loop .route('/percy/stop', (req, res) => { setImmediate(() => percy.stop()); diff --git a/packages/webdriver-utils/src/index.js b/packages/webdriver-utils/src/index.js index 35c3ed8b8..a0f89b134 100644 --- a/packages/webdriver-utils/src/index.js +++ b/packages/webdriver-utils/src/index.js @@ -3,17 +3,20 @@ import utils from '@percy/sdk-utils'; export default class WebdriverUtils { log = utils.logger('webdriver-utils:main'); - constructor({ sessionId, commandExecutorUrl, capabilities, sessionCapabilites, snapshotName }) { + constructor({ sessionId, commandExecutorUrl, capabilities, sessionCapabilites, snapshotName, clientInfo, environmentInfo, options }) { this.sessionId = sessionId; this.commandExecutorUrl = commandExecutorUrl; this.capabilities = capabilities; this.sessionCapabilites = sessionCapabilites; this.snapshotName = snapshotName; + this.clientInfo = clientInfo; + this.environmentInfo = environmentInfo; + this.options = options; } async automateScreenshot() { this.log.info('Starting automate screenshot'); - const automate = ProviderResolver.resolve(this.sessionId, this.commandExecutorUrl, this.capabilities, this.sessionCapabilites); + const automate = ProviderResolver.resolve(this.sessionId, this.commandExecutorUrl, this.capabilities, this.sessionCapabilites, this.clientInfo, this.environmentInfo, this.options); await automate.createDriver(); return await automate.screenshot(this.snapshotName); } diff --git a/packages/webdriver-utils/src/metadata/desktopMetaData.js b/packages/webdriver-utils/src/metadata/desktopMetaData.js index 0977060b4..b4fe64419 100644 --- a/packages/webdriver-utils/src/metadata/desktopMetaData.js +++ b/packages/webdriver-utils/src/metadata/desktopMetaData.js @@ -8,21 +8,26 @@ export default class DesktopMetaData { return this.capabilities.browserName.toLowerCase(); } + browserVersion() { + return this.capabilities.browserVersion.split('.')[0]; + } + osName() { - let osName = this.capabilities.osVersion; + let osName = this.capabilities.os; if (osName) return osName.toLowerCase(); osName = this.capabilities.platform; return osName; } - // desktop will show this as browser version + // showing major version osVersion() { - return this.capabilities.version.split('.')[0]; + return this.capabilities.osVersion.toLowerCase(); } + // combination of browserName + browserVersion + osVersion + osName deviceName() { - return this.browserName() + '_' + this.osVersion() + '_' + this.osName(); + return this.browserName() + '_' + this.browserVersion() + '_' + this.osVersion() + '_' + this.osName(); } orientation() { diff --git a/packages/webdriver-utils/src/providers/automateProvider.js b/packages/webdriver-utils/src/providers/automateProvider.js index 0369ac592..35051d484 100644 --- a/packages/webdriver-utils/src/providers/automateProvider.js +++ b/packages/webdriver-utils/src/providers/automateProvider.js @@ -1,11 +1,122 @@ +import utils from '@percy/sdk-utils'; import GenericProvider from './genericProvider.js'; import Cache from '../util/cache.js'; +import Tile from '../util/tile.js'; +import TimeIt from '../util/timing.js'; +const log = utils.logger('webdriver-utils:automateProvider'); export default class AutomateProvider extends GenericProvider { + constructor( + sessionId, + commandExecutorUrl, + capabilities, + sessionCapabilites, + clientInfo, + environmentInfo, + options + ) { + super( + sessionId, + commandExecutorUrl, + capabilities, + sessionCapabilites, + clientInfo, + environmentInfo, + options + ); + this._markedPercy = false; + } + static supports(commandExecutorUrl) { return commandExecutorUrl.includes(process.env.AA_DOMAIN || 'browserstack'); } + async screenshot(name) { + let response = null; + let error; + try { + let result = await this.percyScreenshotBegin(name); + this.setDebugUrl(result); + response = await super.screenshot(name); + } catch (e) { + error = e; + throw e; + } finally { + await this.percyScreenshotEnd(name, response?.body?.link, `${error}`); + } + return response; + } + + async percyScreenshotBegin(name) { + return await TimeIt.run('percyScreenshotBegin', async () => { + try { + let result = await this.browserstackExecutor('percyScreenshot', { + name, + percyBuildId: process.env.PERCY_BUILD_ID, + percyBuildUrl: process.env.PERCY_BUILD_URL, + state: 'begin' + }); + this._markedPercy = result.success; + return result; + } catch (e) { + log.debug(`[${name}] Could not mark Automate session as percy`); + return null; + } + }); + } + + async percyScreenshotEnd(name, percyScreenshotUrl, statusMessage = null) { + return await TimeIt.run('percyScreenshotEnd', async () => { + try { + await this.browserstackExecutor('percyScreenshot', { + name, + percyScreenshotUrl, + status: percyScreenshotUrl ? 'success' : 'failure', + statusMessage, + state: 'end' + }); + } catch (e) { + log.debug(`[${name}] Could not mark App Automate session as percy`); + } + }); + } + + async getTiles(fullscreen) { + if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().'); + + const response = await TimeIt.run('percyScreenshot:screenshot', async () => { + return await this.browserstackExecutor('percyScreenshot', { + state: 'screenshot', + percyBuildId: process.env.PERCY_BUILD_ID, + screenshotType: 'singlepage', + scaleFactor: await this.driver.executeScript({ script: 'return window.devicePixelRatio;', args: [] }), + options: this.options + }); + }); + + const responseValue = JSON.parse(response.value); + if (!responseValue.success) { + throw new Error('Failed to get screenshots from Automate.' + + ' Check dashboard for error.'); + } + + const tiles = []; + const tileResponse = JSON.parse(responseValue.result); + + for (let tileData of tileResponse.sha) { + tiles.push(new Tile({ + statusBarHeight: 0, + navBarHeight: 0, + headerHeight: 0, + footerHeight: 0, + fullscreen, + sha: tileData.split('-')[0] // drop build id + // dom-sha: tileData.dom_sha + })); + } + return tiles; + } + async browserstackExecutor(action, args) { if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().'); let options = args ? { action, arguments: args } : { action }; diff --git a/packages/webdriver-utils/src/providers/genericProvider.js b/packages/webdriver-utils/src/providers/genericProvider.js index da0c5204c..75b8a9d52 100644 --- a/packages/webdriver-utils/src/providers/genericProvider.js +++ b/packages/webdriver-utils/src/providers/genericProvider.js @@ -5,21 +5,27 @@ import Tile from '../util/tile.js'; import Driver from '../driver.js'; const log = utils.logger('webdriver-utils:genericProvider'); -// TODO: Need to pass parameter from sdk and catch in cli -const CLIENT_INFO = 'local-poc-poa'; -const ENV_INFO = 'staging-poc-poa'; export default class GenericProvider { + clientInfo = new Set(); + environmentInfo = new Set(); + options = {}; constructor( sessionId, commandExecutorUrl, capabilities, - sessionCapabilites + sessionCapabilites, + clientInfo, + environmentInfo, + options ) { this.sessionId = sessionId; this.commandExecutorUrl = commandExecutorUrl; this.capabilities = capabilities; this.sessionCapabilites = sessionCapabilites; + this.addClientInfo(clientInfo); + this.addEnvironmentInfo(environmentInfo); + this.options = options; this.driver = null; this.metaData = null; this.debugUrl = null; @@ -35,6 +41,18 @@ export default class GenericProvider { return true; } + addClientInfo(info) { + for (let i of [].concat(info)) { + if (i) this.clientInfo.add(i); + } + } + + addEnvironmentInfo(info) { + for (let i of [].concat(info)) { + if (i) this.environmentInfo.add(i); + } + } + async screenshot(name) { let fullscreen = false; @@ -51,8 +69,8 @@ export default class GenericProvider { tiles, // TODO: Fetch this one for bs automate, check appium sdk externalDebugUrl: this.debugUrl, - environmentInfo: ENV_INFO, - clientInfo: CLIENT_INFO + environmentInfo: [...this.environmentInfo].join('; '), + clientInfo: [...this.clientInfo].join(' ') }; } @@ -84,8 +102,7 @@ export default class GenericProvider { height, orientation: orientation, browserName: this.metaData.browserName(), - // TODO - browserVersion: 'unknown' + browserVersion: this.metaData.browserVersion() }; } diff --git a/packages/webdriver-utils/src/providers/providerResolver.js b/packages/webdriver-utils/src/providers/providerResolver.js index d0d3c6a92..b7f446b61 100644 --- a/packages/webdriver-utils/src/providers/providerResolver.js +++ b/packages/webdriver-utils/src/providers/providerResolver.js @@ -2,9 +2,9 @@ import GenericProvider from './genericProvider.js'; import AutomateProvider from './automateProvider.js'; export default class ProviderResolver { - static resolve(sessionId, commandExecutorUrl, capabilities, sessionCapabilities) { + static resolve(sessionId, commandExecutorUrl, capabilities, sessionCapabilities, clientInfo, environmentInfo, options) { // We can safely do [0] because GenericProvider is catch all const Klass = [AutomateProvider, GenericProvider].filter(x => x.supports(commandExecutorUrl))[0]; - return new Klass(sessionId, commandExecutorUrl, capabilities, sessionCapabilities); + return new Klass(sessionId, commandExecutorUrl, capabilities, sessionCapabilities, clientInfo, environmentInfo, options); } } diff --git a/packages/webdriver-utils/src/util/timing.js b/packages/webdriver-utils/src/util/timing.js new file mode 100644 index 000000000..c07c6ebe4 --- /dev/null +++ b/packages/webdriver-utils/src/util/timing.js @@ -0,0 +1,54 @@ +import validations from './validations.js'; +const { Undefined } = validations; + +export default class TimeIt { + static data = {}; + + static enabled = process.env.PERCY_METRICS === 'true'; + + static async run(store, func) { + if (!this.enabled) return await func(); + + const t1 = Date.now(); + try { + return await func(); + } finally { + if (Undefined(this.data[store])) this.data[store] = []; + this.data[store].push(Date.now() - t1); + } + } + + static min(store) { + return Math.min(...this.data[store]); + } + + static max(store) { + return Math.max(...this.data[store]); + } + + static avg(store) { + const vals = this.data[store]; + + return vals.reduce((a, b) => a + b, 0) / vals.length; + } + + static summary({ + includeVals + } = {}) { + const agg = {}; + for (const key of Object.keys(this.data)) { + agg[key] = { + min: this.min(key), + max: this.max(key), + avg: this.avg(key), + count: this.data[key].length + }; + if (includeVals) agg[key].vals = this.data[key]; + } + return agg; + } + + static reset() { + this.data = {}; + } +};