class RunLoop::Instruments

A class for interacting with the instruments command-line tool

@note All instruments commands are run in the context of ‘xcrun`.

Constants

INSTRUMENTS_FIND_PIDS_CMD

@!visibility private

“‘ $ ps x -o pid,command | grep -v grep | grep instruments 98081 sh -c xcrun instruments -w “43be3f89d9587e9468c24672777ff6241bd91124” < args > 98082 /Xcode/6.0.1/Xcode.app/Contents/Developer/usr/bin/instruments -w < args > “`

When run from run-loop (via rspec), expect this:

“‘ $ ps x -o pid,command | grep -v grep | grep instruments 98082 /Xcode/6.0.1/Xcode.app/Contents/Developer/usr/bin/instruments -w < args > “`

Attributes

xcode[R]

Public Class Methods

rotate_cache_directories() click to toggle source

@!visibility private

Rotates xrtmp__ directories in /Library/Caches/com.apple.dt.instruments keeping the last 5. On CI systems these can be hundreds of gigabytes.

# File lib/run_loop/instruments.rb, line 14
def self.rotate_cache_directories

  # Never run on the XTC
  return :xtc if RunLoop::Environment.xtc?

  cache = self.library_cache_dir

  # If the directory does not exist, do nothing.
  return :no_cache if !cache || !File.exist?(cache)

  start = Time.now

  glob = "#{cache}/xrtmp__*"

  RunLoop.log_debug("Searching for instruments caches with glob: #{glob}")

  directories = Dir.glob(glob).select do |path|
    File.directory?(path)
  end

  log_progress = false
  if directories.count > 25
    RunLoop.log_info2("Found #{directories.count} instruments caches: ~#{20 * directories.count} Mb")
    RunLoop.log_info2("Deleting them could take a long time.")
    RunLoop.log_info2("This delay will only happen once!")
    RunLoop.log_info2("Please be patient and allow the directories to be deleted")
    log_progress = true
  else
    RunLoop.log_debug("Found #{directories.count} instruments caches")
  end

  if log_progress
    RunLoop.log_info2("Sorting instruments caches by modification time...")
  end

  oldest_first = directories.sort_by { |f| File.mtime(f) }

  oldest_first.pop(5)

  if log_progress
    RunLoop.log_info2("Will delete #{oldest_first.count} instruments caches...")
  else
    RunLoop.log_debug("Will delete #{oldest_first.count} instruments caches")
  end

  oldest_first.each do |path|
    FileUtils.rm_rf(path)
    if log_progress
      printf "."
    end
  end

  elapsed = Time.now - start

  if log_progress
    puts ""
    RunLoop.log_info2("Deleted #{oldest_first.count} instruments caches in #{elapsed} seconds")
  else
    RunLoop.log_debug("Deleted #{oldest_first.count} instruments caches in #{elapsed} seconds")
  end
  true
end

Private Class Methods

library_cache_dir() click to toggle source

@!visibility private

Instruments caches files in this directory and it can become quite large over time; particularly on CI system.

# File lib/run_loop/instruments.rb, line 433
def self.library_cache_dir
  path = "/Library/Caches/com.apple.dt.instruments"

  if File.exist?(path)
    path
  else
    nil
  end
end

Public Instance Methods

inspect() click to toggle source

@!visibility private

# File lib/run_loop/instruments.rb, line 97
def inspect
  to_s
end
instruments_app_running?() click to toggle source

Is the Instruments.app running?

If the Instruments.app is running, the instruments command line tool cannot take control of applications.

# File lib/run_loop/instruments.rb, line 141
def instruments_app_running?
  ps_output = `ps x -o pid,comm | grep Instruments.app | grep -v grep`.strip
  if ps_output[/Instruments\.app/, 0]
    true
  else
    false
  end
end
instruments_pids(&block) click to toggle source

Returns an Array of instruments process ids.

@note The ‘block` parameter is included for legacy API and will be

deprecated.  Replace your existing calls with with .each or .map.  The
block argument makes this method hard to mock.

@return [Array<Integer>] An array of instruments process ids.

# File lib/run_loop/instruments.rb, line 107
def instruments_pids(&block)
  pids = pids_from_ps_output
  if block_given?
    pids.each do |pid|
      block.call(pid)
    end
  else
    pids
  end
end
instruments_running?() click to toggle source

Are there any instruments processes running? @return [Boolean] True if there is are any instruments processes running.

# File lib/run_loop/instruments.rb, line 120
def instruments_running?
  instruments_pids.count > 0
end
kill_instruments(_=nil) click to toggle source

Send a kill signal to any running ‘instruments` processes.

Only one instruments process can be running at any one time.

# File lib/run_loop/instruments.rb, line 127
def kill_instruments(_=nil)
  instruments_pids.each do |pid|
    terminator = RunLoop::ProcessTerminator.new(pid, "QUIT", "instruments")
    unless terminator.kill_process
      terminator = RunLoop::ProcessTerminator.new(pid, "KILL", "instruments")
      terminator.kill_process
    end
  end
end
pbuddy() click to toggle source
# File lib/run_loop/instruments.rb, line 79
def pbuddy
  @pbuddy ||= RunLoop::PlistBuddy.new
end
physical_devices() click to toggle source

Returns an array of the available physical devices.

@return [Array<RunLoop::Device>] All the devices will be physical

devices.
# File lib/run_loop/instruments.rb, line 216
def physical_devices
  @instruments_physical_devices ||= lambda do
    fetch_devices[:out].chomp.split("\n").map do |line|
      udid = line[DEVICE_UDID_REGEX, 0]
      if udid
        version = line[VERSION_REGEX, 0]
        if version
          name = line.split('(').first.strip
          if name
            RunLoop::Device.new(name, version, udid)
          end
        end
      else
        nil
      end
    end.compact
  end.call
end
simulators() click to toggle source

Returns an array of the available simulators.

**Xcode 5.1**

  • iPad Retina - Simulator - iOS 7.1

**Xcode 6**

  • iPad Retina (8.3 Simulator) [EA79555F-ADB4-4D75-930C-A745EAC8FA8B]

**Xcode 7**

  • iPhone 6 (9.0) [3EDC9C6E-3096-48BF-BCEC-7A5CAF8AA706]

  • iPhone 6 (9.0) + Apple Watch - 38mm (2.0) [EE3C200C-69BA-4816-A087-0457C5FCEDA0]

@return [Array<RunLoop::Device>] All the devices will be simulators.

# File lib/run_loop/instruments.rb, line 248
def simulators
  @instruments_simulators ||= lambda do
    fetch_devices[:out].chomp.split("\n").map do |line|
      stripped = line.strip
      if line_is_simulator?(stripped) &&
            !line_is_simulator_paired_with_watch?(stripped) &&
            !line_is_apple_tv?(stripped)

        version = stripped[VERSION_REGEX, 0]

        if line_is_xcode5_simulator?(stripped)
          name = line
          udid = line
        else
          name = stripped.split('(').first.strip
          udid = line[CORE_SIMULATOR_UDID_REGEX, 0]
        end

        RunLoop::Device.new(name, version, udid)
      else
        nil
      end
    end.compact
  end.call
end
spawn(automation_template, options, log_file) click to toggle source

Spawn a new instruments process in the context of ‘xcrun` and detach.

@param [String] automation_template The template instruments will use when

launching the application.

@param [Hash] options The launch options. @param [String] log_file The file to log to. @return [Integer] Returns the process id of the instruments process. @todo Do I need to enumerate the launch options in the docs? @todo Should this raise errors? @todo Is this jruby compatible?

# File lib/run_loop/instruments.rb, line 160
def spawn(automation_template, options, log_file)
  env = {
    "CLOBBER" => "1"
  }
  splat_args = spawn_arguments(automation_template, options)
  logger = options[:logger]
  RunLoop::Logging.log_debug(logger, "xcrun #{splat_args.join(' ')} >& #{log_file}")
  pid = Process.spawn(env, 'xcrun', *splat_args, {:out => log_file, :err => log_file})
  Process.detach(pid)
  pid.to_i
end
templates() click to toggle source

Returns an array of Instruments.app templates.

Depending on the Xcode version Instruments.app templates will either be:

  • A full path to the template. # Xcode 5 and Xcode > 5 betas

  • The name of a template. # Xcode >= 6 (non beta)

**Maintainers!** The rules above are important and explain why we can’t simply filter by ‘~= /tracetemplate/`.

Templates that users have saved will always be full paths - regardless of the Xcode version.

@return [Array<String>] Instruments.app templates.

# File lib/run_loop/instruments.rb, line 196
def templates
  @instruments_templates ||= lambda do
    args = ['xctrace', 'list', 'templates']

    hash = xcrun.run_command_in_context(args, log_cmd: true)
    hash[:out].chomp.split("\n").map do |elm|
      stripped = elm.strip.tr('"', '')
      if stripped == '' || stripped == 'Known Templates:'
        nil
      else
        stripped
      end
    end.compact
  end.call
end
to_s() click to toggle source

@!visibility private

# File lib/run_loop/instruments.rb, line 92
def to_s
  "#<Instruments #{version.to_s}>"
end
version() click to toggle source

Returns the instruments version. @return [RunLoop::Version] A version object.

# File lib/run_loop/instruments.rb, line 174
def version
  @instruments_version ||= lambda do
    version_string = pbuddy.plist_read('CFBundleShortVersionString',
                                       path_to_instruments_app_plist)
    RunLoop::Version.new(version_string)
  end.call
end
xcrun() click to toggle source
# File lib/run_loop/instruments.rb, line 87
def xcrun
  RunLoop::Xcrun.new
end

Private Instance Methods

execute_command(args) { |stdout, stderr, process_status| ... } click to toggle source

@!visibility private

Execute an instruments command. @param [Array] args An array of arguments

# File lib/run_loop/instruments.rb, line 380
def execute_command(args)
  Open3.popen3('xcrun', 'instruments', *args) do |_, stdout, stderr, process_status|
    yield stdout, stderr, process_status
  end
end
fetch_devices() click to toggle source

@!visibility private

# File lib/run_loop/instruments.rb, line 277
def fetch_devices
  @device_hash ||= lambda do
    args = ['xctrace', 'list', 'devices']
    xcrun.run_command_in_context(args, log_cmd: true)
  end.call
end
is_instruments_process?(ps_details) click to toggle source

@!visibility private Is the process described an instruments process?

@param [String] ps_details Details about a process as returned by ‘ps` @return [Boolean] True if the details describe an instruments process.

# File lib/run_loop/instruments.rb, line 348
def is_instruments_process?(ps_details)
  return false if ps_details.nil?
  ps_details[/\/usr\/bin\/instruments/, 0] != nil
end
line_has_a_version?(line) click to toggle source

@!visibility private

# File lib/run_loop/instruments.rb, line 404
def line_has_a_version?(line)
  line[VERSION_REGEX, 0]
end
line_is_apple_tv?(line) click to toggle source

@!visibility private

# File lib/run_loop/instruments.rb, line 414
def line_is_apple_tv?(line)
  line[/Apple TV/, 0]
end
line_is_core_simulator?(line) click to toggle source

@!visibility private

# File lib/run_loop/instruments.rb, line 397
def line_is_core_simulator?(line)
  return nil if !line_has_a_version?(line)

  line[CORE_SIMULATOR_UDID_REGEX, 0]
end
line_is_simulator?(line) click to toggle source

@!visibility private

# File lib/run_loop/instruments.rb, line 387
def line_is_simulator?(line)
  line_is_core_simulator?(line) || line_is_xcode5_simulator?(line)
end
line_is_simulator_paired_with_watch?(line) click to toggle source

@!visibility private

# File lib/run_loop/instruments.rb, line 409
def line_is_simulator_paired_with_watch?(line)
  line[CORE_SIMULATOR_UDID_REGEX, 0] && line[/Apple Watch/, 0]
end
line_is_xcode5_simulator?(line) click to toggle source

@!visibility private

# File lib/run_loop/instruments.rb, line 392
def line_is_xcode5_simulator?(line)
  !line[CORE_SIMULATOR_UDID_REGEX, 0] && line[/Simulator/, 0]
end
path_to_instruments_app_plist() click to toggle source

@!visibility private

# File lib/run_loop/instruments.rb, line 419
def path_to_instruments_app_plist
  @path_to_instruments_app_plist ||=
        File.expand_path(File.join(xcode.developer_dir,
                             '..',
                             'Applications',
                             'Instruments.app',
                             'Contents',
                             'Info.plist'))
end
pids_from_ps_output(ps_cmd=INSTRUMENTS_FIND_PIDS_CMD) click to toggle source

@!visibility private Extracts an Array of integer process ids from the output of executing the Unix ‘ps_cmd`.

@param [String] ps_cmd The Unix ‘ps` command used to find instruments

processes.

@return [Array<Integer>] An array of integer pids for instruments

processes.  Returns an empty list if no instruments process are found.
# File lib/run_loop/instruments.rb, line 361
def pids_from_ps_output(ps_cmd=INSTRUMENTS_FIND_PIDS_CMD)
  ps_output = ps_for_instruments(ps_cmd)
  lines = ps_output.lines("\n").map { |line| line.strip }
  lines.map do |line|
    tokens = line.strip.split(' ').map { |token| token.strip }
    pid = tokens.fetch(0, nil)
    process_description = tokens[1..-1].join(' ')
    if is_instruments_process? process_description
      pid.to_i
    else
      nil
    end
  end.compact.sort
end
ps_for_instruments(ps_cmd=INSTRUMENTS_FIND_PIDS_CMD) click to toggle source

@!visibility private

Executes ‘ps_cmd` to find instruments processes and returns the result.

@param [String] ps_cmd The Unix ps command to execute to find instruments

processes.

@return [String] A ps-style list of process details. The details returned

are controlled by the `ps_cmd`.
# File lib/run_loop/instruments.rb, line 339
def ps_for_instruments(ps_cmd=INSTRUMENTS_FIND_PIDS_CMD)
  `#{ps_cmd}`.strip
end
spawn_arguments(automation_template, options) click to toggle source

@!visibility private Parses the run-loop options hash into an array of arguments that can be passed to ‘Process.spawn` to launch instruments.

# File lib/run_loop/instruments.rb, line 303
def spawn_arguments(automation_template, options)
  array = ['instruments']
  array << '-w'

  array << options[:udid]

  trace = options[:results_dir_trace]
  if trace
    array << '-D'
    array << trace
  end

  array << '-t'
  array << automation_template

  array << options[:bundle_id]

  {
        'UIARESULTSPATH' => options[:results_dir],
        'UIASCRIPT' => options[:script]
  }.each do |key, value|
    array << '-e'
    array << key
    array << value
  end
  array + options.fetch(:args, [])
end