Code Monkey home page Code Monkey logo

perf_check's Introduction

What is perf check

perf_check is a quick-n-dirty way to benchmark git branches of your rails app.

Imagine a rails-aware apache ab.

We typically run it locally or on staging, to get a general idea of how our branches might have affected app performance. Often, certain pages render differently if logged in, or as an admin, so perf_check provides an easy way to deal with that.

How to install

Add it to your Gemfile, stick it just in the :development group

gem 'perf_check'

You will actually have to commit this to git. Preferably to master, this will make life easiest. However, as long as the gem exists on whatever reference branch you are benchmarking against, you are good to go.

How to use

In it's simplest incarnation, just feed it an url:

$ bundle exec perf_check /notes/browse
=============================================================================
PERRRRF CHERRRK! Grab a coffee and don't touch your working tree (we automate git)
=============================================================================


Benchmarking /notes/browse:
	Request  1:  774.8ms	  66MB
	Request  2:  773.4ms	  66MB
	Request  3:  771.1ms	  66MB
	Request  4:  774.1ms	  66MB
	Request  5:  773.7ms	  66MB
	Request  6:  774.8ms	  66MB
	Request  7:  773.4ms	  66MB
	Request  8:  771.1ms	  66MB
	Request  9:  774.1ms	  66MB
	Request  10: 773.7ms	  67MB

Benchmarking /notes/browse:
	Request  1:  20.2ms	  68MB
	Request  2:  23.0ms	  68MB
	Request  3:  19.9ms	  68MB
	Request  4:  19.5ms	  68MB
	Request  5:  19.4ms	  68MB
	Request  6:  20.2ms	  68MB
	Request  7:  23.0ms	  68MB
	Request  8:  19.9ms	  68MB
	Request  9:  19.5ms	  69MB
	Request  10: 19.4ms	  69MB

==== Results ====
/notes/browse
       master: 20.4ms
  your branch: 773.4ms
       change: +753.0ms (yours is 37.9x slower!!!)

How does it work

In the above example, perf_check assumes you are already on a feature branch. It then:

  • Launches its own rails server on a custom port (with fragment caching enabled)
  • Hits /user/45/posts 11 times (throws away the first 2 requests)
  • Git stashes in case you have uncommitted stuff. Checks out master. Restarts server
  • Hits /user/45/posts 11 times (throws away the first 2 requests)
  • Applies the stash if it existed
  • Prints results

Caveats of DOOOOM

We automate git

Do not edit the working tree while perf_check is running!

This program performs git checkouts and stashes, which are undone after the benchmark completes. If the working tree changes after the reference commit is checked out, numerous problems may arise.

Caching is forced on (by default)

Perf check start ups its rails server with cache_classes=true and perform_caching=true regardless of what's in your development.rb

You can pass --clear-cache which will run Rails.cache.clear before each batch of requests. This is useful when testing caching.

All options

$ bundle exec perf_check
Usage: perf_check [options] [route ...]
Benchmark options:
    -n, --requests N                 Use N requests in benchmark, defaults to 10
    -r, --reference COMMIT           Benchmark against COMMIT instead of master
    -q, --quick                      Fire off 5 requests just on this branch, no comparison with master
        --clear-cache                Call Rails.cache.clear before running benchmark
        --302-success                Consider HTTP 302 code a successful request
        --302-failure                Consider HTTP 302 code an unsuccessful request
        --run-migrations             Run migrations up and down with branch
        --compare-paths              Compare two paths against each other on the same branch

Usage examples:
  Benchmark PostController#index against master
     perf_check /user/45/posts
     perf_check /user/45/posts -n5

  Benchmark against a specific commit
     perf_check /user/45/posts -r 0123abcdefg
     perf_check /user/45/posts -r HEAD~2

  Benchmark the changes in the working tree
     perf_check /user/45/posts -r HEAD

  Benchmark and diff the output against master
     perf_check /user/45/posts --verify-no-diff

  Diff the output on your branch with master
     perf_check /user/45/posts --diff

  Diff a bunch of urls listed in a file (newline seperated)
     perf_check --diff --input FILE

Troubleshooting

Perf Check

Setting the PERF_CHECK env variable will start up your app with the middleware which does things like capture backtraces and count sql queries

PERF_CHECK=true bundle exec rails s

Redis

Certain versions of redis might need the following snippet to fork properly:

  # Circumvent redis forking issues with our version of redis
  # Will be fixed when we update redis https://github.com/redis/redis-rb/issues/364
  class Redis
    class Client
      def ensure_connected
        tries = 0
        begin
          if connected?
            if Process.pid != @pid
              reconnect
            end
          else
            connect
          end
          tries += 1
          yield
        rescue ConnectionError
          disconnect
          if tries < 2 && @reconnect
            retry
          else
            raise
          end
        rescue Exception
          disconnect
          raise
        end
      end
    end
  end

perf_check's People

Contributors

bobcats avatar cagmz avatar edanc avatar manfred avatar nathanbertram avatar siegfault avatar sudara avatar wioux avatar wjessop avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

perf_check's Issues

Output: status, progress, measurements, reports, errors, and logs

PerfCheck currently communicates at the user in two ways: a Logger instance and exceptions. This implementation makes sense in the context of a terminal application with a command-line interface but doesn't work well when communicating through a CI or desktop application.

There are a number of different types of information that PerfCheck needs to output:

  1. Status of the run (started, finished, failed, broken, etc)
  2. Progress of the run (git: Checking out `master', bundler: Bundling, http: Request /companies 243.3ms, etc)
  3. Measurements and results in structured format (request_path: '/companies', latency: 24.3, query_count: 12, body: 'Hi!', diff: "-Hello\n+Hi!")
  4. Formatted reports (Your branch is 20% less awesome than master)
  5. Errors (source of the error, message, and structured details)
  6. Logs (Rails server logs)

We don't want to keep doing this over the logger interface because it restricts the freedom we have in the UI when presenting this information.

Short term future

Because PerfCheck currently runs in the same process space as PerfCheck CI we can easily define some Interface in Ruby to pass this information back.

For example:

perf_check.publish(
  :measurement,
  request_path: '/sessions/new',
  latency: 24.3
)
perf_check.publish(
  :progress,
  "Checking out `#{branch}'
  branch: branch
)
perf_check.publish(
  :error,
  "Failed to check out `#{branch}'",
  branch: branch,
  output: stderr.read
)
perf_check.publish(
  :error,
  "Failed to check out `#{branch}'",
  branch: branch,
  output: stderr.read
)
perf_check.publish(
  :log, output,
  job_uuid: @uuid,
  name: 'Rails server log'
)

On the other end we can subscribe to these publish events and do something with it.

perf_check.on_published(:measurement, ->(**details) {
  perf_check.logger.info(
    PerfCheck::Measurement.table.row(
      *details.slice(:request_path, :latency)
    )
  )
})
perf_check.on_published(:progress, ->(message, **details) {
  perf_check.logger.info(message)
})

perf_check.on_published(:log, ->(output, **attributes) {
  Job.find_by!(uuid: job_uuid).logs.create!(attributes)
})

Long term future

At some point we might switch to a distributed architecture where PerfCheck runs in multiple instances and reports back to PerfCheck CI through HTTP or ZeroMQ or something.

perf_check.on_published(:measurement, ->(**details) {
  perf_check.zero_mq.send_string(JSON.dump(details))
})

Switch to experimental branch when running --deployment

In the previous situation (i.e. using perf_check_daemon) we would always start perf_check in the ‘current branch’. This meant we never had to change branches at the start of the run. So:

Basically there are three relevant branches when running Perf Check:

  1. The current branch
  2. The reference branch (zero measurement for comparison)
  3. The experimental branch
  • As a user I want to automatically switch to the experimental branch when it's not the current branch when running in deployment mode.
  • As a user I want to automatically switch back the current branch at the end of the experiment when running in deployment mode.

Auto-bundle on the ref/master branch

I think we should always bundle on the ref branch to save the user the pain of finding out they haven't done so since pulling.

My only question is if Gemfile.lock would get modified on the ref branch then? I'm thinking not in 99% of cases.

delay perf_check runs until the server is actually ready for service

putting '@PerfCheck' into a github PR causes a perf check to be run.

However, when the database that perf check uses is being backed up, perf-check reports spurious, random errors.

Perf check should either stall while it's database is being restored, and should not process github callbacks until it's actually ready for work or error in a more sane way.

Label rss size

Benchmarking /sudara/tracks:
    Request  1: 261.5ms  125MB  
    Request  2: 269.4ms  136MB  
    Request  3: 278.1ms  147MB  
    Request  4: 250.4ms  152MB  
    Request  5: 298.9ms  153MB  
    Request  6: 258.6ms  156MB  
    Request  7: 277.9ms  157MB  

support envar changes as part of testing

Some app changes appear only with envar changes, so it should be possible to set envars on the "control" test and set the same envars on the "test" runs.

Suggested: -m ENVAR=VALUE1 and -m ENVAR=VALUE2

Allow specs against target apps to run isolated

The spec suite will currently issue git commands against the perf_check working directory instead of against the test apps directly. Doing so can break the current git state when running specs on change. A solution is to pack an entire test app with a git directory and run them completely isolated.

Don't daemonize the Rails server

If we don't deamonize the Rails server we have far more direct control over the process.

Open3.popen2e(
  'rails',
  'server',
  '--environment', rails_environment,
  '--binding', rails_hostname,
  '--port', rails_port.to_s,
  chdir: app_root
) do |stdin, output, thread|
  stdin.close
  # Do things and then quit the server.
  Process.kill('TERM', thread.pid)
  # After the process stopped we can read the entire server output. This is
  # useful when debugging broken runs and possibly digging performance
  # information out of the request logs.
  logger.debug(output.read)
end

Stop sleeping

./lib/perf_check/server.rb-      if p
./lib/perf_check/server.rb-        Process.kill('QUIT', pid)
./lib/perf_check/server.rb:        sleep(1.5)
./lib/perf_check/server.rb-      end
./lib/perf_check/server.rb-    end
--
--
./lib/perf_check/server.rb-    def start
./lib/perf_check/server.rb-      system('rails server -b 127.0.0.1 -d -p 3031 >/dev/null')
./lib/perf_check/server.rb:      sleep(1.5)
./lib/perf_check/server.rb-

Perf check should exit early when important command fails

There is no really good reason to continue trying to perform requests when a git reset fails. We could technically differentiate between fatal errors (e.g. git issues) and acceptable failures (e.g. determining memory usage with ps).

Add option to direct logger to specific IO object

When running perf_check as a CLI it's pretty obvious that you will want to log to disk or one of the standard outputs. In a server environment like PerfCheckCI we want to redirect the PerfCheck logger to a very specific IO so we can separate PerfCheck for framework output easily without having to create files on disk.

  • As a developer I want to initialize PerfCheck with a logger object so I have freedom to redirect test output.

The before_start callback fires before running migrations

In a specific edge case in the PerfCheck CI test suite we built an app that has a blank database when we run PerfCheck.

The following happens:

  1. Fetch and hard reset to experiment branch
  2. Bundle install
  3. Trigger before start callbacks
  4. Run migrations
  5. (Re)start Rails server etc…

When the before start callbacks attempt to access something introduced by the migrations it will not be able to do so.

One solution would be to trigger the start callbacks after the migrations, but that might introduce other issues.

Potential solutions:

  1. Move the callbacks and hope for the best.
  2. More fine-grained callbacks (eg. before_all, before_setup, before_each_test_case, etc).
  3. Insane callbacks by using event handlers instead of callbacks*

Callbacks

We can have a system where every step in a run can have its own before, around, and after callbacks.

# Configure an around callback so you can decide not to yield for
# a certain step in the process to keep it from happening.
perf_check.steps.around(:start) do |step|
  if perf_check.options.experiment_branch == 'master'
    perf_check.logger.info(
      "Sorry, running benchmarks against master is " \
      "not allowed."
    )
  else
    step.call
  end
end
# Run something afterwards.
perf_check.steps.after(:migrations) do
  puts "Migrations done!"
end
perf_check.steps.around(:request) do |step|
  puts step.call.body
end
class Git
  def checkout(branch)
    perf_check.steps.call(:checkout) do
      git.checkout(branch)
    end
  end
end

Stash pop fails if Gemfile.lock is changed on dev branch

When rails s gets run on the master branch, Gemfile.lock is regenerated, and if it differs from the version on the dev branch then git stash pop will bail.

Git stash applying...
error: Your local changes to the following files would be overwritten by merge:
    Gemfile.lock
Please, commit your changes or stash them before you can merge.
Aborting

Gather profiles globally for the entire run

An object representing a run or perf_check could keep all the profiles. Then perf_check.profiles could return all the measurements. Statistics code and figure out how to interpret depending on the report (ie. compare branches, compare paths, compare responses).

[
  { branch: 'master', request_path: '/companies', latency: 0.23, query_count: 23, status: 200 },
  { branch: 'master', request_path: '/companies', latency: 0.27, query_count: 23, status: 200 },
  { branch: 'slower', request_path: '/companies', latency: 0.56, query_count: 37, status: 200 },
  { branch: 'slower', request_path: '/companies', latency: 0.66, query_count: 37, status: 200 }
]

Add authentication support

Most applications use some form of authentication so it's probably useful to add some sort of mechanism to PerfCheck to select a role and/or user for authentication.

I think we want to pass the role as a string so the target application can resolve it in a useful way. In the context of this issue I don't care how the target application does this.

perf_check --user-role admin /manage/users

For selecting a specific user we also want to accept a string.

perf_check --user [email protected] /dashboard
perf_check --user 5543453 /dashboard

We can also limit the API surface by forcing users to encode their user scheme into some sort of serialized form.

perf_check --user company:12,user:5 /dashboard
perf_check --user role=admin /admin/board

Internally we can call this options.user or option.authenticated_user for extreme clarity. Eventually it should be replaced by options.cookie or options.headers.

500 errors should be more obvious/verbose

Somehow...I'm thinking

  1. We should be like YO 500 ERROR HAPPENED!
  2. More explicitly instruct peeps to check out /perf_check_failed_request.html for trace
sudara@juno ⚡️  ~/Sites/alonetone (master) ✗ perf_check /sudara/tracks
Yo, profiling master vs. master isn't too useful, but hey, we'll do it
=============================================================================
PERRRRF CHERRRK! Grab a ☕️  and don't touch your working tree (we automate git)
=============================================================================
starting rails...

Benchmarking /sudara/tracks:
    Request  0: —— FAILURE (HTTP 500): /perf_check_failed_request.html

PerfCheck should spawn commands in clean environment

Using Process.spawn will create a process in the same process group as the parent (i.e. the Sidekiq process) and inherit most of its environment. A contaminated environment causes Bundler, Webpacker, and other tools to bleed over their settings into the new process.

A solution is to use Process.spawn options to decouple the new process as much as possible when creating a new process and re-use this as much as possible for rails server, bundle install, and rails db:migrate.

yours is 1.0x slower

==== Results ====
/sudara/tracks
       master: 263.5ms
  your branch: 264.4ms
       change: +0.9ms (yours is 1.0x slower!!!)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.