rack-bug-speedtracer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,88 @@
1
+ Rack::SpeedTracer
2
+ =========
3
+
4
+ Blog post: [Speed Tracer Server-side Tracing with Rack](http://www.igvita.com/2010/07/19/speed-tracer-server-side-tracing-with-rack/)
5
+
6
+ Rack::SpeedTracer middleware provides server-side tracing capabilities to any Rack compatible app. Include the middleware, instrument your application, and then load it in your Google Chrome + SpeedTracer to view detailed breakdown of your JavaScript/CSS load times, GC cycles, as well as, server side performance data provided by this middleware - you can preview both server side and client side performance data all within the same view in SpeedTracer!
7
+
8
+ Preview of a sample, server side Rails trace (see below for setup) in SpeedTracer:
9
+ ![rails trace](http://img.skitch.com/20100717-cd31bhd5dh13sge7c2q1hefh4p.png)
10
+
11
+ Todo / Wishlist
12
+ ---------------
13
+
14
+ * Pluggable storage, ala rack-cache: capped mongo collection, or capped redis list would be ideal
15
+ * At the moment all of the data is stored directly in memory (unbounded)
16
+ * It would be great to have a mechanism to either (a) limit number of store traces or (b) expire them
17
+ * Authentication / optional enable, ala rack-bug: IP-based, password based
18
+ * At the moment, every request will record & store a trace
19
+ * Could also do conditional tracing based on a request header: 'X-SpeedTracer: true'
20
+ * Automagic Rails instrumentation for AR, layout, etc, ala rack-bug
21
+
22
+ Because the middleware has no authentication, or does not yet provide a capped memory footprint, it is not ready to be run in production - use it in development mode until these mechanisms are in place. Patches are welcome, of course!
23
+
24
+ How it works
25
+ ------------
26
+
27
+ Rack::SpeedTracer provides a Tracer class which you can use to instrument your code. From there, the trace details are stored as a JSON blob, and a special X-TraceUrl header is sent back to the client. If the user clicks on the network resource that corresponds to a request which returned a X-TraceUrl header, then SpeedTracer will make a request to our app to load the server side trace. Rack::SpeedTracer responds to this request and returns the full trace - aka, the data is provided on demand.
28
+
29
+ ### Quickstart Guide ###
30
+
31
+ gem install rack-speedtracer
32
+
33
+ # in your rack app / rackup file
34
+ use Rack::SpeedTracer
35
+
36
+ # in your app
37
+ env['st.tracer'].run('name of operation') do
38
+ ... your code ...
39
+ end
40
+
41
+ Check out a full sample rack app: examples/runner.rb
42
+
43
+ ### Manually instrumenting Rails ###
44
+ To produce a server-side trace equivalent to one in the screenshot above:
45
+
46
+ # in your Gemfile
47
+ gem 'rack-speedtracer', :require => 'rack/speedtracer'
48
+
49
+ # in development.rb environment
50
+ config.middleware.use Rack::SpeedTracer
51
+
52
+ # define a widgets controller
53
+ class WidgetsController < ApplicationController
54
+ def index
55
+ env['st.tracer'].run('Widgets#index') do
56
+ env['st.tracer'].run("ActiveRecord: Widgets.all") do
57
+ Widget.all
58
+ end
59
+
60
+ env['st.tracer'].run('Render') { render :text => 'oh hai' }
61
+ end
62
+ end
63
+ end
64
+
65
+ Speed Tracer
66
+ ------------
67
+
68
+ Speed Tracer is a Google Chrome extension to help you identify and fix performance problems in your web applications. It visualizes metrics that are taken from low level instrumentation points inside of the browser and analyzes them as your application runs. Speed Tracer is available as a Chrome extension and works on all platforms where extensions are currently supported (Windows and Linux).
69
+
70
+ Using Speed Tracer you are able to get a better picture of where time is being spent in your application. This includes problems caused by JavaScript parsing and execution, layout, CSS style recalculation and selector matching, DOM event handling, network resource loading, timer fires, XMLHttpRequest callbacks, painting, and more.
71
+
72
+ * [Official SpeedTracer site](http://code.google.com/webtoolkit/speedtracer/)
73
+ * [Install SpeedTracer](http://code.google.com/webtoolkit/speedtracer/get-started.html#downloading)
74
+ * [Getting Started](http://code.google.com/webtoolkit/speedtracer/speed-tracer-examples.html)
75
+ * [Google Group](https://groups.google.com/group/speedtracer/topics)
76
+
77
+ License
78
+ -------
79
+
80
+ (The MIT License)
81
+
82
+ Copyright © 2010 Ilya Grigorik
83
+
84
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
85
+
86
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
87
+
88
+ THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,243 @@
1
+ require 'rubygems'
2
+ require 'rubygems/installer'
3
+ require 'rake/gempackagetask'
4
+ require 'rake/gemcutter'
5
+ require 'hanna/rdoctask'
6
+ require 'spec/rake/spectask'
7
+ require 'mailfactory'
8
+ require 'net/smtp'
9
+
10
+ require File.dirname(__FILE__) +'/gemspec'
11
+ require 'spec/rake/verify_rcov'
12
+
13
+ PACKAGE_DIR = "pkg"
14
+ directory "doc"
15
+
16
+ #spec_files = FileList["spec/**/*.rb"]
17
+
18
+ RakeConfig = {
19
+ :email_servers => [ {
20
+ :server => "ruby-lang.org",
21
+ :helo => "gmail.com"
22
+ } ],
23
+ :announce_to_email => "ruby-talk@ruby-lang.org"
24
+ }
25
+
26
+ class SpecTask < Spec::Rake::SpecTask
27
+ def initialize(name=:spec)
28
+ super(name) do
29
+ @spec_files = FileList["spec/**/*"]
30
+ @libs = [%w{spec_help interpose}, %w{lib}, %w{spec_help}].map do|dir|
31
+ File::join(File::dirname(__FILE__), *dir)
32
+ end
33
+ @ruby_opts = %w{-rubygems}
34
+ self.spec_opts= %w{-f s --diff c --color}
35
+ @rcov_opts = %w{--exclude ^gemspec.rb,^spec/,^spec_help/ --sort coverage --threshold 101 -o doc/coverage --xrefs --no-color}
36
+ @rcov = true
37
+ @failure_message = "Spec examples failed."
38
+ @verbose = true
39
+ yield(self) if block_given?
40
+ end
41
+ end
42
+
43
+ def spec_opts=(opts)
44
+ @spec_opts = %w{-r cbt-tweaker -f e:last_run} + opts + %w{-r spec_helper -p spec/**/*.rb}
45
+ end
46
+ end
47
+
48
+ task :needs_root do
49
+ unless Process::uid == 0
50
+ fail "This task must be run as root"
51
+ end
52
+ end
53
+
54
+ desc "Run failing examples if any exist, otherwise, run the whole suite"
55
+ task :spec => "spec:quick"
56
+
57
+ namespace :spec do
58
+ desc "Always run every spec"
59
+ SpecTask.new(:all)
60
+
61
+ desc "Generate spec/spec.opts"
62
+ SpecTask.new(:spec_opts) do |t|
63
+ t.spec_opts= %w{-f s --diff c --color --generate-options spec/spec.opts}
64
+ t.rcov = false
65
+ end
66
+
67
+ desc "Generate specifications documentation"
68
+ SpecTask.new(:doc) do |t|
69
+ t.spec_opts = %w{-f s:doc/Specifications -b}
70
+ t.failure_message = "Failed generating specification docs"
71
+ t.verbose = false
72
+ end
73
+
74
+ desc "Run specs with Ruby profiling"
75
+ SpecTask.new(:profile) do |t|
76
+ t.ruby_opts += %w{-rprofile}
77
+ end
78
+
79
+ desc "Run only failing examples"
80
+ SpecTask.new(:quick) do |t|
81
+ t.spec_opts = %w{-f s --diff c --color --example last_run -b}
82
+ t.rcov = false
83
+ t.failure_message = "Spec examples failed."
84
+ end
85
+
86
+ desc "Run rspecs prior to a package publication"
87
+ SpecTask.new(:check) do |t|
88
+ t.spec_opts = %w{--format p:/dev/null}
89
+ t.failure_message = "Package does not conform to spec"
90
+ t.verbose = false
91
+ end
92
+
93
+ desc "Open firefox to view RCov output"
94
+ task :view_coverage => :doc do |t|
95
+ sh "/usr/bin/chromium coverage/index.html"
96
+ end
97
+ end
98
+
99
+ namespace :package do
100
+ RCov::VerifyTask.new do |t|
101
+ t.require_exact_threshold = false
102
+ t.threshold = 80
103
+ t.verbose = true
104
+ end
105
+ task :verify_rcov => ['spec:doc']
106
+
107
+ task :chown_coverage do
108
+ unless (user = ENV['SUDO_USER']).nil?
109
+ FileUtils::chown_R(user, ENV['SUDO_GID'].to_i, 'coverage')
110
+ end
111
+ end
112
+
113
+ package = Rake::GemPackageTask.new(SPEC) {|t|
114
+ t.need_tar_gz = true
115
+ t.need_tar_bz2 = true
116
+ }
117
+ task(:package).prerequisites.each do |package_type|
118
+ file package_type => "spec:check"
119
+ end
120
+
121
+ Rake::RDocTask.new(:docs) do |rd|
122
+ rd.options += SPEC.rdoc_options
123
+ rd.rdoc_dir = 'rubydoc'
124
+ rd.rdoc_files.include("lib/**/*.rb")
125
+ rd.rdoc_files += (SPEC.extra_rdoc_files)
126
+ end
127
+ task :docs => ['spec:doc']
128
+ end
129
+
130
+ desc "Publish the gem and its documentation to Rubyforge and Gemcutter"
131
+ task :publish => ['publish:docs', 'publish:rubyforge', 'publish:gem:push']
132
+ namespace :publish do
133
+ desc "Deploy gem via scp"
134
+ task :scp, :dest, :destpath, :needs => :package do |t, args|
135
+ destpath = args[:destpath] || "~"
136
+ sh "scp", File::join(package.package_dir, package.gem_file), "#{args[:dest]}:#{destpath}"
137
+ sh *(%w{ssh} + [args[:dest]] + %w{sudo gem install} + [File::join(destpath, package.gem_file)])
138
+ end
139
+
140
+ task :sign_off => %w{package:verify_rcov package:gem package:chown_coverage}
141
+
142
+ Rake::Gemcutter::Tasks.new(SPEC)
143
+ task 'gem:push' => :sign_off
144
+ task 'gem:install' => [:needs_root, :sign_off]
145
+ task 'gem:reinstall' => :needs_root
146
+
147
+ desc 'Publish RDoc to RubyForge'
148
+ task :docs => 'package:docs' do
149
+ config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))
150
+ host = "#{config["username"]}@rubyforge.org"
151
+ remote_dir = "/var/www/gforge-projects/#{RUBYFORGE[:group_id]}"
152
+ local_dir = 'rubydoc'
153
+ sh %{rsync -av --delete #{local_dir}/ #{host}:#{remote_dir}}
154
+ end
155
+
156
+ task :scrape_rubyforge do
157
+ require 'rubyforge'
158
+ forge = RubyForge.new
159
+ forge.configure
160
+ forge.scrape_project(RUBYFORGE[:package_id])
161
+ end
162
+
163
+ desc "Publishes to RubyForge"
164
+ task :rubyforge => ['package:verify_rcov', 'package:package', :docs, :scrape_rubyforge] do |t|
165
+ require 'rubyforge'
166
+ forge = RubyForge.new
167
+ forge.configure
168
+ files = [".gem", ".tar.gz", ".tar.bz2"].map do |extension|
169
+ File::join(PACKAGE_DIR, SPEC.full_name) + extension
170
+ end
171
+ release = forge.lookup("release", RUBYFORGE[:package_id])[RUBYFORGE[:release_name]] rescue nil
172
+ if release.nil?
173
+ forge.add_release(RUBYFORGE[:group_id], RUBYFORGE[:package_id], RUBYFORGE[:release_name], *files)
174
+ else
175
+ files.each do |file|
176
+ forge.add_file(RUBYFORGE[:group_id], RUBYFORGE[:package_id], RUBYFORGE[:release_name], file)
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ def announcement # :nodoc:
183
+ changes = ""
184
+ begin
185
+ File::open("./Changelog", "r") do |changelog|
186
+ changes = "Changes:\n\n"
187
+ changes += changelog.read
188
+ end
189
+ rescue Exception
190
+ end
191
+
192
+ urls = "Project: #{RUBYFORGE[:project_page]}\n" +
193
+ "Homepage: #{RUBYFORGE[:home_page]}"
194
+
195
+ subject = "#{SPEC.name} #{SPEC.version} Released"
196
+ title = "#{SPEC.name} version #{SPEC.version} has been released!"
197
+ body = "#{SPEC.description}\n\n#{changes}\n\n#{urls}"
198
+
199
+ return subject, title, body
200
+ end
201
+
202
+ desc 'Announce release on RubyForge and email'
203
+ task :press => ['press:rubyforge', 'press:email']
204
+ namespace :press do
205
+ desc 'Post announcement to rubyforge.'
206
+ task :rubyforge do
207
+ require 'rubyforge'
208
+ subject, title, body = announcement
209
+
210
+ forge = RubyForge.new
211
+ forge.configure
212
+ forge.post_news(RUBYFORGE[:group_id], subject, "#{title}\n\n#{body}")
213
+ puts "Posted to rubyforge"
214
+ end
215
+
216
+ desc 'Generate email announcement file.'
217
+ task :email do
218
+ require 'rubyforge'
219
+ subject, title, body= announcement
220
+
221
+ mail = MailFactory.new
222
+ mail.To = RakeConfig[:announce_to_email]
223
+ mail.From = SPEC.email
224
+ mail.Subject = "[ANN] " + subject
225
+ mail.text = [title, body].join("\n\n")
226
+
227
+ File.open("email.txt", "w") do |mailfile|
228
+ mailfile.write mail.to_s
229
+ end
230
+ puts "Created email.txt"
231
+
232
+ RakeConfig[:email_servers].each do |server_config|
233
+ begin
234
+ Net::SMTP.start(server_config[:server], 25, server_config[:helo], server_config[:username], server_config[:password]) do |smtp|
235
+ smtp.send_message(mail.to_s, mail.From, mail.To)
236
+ end
237
+ break
238
+ rescue Object => ex
239
+ puts ex.message
240
+ end
241
+ end
242
+ end
243
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
Binary file
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'rack'
3
+
4
+ $LOAD_PATH.unshift 'lib'
5
+ require 'rack/speedtracer'
6
+
7
+ class SomeApp
8
+ def call(env)
9
+ env['st.tracer'].run('computation: 5**100000') do
10
+ env['st.tracer'].run('computation 2: 5**10000') do
11
+ 5**10000
12
+ end
13
+
14
+ env['st.tracer'].run('sleep(0.01)') { sleep(0.01) }
15
+
16
+ 5**100000
17
+ end
18
+
19
+ env['st.tracer'].run('sleep(0.5)') do
20
+ sleep(0.5)
21
+ end
22
+
23
+ [200, {"Content-Type" => "text/plain"}, "Hello World"]
24
+ end
25
+ end
26
+
27
+ builder = Rack::Builder.new do
28
+ use Rack::CommonLogger
29
+ use Rack::SpeedTracer
30
+
31
+ run SomeApp.new
32
+ end
33
+
34
+ Rack::Handler::Thin.run builder.to_app, :Port => 4567
@@ -0,0 +1 @@
1
+ require 'rack/bug/speedtracer'
@@ -0,0 +1,56 @@
1
+ require 'rack/bug'
2
+ require 'yajl'
3
+ require 'uuid'
4
+
5
+ require 'rack/bug/speedtracer/trace-app'
6
+ require 'rack/bug/speedtracer/tracer'
7
+
8
+ module Rack::Bug
9
+ class SpeedTracer < Panel
10
+ include Rack::Bug::SpeedTrace::Render
11
+
12
+ def self.database
13
+ @db ||= {}
14
+ end
15
+
16
+ def database
17
+ self.class.database
18
+ end
19
+
20
+ def initialize(app)
21
+ @app = app
22
+ @uuid = UUID.new
23
+ super
24
+ end
25
+
26
+ def panel_app
27
+ return SpeedTrace::TraceApp.new(database)
28
+ end
29
+
30
+ def name
31
+ "speedtracer"
32
+ end
33
+
34
+ def heading
35
+ "#{database.keys.length} traces"
36
+ end
37
+
38
+ def content
39
+ render_template "traces", :traces => database
40
+ end
41
+
42
+ def before(env)
43
+ env['st.id'] = @uuid.generate
44
+
45
+ tracer = SpeedTrace::Tracer.new(env['st.id'], env['REQUEST_METHOD'], env['REQUEST_URI'])
46
+ env['st.tracer'] = tracer
47
+ Thread::current['st.tracer'] = tracer
48
+ end
49
+
50
+ def after(env, status, headers, body)
51
+ env['st.tracer'].finish
52
+ database[env['st.id']] = env['st.tracer']
53
+ headers['X-TraceUrl'] = '/speedtracer?id=' + env['st.id']
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,13 @@
1
+ require "rack/bug/speedtracer/duck-puncher"
2
+
3
+ find_constant("ActionView::Template"){|avt| avt.trace_methods :render_template}
4
+
5
+ find_constant("ActiveRecord::Base") do |ar_b|
6
+ ar_b.trace_class_methods :find, :all, :first, :last, :count, :delete_all
7
+ ar_b.trace_methods :save, :save!, :destroy, :delete
8
+ end
9
+
10
+ find_constant("ActionController::Base") do |ac_b|
11
+ ac_b.trace_methods :process, :render
12
+ end
13
+
@@ -0,0 +1,82 @@
1
+ require 'rack/bug/speedtracer'
2
+
3
+ module Kernel
4
+ def find_constant(name)
5
+ parts = name.split("::")
6
+ begin
7
+ const = parts.inject(Kernel) do |namespace, part|
8
+ namespace.const_get(part)
9
+ end
10
+ rescue NameError => ex
11
+ warn "Couldn't find #{name}"
12
+ return nil
13
+ end
14
+ yield(const) if block_given?
15
+ return const
16
+ end
17
+
18
+ # trace_run(method_name, context, caller[1], *args) do
19
+ def trace_run(context = "::", called_at=caller[1], args=[])
20
+ tracer = Thread.current['st.tracer']
21
+ result = nil
22
+ if tracer.nil?
23
+ Rails.logger.debug{"Null trace"}
24
+ result = yield
25
+ else
26
+ tracer.run(context, called_at, args){ result = yield }
27
+ end
28
+ result
29
+ end
30
+ end
31
+
32
+ class Rack::Bug::SpeedTracer
33
+ class << self
34
+ def safe_method_names(mod, method_names)
35
+ unsafe_names = (mod.public_instance_methods(true) +
36
+ mod.protected_instance_methods(true) +
37
+ mod.private_instance_methods(true)).sort.uniq
38
+
39
+ method_names.map do |name|
40
+ name = name.to_s
41
+ prefix = "0"
42
+ hidden_name = ["_", prefix, name].join("_")
43
+ while unsafe_names.include?(hidden_name)
44
+ prefix = prefix.next
45
+ hidden_name = ["_", prefix, name].join("_")
46
+ end
47
+
48
+ unsafe_names << hidden_name
49
+ [name, hidden_name]
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ class Module
56
+ def trace_class_methods(*methods_names)
57
+ (class << self; self; end).build_tracing_wrappers('#{self.name}::', *methods_names)
58
+ end
59
+
60
+ def trace_methods(*methods)
61
+ build_tracing_wrappers('#{self.class.name}#', *methods)
62
+ end
63
+
64
+ def build_tracing_wrappers(context, *methods)
65
+ @traced ||= {}
66
+
67
+ Rack::Bug::SpeedTracer::safe_method_names(self, methods).each do |method_name, old_method|
68
+ next if @traced.has_key?(method_name)
69
+ @traced[method_name] = true
70
+
71
+ alias_method old_method, method_name
72
+
73
+ self.class_eval <<-EOC, __FILE__, __LINE__
74
+ def #{method_name}(*args, &block)
75
+ trace_run("#{context}", caller(0)[0], args) do
76
+ #{old_method}(*args, &block)
77
+ end
78
+ end
79
+ EOC
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,29 @@
1
+ require 'rack/bug/speedtracer'
2
+
3
+ module Rack::Bug
4
+ #Variant of the Speed Tracer Panel that performs a nearly complete profile of
5
+ #all code called during a request. Note that this will slow overall response
6
+ #time by several orders of magnitude, and may return more data than
7
+ #SpeedTracer is prepared to display
8
+ class ProfilingSpeedTracer < SpeedTracer
9
+ def before(env)
10
+ super
11
+ tracer = env['st.tracer']
12
+ Kernel::set_trace_func proc {|event, file, line, name, binding,classname|
13
+ case event
14
+ when "c-call", "call"
15
+ methodname = classname ? "" : classname
16
+ methodname += name.to_s
17
+ tracer.start_event(file, line, name, classname || "", "")
18
+ when "c-return", "return"
19
+ tracer.finish_event
20
+ end
21
+ }
22
+ end
23
+
24
+ def after(env, status, headers, body)
25
+ Kernel::set_trace_func(nil)
26
+ super
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ module Rack::Bug::SpeedTrace
2
+ module Render
3
+ include ::Rack::Bug::Render
4
+
5
+ def compiled_source(filename)
6
+ ::ERB.new(::File.read(::File.dirname(__FILE__) + "/../views/#{filename}.html.erb"), nil, "-").src
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,51 @@
1
+ module Rack::Bug
2
+ module SpeedTrace
3
+ class TraceApp
4
+ TRACER_PATH = /^\/speedtracer/.freeze
5
+ CONTENT_TYPE = 'application/json;charset=UTF-8'.freeze
6
+
7
+ FourOhFour = [404, {"Content-Type" => "text/html"}, "App tracker doesn't know that path or id"].freeze
8
+
9
+ def initialize(db)
10
+ @db = db
11
+ end
12
+
13
+ def call(env)
14
+ return FourOhFour unless env['PATH_INFO'].match(TRACER_PATH)
15
+
16
+ resp = Rack::Response.new('', 200)
17
+ resp['Content-Type'] = CONTENT_TYPE
18
+
19
+ case env['REQUEST_METHOD']
20
+ when 'HEAD' then
21
+ # SpeedTracer dispatches HEAD requests to verify the
22
+ # tracer endpoint when it detects the X-TraceUrl
23
+ # header for the first time. After the initial load
24
+ # the verification is cached by the extension.
25
+ #
26
+ # By default, we'll return 200.
27
+
28
+ when 'GET' then
29
+ # GET requests for specific trace are generated by
30
+ # the extension when the user expands the network
31
+ # resource tab. Hence, server-side tracer data is
32
+ # request on-demand, and we need to store it for
33
+ # some time.
34
+
35
+ qs = Rack::Utils.parse_query(env['QUERY_STRING'])
36
+ if qs['id'] && @db[qs['id']]
37
+ resp.write @db[qs['id']].to_json
38
+ else
39
+ # Invalid request or missing request trace id
40
+ return FourOhFour
41
+ end
42
+ else
43
+ # SpeedTracer should only issue GET & HEAD requests
44
+ resp.status = 400
45
+ end
46
+
47
+ return resp.finish
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,212 @@
1
+ require 'rack/bug/speedtracer/render'
2
+
3
+ module Rack::Bug
4
+ module SpeedTrace
5
+ class TraceRecord
6
+ include Render
7
+ def initialize(id)
8
+ @id = id
9
+ @start = Time.now
10
+ @children = []
11
+ end
12
+
13
+ def finish; @finish = Time.now; end
14
+
15
+ def time_in_children
16
+ @children.inject(0) do |time, child|
17
+ time + child.duration
18
+ end
19
+ end
20
+
21
+ def duration
22
+ ((@finish - @start) * 1000).to_i
23
+ end
24
+
25
+ def to_json
26
+ Yajl::Encoder.encode(hash_representation, :pretty => true, :indent => ' ')
27
+ end
28
+
29
+ private
30
+ # all timestamps in SpeedTracer are in milliseconds
31
+ def range(start, finish)
32
+ {
33
+ 'duration' => ((finish - start) * 1000).to_i,
34
+ 'start' => [start.to_i, start.usec/1000].join(''),
35
+ #'end' => [finish.to_i, finish.usec/1000].join('')
36
+ }
37
+ end
38
+
39
+ def symbolize_hash(hash)
40
+ hash.each_key do |key|
41
+ if String === key
42
+ next if hash.has_key?(key.to_sym)
43
+ hash[key.to_sym] = hash[key]
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ class ServerEvent < TraceRecord
50
+ attr_accessor :children
51
+ attr_reader :name
52
+
53
+ def initialize(id, file, line, method, context, arguments)
54
+ super(id)
55
+
56
+ @file = file
57
+ @line = line
58
+ @method = method
59
+ @context = context
60
+ @arguments = arguments
61
+ @name = [context, method, "(", arguments, ")"].join("")
62
+ end
63
+
64
+ def hash_representation
65
+ {
66
+ 'range' => range(@start, @finish),
67
+ #'id' => @id,
68
+ 'operation' => {
69
+ # 'sourceCodeLocation' => {
70
+ # 'className' => @file,
71
+ # 'methodName' => @method,
72
+ # 'lineNumber' => @line
73
+ # },
74
+ 'type' => 'METHOD',
75
+ 'label' => @name
76
+ },
77
+ 'children' => @children
78
+ }
79
+ end
80
+
81
+ def to_html
82
+ render_template('serverevent',
83
+ {:self_time => duration - time_in_children}.merge(symbolize_hash(hash_representation)))
84
+ end
85
+ end
86
+
87
+ class Tracer < TraceRecord
88
+ def initialize(id, method, uri)
89
+ super(id)
90
+
91
+ @method = method
92
+ @uri = uri
93
+ @event_id = 0
94
+ @pstack = []
95
+ end
96
+
97
+ #TODO: Threadsafe
98
+ def run(context="::", called_at = caller[0], args=[], &blk)
99
+ file, line, method = called_at.split(':')
100
+
101
+ method = method.gsub(/^in|[^\w]+/, '') if method
102
+
103
+ start_event(file, line, method, context, args)
104
+ blk.call # execute the provided code block
105
+ finish_event
106
+ end
107
+
108
+ def short_string(item, max_per_elem = 50)
109
+ begin
110
+ string = item.inspect
111
+ if string.length > max_per_elem
112
+ case item
113
+ when NilClass
114
+ "nil"
115
+ when Hash
116
+ "{ " + item.map do |key, value|
117
+ short_string(key, 15) + "=>" + short_string(value, 30)
118
+ end.join(", ") + " }"
119
+ when find_constant("ActionView::Base")
120
+ tmpl = item.template
121
+ if tmpl.nil?
122
+ item.path.inspect
123
+ else
124
+ [tmpl.base_path, tmpl.name].join("/")
125
+ end
126
+ when find_constant("ActiveRecord::Base")
127
+ string = "#{item.class.name}(#{item.id})"
128
+ else
129
+ string = item.class.name
130
+ end
131
+ else
132
+ string
133
+ end
134
+ rescue Exception => ex
135
+ "..."
136
+ end
137
+ end
138
+
139
+ def make_string_of(array)
140
+ array.map do |item|
141
+ short_string(item)
142
+ end.join(",")
143
+ end
144
+
145
+ def start_event(file, line, method, context, arguments)
146
+ @event_id += 1
147
+
148
+ arguments_string = make_string_of(arguments)
149
+ event = ServerEvent.new(@event_id, file, line, method, context, arguments_string)
150
+ @pstack.push event
151
+ end
152
+
153
+ def finish_event
154
+ event = @pstack.pop
155
+ if event.nil?
156
+ else
157
+ event.finish
158
+
159
+
160
+ unless (parent = @pstack.last).nil?
161
+ parent.children.push event
162
+ else
163
+ @children.push event
164
+ end
165
+ end
166
+ end
167
+
168
+ def hash_representation
169
+ finish
170
+ { 'trace' => {
171
+
172
+ 'url' => "/speedtracer?id=#@id",
173
+
174
+ 'frameStack' => {
175
+
176
+ 'range' => range(@start, @finish),
177
+ 'operation' => {
178
+ 'type' => 'HTTP',
179
+ 'label' => [@method, @uri].join(' ')
180
+ },
181
+ 'children' => @children
182
+
183
+ }, #end frameStack
184
+
185
+ 'resources' => {
186
+ 'Application' => '/', #Should get the Rails app name...
187
+ 'Application.endpoint' => '/' #Should get the env path thing
188
+ }, #From what I can tell, Speed Tracer treats this whole hash as optional
189
+
190
+ 'range' => range(@start, @finish)
191
+ }
192
+ }
193
+ end
194
+
195
+ def to_html
196
+ hash = hash_representation
197
+ extra = {:self_time => duration - time_in_children}
198
+ "<a href='#{hash['url']}'>Raw JSON</a>\n" +
199
+ render_template('serverevent', extra.merge(symbolize_hash(hash['trace']['frameStack'])))
200
+ end
201
+
202
+ def finish
203
+ super()
204
+
205
+ until @pstack.empty?
206
+ finish_event
207
+ end
208
+ self
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,10 @@
1
+ <div class="traceblock">
2
+ <%=operation['label']%> - <%=range['duration']%>ms (self: <%=self_time%>ms)
3
+ <% unless children.empty? %>
4
+ <ul>
5
+ <% children.each do |child|%>
6
+ <li><%=child.to_html%></li>
7
+ <%end%>
8
+ </ul>
9
+ <% end %>
10
+ </div>
@@ -0,0 +1,8 @@
1
+ <h3>Traces</h3>
2
+
3
+ <dl>
4
+ <% traces.each_pair do |uuid, trace| %>
5
+ <dt><%= uuid %></dt>
6
+ <dd><%= trace.to_html %></dd>
7
+ <% end %>
8
+ </dl>
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+ require "rack/bug/speedtracer/duck-puncher"
3
+
4
+ describe "Duck punching" do
5
+ describe "::find_constant" do
6
+ it "should find constants" do
7
+ find_constant("Rack::Bug").should == Rack::Bug
8
+ end
9
+
10
+ it "should yield constants" do
11
+ find_constant("Rack::Bug") do |rb|
12
+ rb.should == Rack::Bug
13
+ end
14
+ end
15
+
16
+ it "should swallow exceptions when constants aren't found" do
17
+ expect do
18
+ find_constant("I::Made::This::Up")
19
+ end.to_not raise_error
20
+ end
21
+ end
22
+ describe "with tracing" do
23
+
24
+ before :each do
25
+ @tracer = Rack::Bug::SpeedTrace::Tracer.new(0, 'GET', '/test')
26
+ Thread.current["st.tracer"] = @tracer
27
+
28
+ @test_class = Class.new do
29
+ class << self
30
+ def klass_method(a,b,c)
31
+ b
32
+ end
33
+ end
34
+
35
+ def instance_method(a,b,c)
36
+ a << b
37
+ a << c
38
+ end
39
+
40
+ def other_instance_method
41
+ end
42
+ end
43
+ end
44
+
45
+ it "should trace instance methods" do
46
+ @test_class.trace_methods :instance_method
47
+
48
+ test_instance = @test_class.new
49
+
50
+ array = []
51
+ test_instance.instance_method(array, 1, 2)
52
+ array.should include(1,2)
53
+
54
+ @tracer.finish.to_json.should =~ /"operation":\s*{\s*"label":\s*"#instance_method\(\[\],1,2\)"/m
55
+ end
56
+
57
+ it "should trace class methods" do
58
+ @test_class.trace_class_methods :klass_method
59
+
60
+ @test_class.klass_method([5],6,7).should == 6
61
+ @tracer.finish.to_json.should =~ /"label":\s*"::klass_method\(\[5\],6,7\)"/
62
+
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+ require 'rack/bug/speedtracer/profiling'
3
+
4
+ describe Rack::Bug::ProfilingSpeedTracer do
5
+ it 'should set the X-TraceUrl header after rendering the response' do
6
+ respond_with(200)
7
+ response = get('/')
8
+
9
+ response.headers.should include 'X-TraceUrl'
10
+ response.headers['X-TraceUrl'].should match(/^\/speedtracer\?id=/)
11
+ end
12
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rack::Bug::SpeedTracer do
4
+ let(:app) { [200, {'Content-Type' => 'text/plain'}, 'Hello World'] }
5
+
6
+ describe 'middleware' do
7
+ it 'take a backend and returns a middleware component' do
8
+ Rack::Bug::SpeedTracer.new(app).should respond_to(:call)
9
+ end
10
+
11
+ it 'take an options Hash' do
12
+ lambda { Rack::Cache.new(app, {}) }.should_not raise_error(ArgumentError)
13
+ end
14
+ end
15
+
16
+ describe 'response' do
17
+ it 'should set the X-TraceUrl header after rendering the response' do
18
+ respond_with(200)
19
+ response = get('/')
20
+
21
+ response.headers.should include 'X-TraceUrl'
22
+ response.headers['X-TraceUrl'].should match(/^\/speedtracer\?id=/)
23
+ end
24
+
25
+ it 'should respond with 200 to HEAD requests to the speedtracer endpoint' do
26
+ respond_with(200)
27
+ response = head('/speedtracer?id=test')
28
+
29
+ response.status.should == 200
30
+ response.headers['Content-Length'].to_i.should == 0
31
+ end
32
+
33
+ it 'should return a stored trace in JSON format' do
34
+ sample_trace = Yajl::Encoder.encode({'trace' => {}})
35
+ tracer = mock(Rack::Bug::SpeedTrace::Tracer)
36
+ tracer.stub!(:to_json => sample_trace)
37
+
38
+ respond_with(200)
39
+ response = get('/speedtracer?id=test') do |st|
40
+ st.database['test'] = tracer
41
+ end
42
+
43
+ response.body.should == sample_trace
44
+ end
45
+
46
+ it 'should return 404 on missing trace' do
47
+ respond_with(404)
48
+ response = get('/speedtracer?id=test-missing')
49
+ response.status.should == 404
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rack::Bug::SpeedTrace::Tracer do
4
+
5
+ it 'should accept unique id, method, uri on initialize' do
6
+ lambda { Rack::Bug::SpeedTrace::Tracer.new(1, 'GET', '/') }.should_not raise_error
7
+ end
8
+
9
+ describe 'response' do
10
+ let(:tracer) { Rack::Bug::SpeedTrace::Tracer.new(1, 'GET', '/test') }
11
+
12
+ it 'should produce decent HTML' do
13
+ doc = tracer.finish.to_html
14
+ doc.should =~ /<div class="traceblock">\s*GET \/test/
15
+ end
16
+
17
+ it 'should serialize to json on finish' do
18
+ lambda { Yajl::Parser.parse(tracer.finish.to_json) }.should_not raise_error
19
+ end
20
+
21
+ it 'should conform to base speedtracer JSON schema' do
22
+ trace = Yajl::Parser.parse(tracer.finish.to_json)['trace']
23
+
24
+ # Example base trace:
25
+ # {"date"=>1279403357,
26
+ # "application"=>"Rack SpeedTracer",
27
+ # "id"=>1,
28
+ # "range"=>{"duration"=>0, "end"=>"1279403357651", "start"=>"1279403357651"},
29
+ # "frameStack"=>
30
+ # {"id"=>"0",
31
+ # "range"=>{"duration"=>0, "end"=>"1279403357651", "start"=>"1279403357651"},
32
+ # "operation"=>{"label"=>"GET /test", "type"=>"HTTP"},
33
+ # "children"=>[]}}
34
+
35
+ trace.should include 'range'
36
+ trace['range'].should be_an_instance_of Hash
37
+
38
+ trace.should include 'frameStack'
39
+ trace['frameStack'].should be_an_instance_of Hash
40
+
41
+ # root node description
42
+ root = trace['frameStack']
43
+
44
+ root.should include 'range'
45
+ root['range'].should be_an_instance_of Hash
46
+
47
+ root.should include 'operation'
48
+ root['operation'].should be_an_instance_of Hash
49
+ root['operation']['label'].should match('GET /test')
50
+ root['operation']['type'].should match('HTTP')
51
+
52
+ root.should include 'children'
53
+ root['children'].should be_an_instance_of Array
54
+ end
55
+ end
56
+
57
+ describe 'code tracing' do
58
+ before :each do
59
+ @tracer = Rack::Bug::SpeedTrace::Tracer.new(1, 'GET', '/test')
60
+ end
61
+
62
+ it 'should provide a mechanism to trace a code block' do
63
+ lambda { @tracer.run { sleep(0.01) }}.should_not raise_error
64
+ end
65
+
66
+ it 'should measure execution time in milliseconds' do
67
+ @tracer.run { sleep(0.01) }
68
+ trace = Yajl::Parser.parse(@tracer.finish.to_json)['trace']
69
+
70
+ trace['range']['duration'].to_i.should == 10
71
+ end
72
+
73
+ it 'should report traced codeblocks' do
74
+ @tracer.run { sleep(0.01) }
75
+ trace = Yajl::Parser.parse(@tracer.finish.to_json)['trace']
76
+
77
+ trace['frameStack']['children'].size.should == 1
78
+
79
+ child = trace['frameStack']['children'].first
80
+ child.should include 'operation'
81
+ child['operation'].should include 'label'
82
+ child.should include 'children'
83
+ end
84
+
85
+ it 'should accept optional label for each trace' do
86
+ @tracer.run('label') { sleep(0.01) }
87
+ trace = Yajl::Parser.parse(@tracer.finish.to_json)['trace']
88
+
89
+ trace['frameStack']['children'].first['operation']['label'].should match('label')
90
+ end
91
+
92
+ it 'should produce nested traces' do
93
+ @tracer.run('parent') do
94
+ @tracer.run('child') { sleep(0.01) }
95
+ end
96
+
97
+ trace = Yajl::Parser.parse(@tracer.finish.to_json)['trace']
98
+
99
+ parent = trace['frameStack']['children'].first
100
+ parent['operation']['label'].should match('parent')
101
+ parent['children'].size.should == 1
102
+
103
+ child = parent['children'].first
104
+ child['operation']['label'].should match('child')
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,48 @@
1
+ $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__)) + '/lib'
2
+
3
+ require 'rubygems'
4
+ require 'rack/bug/speedtracer'
5
+ require 'yajl'
6
+ require 'spec'
7
+ require 'pp'
8
+
9
+ [ STDOUT, STDERR ].each { |io| io.sync = true }
10
+
11
+ def respond_with(status=200, headers={}, body=['Hello World'])
12
+ called = false
13
+ @app = lambda do |env|
14
+ called = true
15
+ response = Rack::Response.new(body, status, headers)
16
+ request = Rack::Request.new(env)
17
+
18
+ yield request, response if block_given?
19
+
20
+ response.finish
21
+ end
22
+
23
+ @app
24
+ end
25
+
26
+ def request(method, uri='/', opts={})
27
+ @errors ||= []
28
+ opts = {
29
+ 'rack.run_once' => true,
30
+ 'rack.errors' => @errors,
31
+ 'rack-bug.panels' => []
32
+ }.merge(opts)
33
+
34
+ @speedtracer ||= self.class.described_class.new(@app)
35
+ @request = Rack::MockRequest.new(@speedtracer)
36
+
37
+ yield @speedtracer if block_given?
38
+
39
+ @request.request(method.to_s.upcase, uri, opts)
40
+ end
41
+
42
+ def get(uri, env={}, &b)
43
+ request(:get, uri, env, &b)
44
+ end
45
+
46
+ def head(uri, env={}, &b)
47
+ request(:head, uri, env, &b)
48
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-bug-speedtracer
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Ilya Grigorik
14
+ - Judson Lester
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2010-07-20 00:00:00 -07:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: rack-bug
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 3
31
+ segments:
32
+ - 0
33
+ version: "0"
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: uuid
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 3
45
+ segments:
46
+ - 0
47
+ version: "0"
48
+ type: :runtime
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: yajl-ruby
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ type: :runtime
63
+ version_requirements: *id003
64
+ description: Rack-bug SpeedTracer panel for server side debugging
65
+ email: judson@lrdesign.com
66
+ executables: []
67
+
68
+ extensions: []
69
+
70
+ extra_rdoc_files:
71
+ - README.md
72
+ files:
73
+ - README.md
74
+ - Rakefile
75
+ - VERSION
76
+ - examples/runner.png
77
+ - examples/runner.rb
78
+ - lib/rack/bug/speedtracer.rb
79
+ - lib/rack/bug/views/serverevent.html.erb
80
+ - lib/rack/bug/views/traces.html.erb
81
+ - lib/rack/bug/speedtracer/profiling.rb
82
+ - lib/rack/bug/speedtracer/duck-puncher.rb
83
+ - lib/rack/bug/speedtracer/default-rails-tracing.rb
84
+ - lib/rack/bug/speedtracer/trace-app.rb
85
+ - lib/rack/bug/speedtracer/render.rb
86
+ - lib/rack/bug/speedtracer/tracer.rb
87
+ - lib/rack-bug-speedtracer.rb
88
+ - spec_help/spec_helper.rb
89
+ - spec/speedtracer_spec.rb
90
+ - spec/profiling-speedtracer.rb
91
+ - spec/duck-puncher.rb
92
+ - spec/tracer_spec.rb
93
+ has_rdoc: true
94
+ homepage: http://github.com/nyarly/rack-speedtracer
95
+ licenses: []
96
+
97
+ post_install_message:
98
+ rdoc_options:
99
+ - --charset=UTF-8
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ hash: 3
108
+ segments:
109
+ - 0
110
+ version: "0"
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ hash: 3
117
+ segments:
118
+ - 0
119
+ version: "0"
120
+ requirements: []
121
+
122
+ rubyforge_project: rack-bug-speedtracer
123
+ rubygems_version: 1.3.7
124
+ signing_key:
125
+ specification_version: 3
126
+ summary: Rack::Bug Panel for server side information for Speed Tracer
127
+ test_files:
128
+ - spec/speedtracer_spec.rb
129
+ - spec/tracer_spec.rb
130
+ - examples/runner.rb