Strange Symphonies Don’t worry, be happy

28Nov/070

Taming the autotest Beast with FSEvents

autotest is a great tool and all, but it is simply resource intensive. Due to autotest's implementation, it eats up CPU resources, not because the tests are always running (only after you modify your file), but because autotest continually polls each file in your directory, and sub directories, and checks to see if it has been modified.

This continual polling isn't good for CPU resources. Now I may have some spare cycles to let autotest do its thang, but it also doesn't sound too healthy for my hard drive.

Note: This is Mac OS X 10.5 Leopard specific.

Along Comes a Leopard

Whilst reading the Arstechnica review on Leopard, I came upon the section on the File System Events (FSEvents) that was introduced in 10.4 actually (for Spotlight), but used once again for Time Machine. In Mac OS X 10.5 Leopard, the API was opened up for the public to consume.

File System Events (FSEvents)

In its simplest level, your application will notify FSEvents, that it wants to listen to a particular directory, and when that directory (or its sub directories) are modified, FSEvents basically triggers a callback in your application. This allows you to hook to, listen to file system changes, and react to accordingly.

This is exactly what I needed to calm the fury of the autotest beast.

Install RubyCocoa

Note: It looks like the code works on the stock Ruby and RubyCocoa out of the box actually. You don't need to install the latest version.

First of all, you need to install RubyCocoa, as this provides us with the bindings required to communicate with FSEvents. As I installed Ruby via MacPorts, I opted to do a source install (the MacPorts at present, is one version behind). I did run into trouble, encountering this error "file is not of required architecture".

If you ever needed a reason to actually use RubyCocoa, let this be your reason!

Taming the beast

Once installed, dump this little gem into your ~/.autotest file:

class Autotest
  def run
    hook :run
    reset

    run_tests

    require 'osx/foundation'
    OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework'

    callback = proc do |stream, ctx, numEvents, paths, marks, eventIDs|
      paths.regard_as('*')
      rpaths = []

      numEvents.times { |i| rpaths << paths[i] }

      run_tests_in_paths(*rpaths)
    end

    allocator = OSX::KCFAllocatorDefault
    context   = nil
    path      = [Dir.pwd]
    sinceWhen = OSX::KFSEventStreamEventIdSinceNow
    latency   = 1.0
    flags     = 0

    stream   = OSX::FSEventStreamCreate(allocator, callback, context, path, sinceWhen, latency, flags)
    unless stream
      puts "Failed to create stream"
      exit
    end

    OSX::FSEventStreamScheduleWithRunLoop(stream, OSX::CFRunLoopGetCurrent(), OSX::KCFRunLoopDefaultMode)
    unless OSX::FSEventStreamStart(stream)
      puts "Failed to start stream"
      exit
    end

    OSX::CFRunLoopRun()
  rescue Interrupt
    OSX::FSEventStreamStop(stream)
    OSX::FSEventStreamInvalidate(stream)
    OSX::FSEventStreamRelease(stream)
  end

  def find_files_in_paths(*paths)
    current_dir = Dir.pwd.length + 1
    result = {}
    paths.each do |path|
      # Largely copied from autotest
      Find.find path do |f|
        Find.prune if @exceptions and f =~ @exceptions and test ?d, f
        next if test ?d, f
        next if f =~ /(swp|~|rej|orig)$/
        next if f =~ /\/\.?#/

        filename = f[current_dir..-1]
        result[filename] = File.stat(filename).mtime rescue next
      end
    end
    return result
  end

  def run_tests_in_paths(*paths)
    find_files_to_test(find_files_in_paths(*paths))
    return if @files_to_test.empty?
    # Copied from autotest
    cmd = make_test_cmd @files_to_test

    hook :run_command
    puts cmd

    old_sync = $stdout.sync
    $stdout.sync = true
    @results = []
    line = []
    begin
      open("| #{cmd}", "r") do |f|
        until f.eof? do
          c = f.getc
          putc c
          line << c
          if c == ?\n then
            @results << line.pack("c*")
            line.clear
          end
        end
      end
    ensure
      $stdout.sync = old_sync
    end
    hook :ran_command
    @results = @results.join

    handle_results(@results)
  end
end

Now when you run autotest, you'll run into something like this:

/Users/aizat/.autotest:4: warning: method redefined; discarding old run

Don't worry about it, and feel free to ignore it.

With this code, you can also tame the beast. There you have it, a much saner autotest, only for Mac OS X 10.5 Leopard.

Related posts

Tags

Comments (0) Trackbacks (0)

No comments yet.


Leave a comment


No trackbacks yet.