How to use requireXctrace method in Appium Xcuitest Driver

Best JavaScript code snippet using appium-xcuitest-driver

Run Appium Xcuitest Driver automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Sign up Free
_

performance.js

Source: performance.js Github

copy
1import _ from 'lodash';
2import path from 'path';
3import { fs, zip, logger, util } from 'appium-support';
4import { SubProcess, exec } from 'teen_process';
5import log from '../logger';
6import { encodeBase64OrUpload } from '../utils';
7import { waitForCondition } from 'asyncbox';
8import os from 'os';
9
10const commands = {};
11
12const PERF_RECORD_FEAT_NAME = 'perf_record';
13const PERF_RECORD_SECURITY_MESSAGE = 'Performance measurement requires relaxing security for simulator. ' +
14  `Please set '--relaxed-security' or '--allow-insecure' with '${PERF_RECORD_FEAT_NAME}' ` +
15  'referencing https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/security.md for more details.';
16const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
17const STOP_TIMEOUT_MS = 3 * 60 * 1000;
18const STARTUP_TIMEOUT_MS = 60 * 1000;
19const DEFAULT_PROFILE_NAME = 'Activity Monitor';
20const DEFAULT_EXT = 'trace';
21const DEFAULT_PID = 'current';
22const INSTRUMENTS = 'instruments';
23const XCRUN = 'xcrun';
24const XCTRACE = 'xctrace';
25
26
27async function requireXctrace () {
28  let xcrunPath;
29  try {
30    xcrunPath = await fs.which(XCRUN);
31  } catch (e) {
32    log.errorAndThrow(`${XCRUN} has not been found in PATH. ` +
33      `Please make sure XCode development tools are installed`);
34  }
35  try {
36    await exec(xcrunPath, [XCTRACE, 'help']);
37  } catch (e) {
38    log.errorAndThrow(`${XCTRACE} is not available for the active Xcode version. ` +
39      `Please make sure XCode is up to date. Original error: ${e.stderr || e.message}`);
40  }
41  return xcrunPath;
42}
43
44async function requireInstruments () {
45  try {
46    return await fs.which(INSTRUMENTS);
47  } catch (e) {
48    log.errorAndThrow(`${INSTRUMENTS} has not been found in PATH. ` +
49      `Please make sure XCode development tools are installed`);
50  }
51}
52
53
54class PerfRecorder {
55  constructor (reportPath, udid, opts = {}) {
56    this._process = null;
57    this._reportPath = reportPath;
58    this._zippedReportPath = '';
59    this._timeout = (opts.timeout && opts.timeout > 0) ? opts.timeout : DEFAULT_TIMEOUT_MS;
60    this._profileName = opts.profileName || DEFAULT_PROFILE_NAME;
61    this._pid = opts.pid;
62    this._udid = udid;
63    this._logger = logger.getLogger(
64      `${_.truncate(this._profileName, {length: 10})}@${this._udid.substring(0, 8)}`);
65    this._archivePromise = null;
66  }
67
68  get profileName () {
69    return this._profileName;
70  }
71
72  async getOriginalReportPath () {
73    return (await fs.exists(this._reportPath)) ? this._reportPath : '';
74  }
75
76  async getZippedReportPath () {
77    if (await fs.exists(this._zippedReportPath)) {
78      return this._zippedReportPath;
79    }
80    const originalReportPath = await this.getOriginalReportPath();
81    if (!originalReportPath) {
82      return '';
83    }
84    const zippedReportPath = originalReportPath.replace(`.${DEFAULT_EXT}`, '.zip');
85    // This is to prevent possible race conditions, because the archive operation
86    // could be pretty time-intensive
87    if (!this._archivePromise) {
88      this._archivePromise = zip.toArchive(zippedReportPath, {
89        cwd: originalReportPath,
90      });
91    }
92    await this._archivePromise;
93    this._zippedReportPath = zippedReportPath;
94    return this._zippedReportPath;
95  }
96
97  isRunning () {
98    return !!(this._process?.isRunning);
99  }
100
101  async _enforceTermination () {
102    if (this._process && this.isRunning()) {
103      this._logger.debug('Force-stopping the currently running perf recording');
104      try {
105        await this._process.stop('SIGKILL');
106      } catch (ign) {}
107    }
108    this._process = null;
109    if (this._archivePromise) {
110      this._archivePromise
111        // eslint-disable-next-line promise/prefer-await-to-then
112        .then(() => fs.rimraf(this._zippedReportPath))
113        .finally(() => {
114          this._archivePromise = null;
115        })
116        .catch(() => {});
117    } else {
118      await fs.rimraf(this._zippedReportPath);
119    }
120    await fs.rimraf(this._reportPath);
121    return '';
122  }
123
124  async start () {
125    let binaryPath;
126    try {
127      binaryPath = await requireXctrace();
128    } catch (e) {
129      log.debug(e.message);
130      log.info(`Defaulting to ${INSTRUMENTS} usage`);
131      binaryPath = await requireInstruments();
132    }
133
134    const args = [];
135    const toolName = path.basename(binaryPath) === XCRUN ? XCTRACE : INSTRUMENTS;
136    if (toolName === XCTRACE) {
137      args.push(
138        XCTRACE, 'record',
139        '--device', this._udid,
140        '--template', this._profileName,
141        '--output', this._reportPath,
142        '--time-limit', `${this._timeout}ms`,
143      );
144      if (this._pid) {
145        args.push('--attach', `${this._pid}`);
146      } else {
147        args.push('--all-processes');
148      }
149    } else {
150      // https://help.apple.com/instruments/mac/current/#/devb14ffaa5
151      args.push(
152        '-w', this._udid,
153        '-t', this._profileName,
154        '-D', this._reportPath,
155        '-l', `${this._timeout}`,
156      );
157      if (this._pid) {
158        args.push('-p', `${this._pid}`);
159      }
160    }
161    const fullCmd = [binaryPath, ...args];
162    this._process = new SubProcess(fullCmd[0], fullCmd.slice(1));
163    this._archivePromise = null;
164    this._logger.debug(`Starting performance recording: ${util.quote(fullCmd)}`);
165    this._process.on('output', (stdout, stderr) => {
166      if (_.trim(stdout || stderr)) {
167        this._logger.debug(`[${toolName}] ${stdout || stderr}`);
168      }
169    });
170    this._process.once('exit', async (code, signal) => {
171      this._process = null;
172      if (code === 0) {
173        this._logger.debug('Performance recording exited without errors');
174        try {
175          // cache zipped report
176          await this.getZippedReportPath();
177        } catch (e) {
178          this._logger.warn(e);
179        }
180      } else {
181        await this._enforceTermination();
182        this._logger.warn(`Performance recording exited with error code ${code}, signal ${signal}`);
183      }
184    });
185    await this._process.start(0);
186    try {
187      await waitForCondition(async () => {
188        if (await this.getOriginalReportPath()) {
189          return true;
190        }
191        if (!this._process) {
192          throw new Error(`${toolName} process died unexpectedly`);
193        }
194        return false;
195      }, {
196        waitMs: STARTUP_TIMEOUT_MS,
197        intervalMs: 500,
198      });
199    } catch (e) {
200      await this._enforceTermination();
201      const listProfilesCommand = toolName === XCTRACE
202        ? `${XCRUN} ${XCTRACE} list templates`
203        : `${INSTRUMENTS} -s`;
204      this._logger.errorAndThrow(`There is no .${DEFAULT_EXT} file found for performance profile ` +
205        `'${this._profileName}'. Make sure the profile is supported on this device. ` +
206        `You could use '${listProfilesCommand}' command to see the list of all available profiles. ` +
207        `Check the server log for more details`);
208    }
209    this._logger.info(`The performance recording has started. Will timeout in ${this._timeout}ms`);
210  }
211
212  async stop (force = false) {
213    if (force) {
214      return await this._enforceTermination();
215    }
216
217    if (!this.isRunning()) {
218      this._logger.debug('Performance recording is not running. Returning the recent result');
219      return await this.getZippedReportPath();
220    }
221
222    try {
223      await this._process.stop('SIGINT', STOP_TIMEOUT_MS);
224    } catch (e) {
225      this._logger.errorAndThrow(`Performance recording has failed to exit after ${STOP_TIMEOUT_MS}ms`);
226    }
227    return await this.getZippedReportPath();
228  }
229}
230
231
232/**
233 * @typedef {Object} StartPerfRecordOptions
234 *
235 * @property {?number|string} timeout [300000] - The maximum count of milliseconds to record the profiling information.
236 * @property {?string} profileName [Activity Monitor] - The name of existing performance profile to apply.
237 *                                                      Can also contain the full path to the chosen template on the server file system.
238 *                                                      Note, that not all profiles are supported on mobile devices.
239 * @property {?string|number} pid - The ID of the process to measure the performance for.
240 *                                  Set it to `current` in order to measure the performance of
241 *                                  the process, which belongs to the currently active application.
242 *                                  All processes running on the device are measured if
243 *                                  pid is unset (the default setting).
244 */
245
246/**
247 * Starts performance profiling for the device under test.
248 * Relaxing security is mandatory for simulators. It can always work for real devices.
249 *
250 * Since XCode 14 the method tries to use `xctrace` tool to record performance stats.
251 * The `instruments` developer utility is used as a fallback for this purpose if `xctrace`
252 * is not available.
253 * It is possible to record multiple profiles at the same time.
254 * Read https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/Recording,Pausing,andStoppingTraces.html
255 * for more details.
256 *
257 * @param {?StartPerfRecordOptions} opts - The set of possible start record options
258 */
259commands.mobileStartPerfRecord = async function mobileStartPerfRecord (opts = {}) {
260  if (!this.isFeatureEnabled(PERF_RECORD_FEAT_NAME) && !this.isRealDevice()) {
261    log.errorAndThrow(PERF_RECORD_SECURITY_MESSAGE);
262  }
263
264  const {
265    timeout = DEFAULT_TIMEOUT_MS,
266    profileName = DEFAULT_PROFILE_NAME,
267    pid,
268  } = opts;
269
270  if (!_.isEmpty(this._perfRecorders)) {
271    const recorders = this._perfRecorders
272      .filter((x) => x.profileName === profileName);
273    if (!_.isEmpty(recorders)) {
274      for (const recorder of recorders) {
275        if (recorder.isRunning()) {
276          log.debug(`Performance recorder for '${profileName}' on device '${this.opts.device.udid}' ` +
277            ` is already running. Doing nothing`);
278          return;
279        }
280        _.pull(this._perfRecorders, recorder);
281        await recorder.stop(true);
282      }
283    }
284  }
285
286  const reportPath = path.resolve(os.tmpdir(),
287    `appium_perf_${profileName.replace(/\W/g, '_')}_${util.uuidV4().substring(0, 8)}.${DEFAULT_EXT}`);
288  let realPid;
289  if (pid) {
290    if (_.toLower(pid) === DEFAULT_PID) {
291      const appInfo = await this.proxyCommand('/wda/activeAppInfo', 'GET');
292      realPid = appInfo.pid;
293    } else {
294      realPid = pid;
295    }
296  }
297  const recorder = new PerfRecorder(reportPath, this.opts.device.udid, {
298    timeout: parseInt(timeout, 10),
299    profileName,
300    pid: parseInt(realPid, 10),
301  });
302  await recorder.start();
303  this._perfRecorders = [...(this._perfRecorders || []), recorder];
304};
305
306/**
307 * @typedef {Object} StopRecordingOptions
308 *
309 * @property {?string} remotePath - The path to the remote location, where the resulting zipped .trace file should be uploaded.
310 *                                  The following protocols are supported: http/https, ftp.
311 *                                  Null or empty string value (the default setting) means the content of resulting
312 *                                  file should be zipped, encoded as Base64 and passed as the endpoint response value.
313 *                                  An exception will be thrown if the generated file is too big to
314 *                                  fit into the available process memory.
315 * @property {?string} user - The name of the user for the remote authentication. Only works if `remotePath` is provided.
316 * @property {?string} pass - The password for the remote authentication. Only works if `remotePath` is provided.
317 * @property {?string} method [PUT] - The http multipart upload method name. Only works if `remotePath` is provided.
318 * @property {?string} profileName [Activity Monitor] - The name of an existing performance profile for which the recording has been made.
319 * @property {?Object} headers - Additional headers mapping for multipart http(s) uploads
320 * @property {?string} fileFieldName [file] - The name of the form field, where the file content BLOB should be stored for
321 *                                            http(s) uploads
322 * @property {?Object|Array<Pair>} formFields - Additional form fields for multipart http(s) uploads
323 */
324
325/**
326 * Stops performance profiling for the device under test.
327 * The resulting file in .trace format can be either returned
328 * directly as base64-encoded zip archive or uploaded to a remote location
329 * (such files can be pretty large). Afterwards it is possible to unarchive and
330 * open such file with Xcode Dev Tools.
331 *
332 * @param {?StopRecordingOptions} opts - The set of possible stop record options
333 * @return {string} Either an empty string if the upload was successful or base-64 encoded
334 * content of zipped .trace file.
335 * @throws {Error} If no performance recording with given profile name/device udid combination
336 * has been started before or the resulting .trace file has not been generated properly.
337 */
338commands.mobileStopPerfRecord = async function mobileStopPerfRecord (opts = {}) {
339  if (!this.isFeatureEnabled(PERF_RECORD_FEAT_NAME) && !this.isRealDevice()) {
340    log.errorAndThrow(PERF_RECORD_SECURITY_MESSAGE);
341  }
342
343  if (_.isEmpty(this._perfRecorders)) {
344    log.info('No performance recorders have been started. Doing nothing');
345    return '';
346  }
347
348  const {
349    profileName = DEFAULT_PROFILE_NAME,
350    remotePath,
351  } = opts;
352
353  const recorders = this._perfRecorders.filter((x) => x.profileName === profileName);
354  if (_.isEmpty(recorders)) {
355    log.errorAndThrow(`There are no records for performance profile '${profileName}' ` +
356      `and device ${this.opts.device.udid}. Have you started the profiling before?`);
357  }
358  const resultPath = await recorders[0].stop();
359  if (!await fs.exists(resultPath)) {
360    log.errorAndThrow(`There is no .${DEFAULT_EXT} file found for performance profile '${profileName}' ` +
361      `and device ${this.opts.device.udid}. Make sure the selected profile is supported on this device`);
362  }
363
364  return await encodeBase64OrUpload(resultPath, remotePath, opts);
365};
366
367
368export { commands };
369export default commands;
370
Full Screen

Accelerate Your Automation Test Cycles With LambdaTest

Leverage LambdaTest’s cloud-based platform to execute your automation tests in parallel and trim down your test execution time significantly. Your first 100 automation testing minutes are on us.

Try LambdaTest

Run JavaScript Tests on LambdaTest Cloud Grid

Execute automation tests with Appium Xcuitest Driver on a cloud-based Grid of 3000+ real browsers and operating systems for both web and mobile applications.

Test now for Free
LambdaTestX

We use cookies to give you the best experience. Cookies help to provide a more personalized experience and relevant advertising for you, and web analytics for us. Learn More in our Cookies policy, Privacy & Terms of service

Allow Cookie
Sarah

I hope you find the best code examples for your project.

If you want to accelerate automated browser testing, try LambdaTest. Your first 100 automation testing minutes are FREE.

Sarah Elson (Product & Growth Lead)