Muitos desenvolvedores não sabem como fazer benchmark e profiling das suas aplicações, e uma dica, o desenvolvedor sempre se engana onde estão os bottlenecks. No meu trabalho atual trabalhei bastante com otimização e benchmark, utilizando desde o simples ab ao Dtrace no Solaris e agora no OSX.
Uma preocupação recorrente de todos é otimização da aplicação, mas isso só é de fato preciso se o desenvolvedor souber exatamente o que é lento! Depois disso se pode usar várias técnicas como cache.
Esse breve artigo visa explicar e exemplificar o uso de:
Esse NÃO é um artigo avançado com milhões de exemplos e não pretende ser um guia para melhorar sua aplicação, apenas quero mostrar brevemente que o desenvolvedor pode, deve e tem maneiras de verificar onde deve se preocupar.
OBS: Esse artigo ainda está sendo escrito, qualquer boa contribuição é sempre muito bem vinda.
Para começar criei uma simples applicação usando Rails 2.3.2 chamada tutor.
rails tutor -d mysql
Criei um controller para ser minha homepage, imprimindo apenas um texto.
class HomepageController < ApplicationController
def index
render :text => "Hello World"
end
end
E uma rota para essa action:
map.root :controller => "homepage"
Agora podemos começar a fazer alguns benchmarks.
Apache benchmark é uma ferramenta distribuída junto com o servidor Apache para benchmark. Podemos fazer testes de performance bastante satisfatórios com ele. Para utilizar o Apache Benchmark, que vem distribuído junto com o Apache Httpd, basta apenas utilizar a o shell.
Fazendo o benchmark da página inicial ab -n 10 -c 2 http://127.0.0.1:3000/
-n 10 indica que você fara 10 request com -c 2, isso é, concorrrência 2.
O apache usará threads para simular diferentes usuários acessando o site simultaneamente, no caso, 2 usuários.
Podemos ver aqui o output do ab:
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 127.0.0.1 (be patient).....done
Server Software:
Server Hostname: 127.0.0.1
Server Port: 3000
Document Path: /
Document Length: 11 bytes
Concurrency Level: 2
Time taken for tests: 0.033 seconds
Complete requests: 10
Failed requests: 0
Write errors: 0
Total transferred: 2540 bytes
HTML transferred: 110 bytes
Requests per second: 300.07 [#/sec] (mean)
Time per request: 6.665 [ms] (mean)
Time per request: 3.333 [ms] (mean, across all concurrent requests)
Transfer rate: 74.43 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 6 6 1.0 6 9
Waiting: 4 5 0.8 5 7
Total: 6 7 1.0 7 9
Percentage of the requests served within a certain time (ms)
50% 7
66% 7
75% 7
80% 8
90% 9
95% 9
98% 9
99% 9
100% 9 (longest request)
O dado mais importante no output do ab é o tempo gasto por cada request, que aqui é muito baixo, afinal não estamos fazendo nenhum tipo de cumputação:
Time per request: 6.665 [ms] (mean)
Obs: Eu estou rodando a aplicação em modo de produção usando uma instância apenas do Mongrel.
Outro dado importante é o “Percentage of the requests served within a certain time (ms)” que pode lhe indicar indícios de bottlenecks quando um request anda demorando muito. Se você tiver um resultado como:
Percentage of the requests served within a certain time (ms)
50% 48
66% 58
75% 67
80% 110
90% 149
95% 149
98% 149
99% 149
100% 149 (longest request)
Isso indica que o request mais longo foi de 149 ms enquanto o menor foi de 48 ms. Talvez tenha alguma coisa de errado na aplicação, algum recurso compartilhado como IO segurando requests.
Outro fator importante para benchmarking e otimização de código é medir a aplicação antes e depois de envolver algum tipo de otimização como cache para ver se realmente você está fazendo a coisa certa.
Sinceramente devo falar, se você quer que sua app escrita em Rails seja rápida, use muito cache, mas antes de usar, verifique sempre com benchmarks se ele realmente vale a pena e o quanto ele vale em números.
Outra dica importante é: Vá de cima pra baixo, não faça microbenchmarks antes de saber do todo e não faça otimização em pequenas partes antes de saber o quanto elas valem no todo.
O próprio rails tem ferramentas bastante úteis para fazer benchmark de partes de seu código. Aqui em baixo é um simples exemplo de uma biblioteca que tem que alguns métodos para gerar tokens.
require 'sha1'
require 'md5'
module FakeApi
def self.genarate_token
SHA1.sha1(Time.now.to_s).hexdigest
end
def self.genarate_token2
MD5.md5(Time.now.to_s).hexdigest
end
def self.genarate_token3
rand(36**8).to_s(36) + rand(36**8).to_s(36)
end
def self.genarate_token4
ActiveSupport::SecureRandom.hex(8)
end
end
Utilizando o Rails Benchmark podemos obter resultados como esse:
bastos@tiago-bastoss-macbook!tutor: script/performance/benchmarker 500 'FakeApi::genarate_token'
user system total real
#1 0.010000 0.000000 0.010000 ( 0.009395)
bastos@tiago-bastoss-macbook!tutor: script/performance/benchmarker 500 'FakeApi::genarate_token2'
user system total real
#1 0.000000 0.000000 0.000000 ( 0.008564)
bastos@tiago-bastoss-macbook!tutor: script/performance/benchmarker 500 'FakeApi::genarate_token3'
user system total real
#1 0.000000 0.000000 0.000000 ( 0.006788)
bastos@tiago-bastoss-macbook!tutor: script/performance/benchmarker 500 'FakeApi::genarate_token4'
user system total real
#1 0.000000 0.000000 0.000000 ( 0.005576)
Podendo assim avaliar que gerar tokens com o ActiveSupport é bem mais rápido no nosso caso.
O Ruby vem com uma biblioteca padrão para benchmark de código, abaixo vamos fazer alguns testes com ela utilizando a biblioteca FakeApi.
require "rubygems"
require 'benchmark'
require 'active_support'
require 'fake_api'
Benchmark.bm do |x|
x.report { FakeApi::genarate_token }
x.report { FakeApi::genarate_token2 }
x.report { FakeApi::genarate_token3 }
x.report { FakeApi::genarate_token4 }
end
O output seria algo como:
user system total real
0.000000 0.000000 0.000000 ( 0.000136)
0.000000 0.000000 0.000000 ( 0.000037)
0.000000 0.000000 0.000000 ( 0.000132)
0.040000 0.020000 0.060000 ( 0.075202)
Essas três colunas mostram o que foi usado pelo seu usuário, pelo sistema, o total e o real.
Sempre faça testes mais de uma vez, faça sempre mudar alguma parte significativa do código como sua biblioteca de cache ou mesmo a versão do Rails.
Lembre-se que benchmarks podem ser bastante diferentes se você rodar em máquinas diferentes. Se você usar uma arquitetura pra deployment diferente da sua de desenvolvimento, tente rodar benchmarks e profiling na sua máquida de produção também. Um exemplo eu tive quando comecei a fazer testes com uma Máquina SPARC com tecnologia Coolthreads,elas são extremamente lentas para Ruby. Sua arquitetura foi feita pra rodar centenas de threads nativas que usam muito IO e pouca CPU, então a empresa começou a usar x86 ao invés de SPARC.
Dtrace é uma ferramenta pre profiling, ela é extremamente útil quando você precisa saber realmente o que anda acontecendo com sua aplicação, quais são os métodos que estão sendo chamados, o número de chamadas etc. Ela está disponível para OSX e Solaris.
Para começar você precisa baixar os scrips
Depois vamos fazer uma mudança na app adicionando mais uso de CPU.
Controller:
class HomepageController < ApplicationController
def index
@token = FakeApi::genarate_token
@token2 = FakeApi::genarate_token2
@token3 = FakeApi::genarate_token3
@token4 = FakeApi::genarate_token4
end
end
View:
SHA1 <%= @token %>
MD5 <%= @token2 %>
CUSTOM1 <%= @token3 %>
ActiveSupport <%= @token4 %>
Agora rode o script rb_functime:
sudo dtrace -s rb_functime.d -p 22719
Troque 22719 pelo PID do processo rails que você está rodando (use ps -ef para achar).
O output é algo parecido com isso.
___ OVERLAP TIMES: ___ ______ ELAPSED ______
CLASS METHOD COUNT AVG(us) SUM(us)
...
ActionController::Base render_for_file 1 33373 33373
Module module_eval 155 241 37366
Object DelegateClass 1 44201 44201
Class method_added 358 148 53107
Class action_methods 1 55793 55793
ActionController::Base action_methods 1 55922 55922
Class new 67 1114 74651
Class [] 210 359 75425
ActionController::Params parse_formatted_paramet 1 94764 94764
Object find 1 117173 117173
Gem::GemPathSearcher find 1 117241 117241
Gem::GemPathSearcher matching_files 210 676 142062
Gem::GemPathSearcher matching_file? 210 808 169705
Kernel gem_original_require 22 7979 175552
Array each 44 6956 306074
Object require 42 7321 307496
Onde vemos quais são os métodos mais chamados e o tempo gasto com eles em média e somando o tempo total.
Vamos mudar (de uma forma estúpida) o código do controller para fazer com que o método generate_token apareça no resultado.
class HomepageController < ApplicationController
def index
1000.times do
@token = FakeApi::genarate_token
end
@token2 = FakeApi::genarate_token2
@token3 = FakeApi::genarate_token3
@token4 = FakeApi::genarate_token4
end
end
O output:
Class now 1014 75 76636
Class orig_new 1001 84 84302
ActionController::Params parse_formatted_paramet 1 96759 96759
Object find 1 115469 115469
Gem::GemPathSearcher find 1 115536 115536
Gem::GemPathSearcher matching_files 210 676 142018
Gem::GemPathSearcher matching_file? 210 807 169589
Object to_s 1002 171 172235
Kernel gem_original_require 22 8203 180485
Object hexdigest 1001 227 228174
Class sha1 1000 230 230607
Object require 42 7526 316101
Array each 49 6479 317483
Class new 1077 315 339898
Module genarate_token 1000 841 841971
Assim podemos ver que o genarate_token foi chamado 1000x! Usar o Dtrace é uma ótima maneira de ver que métodos são chamados costatemente, se eles são “pesados” então podemos assim aplicar alguma forma de otimizacão seja alterando o código ou usando cache.