Merge branch 'szzh' of http://repository.trustie.net/xianbo/trustie2 into szzh
This commit is contained in:
commit
b52690adc2
16
Gemfile
16
Gemfile
|
@ -6,6 +6,7 @@ unless RUBY_PLATFORM =~ /w32/
|
|||
gem 'rubyzip'
|
||||
gem 'zip-zip'
|
||||
end
|
||||
|
||||
gem 'seems_rateable', path: 'lib/seems_rateable'
|
||||
gem "rails", "3.2.13"
|
||||
gem "jquery-rails", "~> 2.0.2"
|
||||
|
@ -15,6 +16,11 @@ gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
|
|||
gem "builder", "3.0.0"
|
||||
gem 'acts-as-taggable-on'
|
||||
|
||||
group :development do
|
||||
gem 'better_errors', path: 'lib/better_errors'
|
||||
gem 'rack-mini-profiler', path: 'lib/rack-mini-profiler'
|
||||
end
|
||||
|
||||
# Optional gem for LDAP authentication
|
||||
group :ldap do
|
||||
gem "net-ldap", "~> 0.3.1"
|
||||
|
@ -70,16 +76,6 @@ else
|
|||
warn("Please configure your config/database.yml first")
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem "rdoc", ">= 2.4.2"
|
||||
if nil
|
||||
gem 'thin'
|
||||
gem 'rack-mini-profiler'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
|
||||
if File.exists?(local_gemfile)
|
||||
puts "Loading Gemfile.local ..." if $DEBUG # `ruby -d` or `bundle -v`
|
||||
|
|
16
Gemfile.lock
16
Gemfile.lock
|
@ -1,3 +1,16 @@
|
|||
PATH
|
||||
remote: lib/better_errors
|
||||
specs:
|
||||
better_errors (1.1.0)
|
||||
coderay (>= 1.0.0)
|
||||
erubis (>= 2.6.6)
|
||||
|
||||
PATH
|
||||
remote: lib/rack-mini-profiler
|
||||
specs:
|
||||
rack-mini-profiler (0.9.1)
|
||||
rack (>= 1.1.3)
|
||||
|
||||
PATH
|
||||
remote: lib/seems_rateable
|
||||
specs:
|
||||
|
@ -105,6 +118,7 @@ DEPENDENCIES
|
|||
activerecord-jdbc-adapter (= 1.2.5)
|
||||
activerecord-jdbcmysql-adapter
|
||||
acts-as-taggable-on
|
||||
better_errors!
|
||||
builder (= 3.0.0)
|
||||
coderay (~> 1.0.6)
|
||||
fastercsv (~> 1.5.0)
|
||||
|
@ -112,8 +126,8 @@ DEPENDENCIES
|
|||
jquery-rails (~> 2.0.2)
|
||||
mysql2 (~> 0.3.11)
|
||||
net-ldap (~> 0.3.1)
|
||||
rack-mini-profiler!
|
||||
rack-openid
|
||||
rails (= 3.2.13)
|
||||
rdoc (>= 2.4.2)
|
||||
ruby-openid (~> 2.1.4)
|
||||
seems_rateable!
|
||||
|
|
|
@ -716,6 +716,11 @@ class ProjectsController < ApplicationController
|
|||
@document = @project.documents.build
|
||||
#
|
||||
@base_courses_tag = @project.project_type
|
||||
#判断能否显示真名(当前用户为课程的教师时显示真名)
|
||||
if @project.project_type == Project::ProjectType_course
|
||||
@teachers= searchTeacherAndAssistant(@project)
|
||||
@canShowRealName = isCourseTeacher(User.current.id)
|
||||
end
|
||||
respond_to do |format|
|
||||
format.html{render :layout => 'base_courses' if @base_courses_tag==1}
|
||||
format.api
|
||||
|
|
|
@ -65,7 +65,7 @@ class UsersController < ApplicationController
|
|||
def user_projects
|
||||
|
||||
if User.current.admin?
|
||||
@memberships = @user.memberships.all
|
||||
@memberships = @user.memberships.all(conditions: "projects.project_type = #{Project::ProjectType_project}")
|
||||
else
|
||||
cond = Project.visible_condition(User.current) + " AND projects.project_type <> 1"
|
||||
@memberships = @user.memberships.all(:conditions => cond)
|
||||
|
|
|
@ -58,9 +58,14 @@ module ApplicationHelper
|
|||
end
|
||||
|
||||
# Displays a link to user's account page if active
|
||||
def link_to_user(user, options={})
|
||||
def link_to_user(user, canShowRealName = false, options={})
|
||||
if user.is_a?(User)
|
||||
name = h(user.name(options[:format]))
|
||||
if canShowRealName
|
||||
name = h(user.realname(options[:format]))
|
||||
else
|
||||
name = h(user.name(options[:format]))
|
||||
end
|
||||
|
||||
if user.active? || (User.current.admin? && user.logged?)
|
||||
link_to name, {:controller=> 'users', :action => 'show', id: user.id, host: Setting.user_domain}, :class => user.css_classes
|
||||
else
|
||||
|
|
|
@ -22,7 +22,7 @@ class Mailer < ActionMailer::Base
|
|||
helper :custom_fields
|
||||
|
||||
include Redmine::I18n
|
||||
|
||||
include CoursesHelper
|
||||
def self.default_url_options
|
||||
{ :host => Setting.host_name, :protocol => Setting.protocol }
|
||||
end
|
||||
|
@ -44,6 +44,16 @@ class Mailer < ActionMailer::Base
|
|||
when :Bid
|
||||
respond_url(journals_for_message.jour, anchor: "word_li_#{journals_for_message.id}")
|
||||
when :Project
|
||||
if journals_for_message.jour.project_type == 1
|
||||
project = journals_for_message.jour
|
||||
@teachers = searchTeacherAndAssistant journals_for_message.jour
|
||||
@recipients ||= []
|
||||
@teachers.each do |teacher|
|
||||
@recipients << teacher
|
||||
end
|
||||
mail :to => @recipients,
|
||||
:subject => "您的课程#{journals_for_message.jour.name}中有了新的留言"
|
||||
end
|
||||
project_feedback_url(journals_for_message.jour, anchor: "word_li_#{journals_for_message.id}")
|
||||
when :Contest
|
||||
show_contest_contest_url(journals_for_message.jour, anchor: "word_li_#{journals_for_message.id}")
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<td colspan="2" valign="top">
|
||||
<strong> <%= h(e.project) if @project.nil? || @project.id != e.project.id %></strong>
|
||||
<span class="font_lighter">
|
||||
<%= link_to_user(e.event_author) if e.respond_to?(:event_author) %>
|
||||
<%= link_to_user(e.event_author,@canShowRealName) if e.respond_to?(:event_author) %>
|
||||
<%= l(:label_new_activity) %> </span>
|
||||
|
||||
<%= link_to "#{eventToLanguageCourse(e.event_type, @project)}: "<< format_activity_title(e.event_title), (e.event_type.eql?("attachment")&&e.container.kind_of?(Project)) ? project_files_path(e.container) : e.event_url %>
|
||||
|
@ -46,6 +46,7 @@
|
|||
|
||||
<!-- Added by Longjun 在最后一页显示创建信息 -->
|
||||
<% if format_date(day) == format_date(@date_to - @days) %>
|
||||
<h1>Test</h1>
|
||||
<div >
|
||||
<table width="660">
|
||||
<tr>
|
||||
|
@ -80,7 +81,16 @@
|
|||
<td colspan="2">
|
||||
<table width="580">
|
||||
<tr>
|
||||
<td > <%= link_to (h @user.try(:name)), user_path(@user) if @user %> <%= l(:label_user_create_project) %> <%= link_to @project.name %><strong> !</strong></td>
|
||||
<td >
|
||||
<%
|
||||
#判断是否显示真名
|
||||
if @canShowRealName
|
||||
%>
|
||||
<%= link_to (h @user.try(:realname)), user_path(@user) if @user %>
|
||||
<% else %>
|
||||
<%= link_to (h @user.try(:name)), user_path(@user) if @user %>
|
||||
<% end %>
|
||||
<%= l(:label_user_create_project) %> <%= link_to @project.name %><strong> !</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font_lighter" style="float: right"><%= l :label_update_time %>: <%= format_time(@project.created_on) %>
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
<hr />
|
||||
<% users_results.each do |user| %>
|
||||
<p class="font_description2">
|
||||
<strong><%= l(:label_tags_user_name) %><%= link_to ("#{user.firstname+user.lastname}"),
|
||||
<strong><%= l(:label_tags_user_name) %><%= link_to ("#{user.name}"),
|
||||
:controller => "users",:action => "show",:id => user.id%></strong>
|
||||
<br />
|
||||
<!--<strong><%= l(:label_tags_user_mail) %></strong><%= mail_to(h(user.mail)) %>-->
|
||||
</p>
|
||||
<div class="line_under"></div>
|
||||
<% end %>
|
||||
|
|
|
@ -266,7 +266,7 @@ en:
|
|||
field_is_public: Public
|
||||
field_parent: Subproject of
|
||||
field_is_in_roadmap: Issues displayed in roadmap
|
||||
field_login: Login
|
||||
field_login: Account/Email
|
||||
field_mail_notification: Email notifications
|
||||
field_admin: Administrator
|
||||
field_last_login_on: Last connection
|
||||
|
|
|
@ -284,7 +284,7 @@ zh:
|
|||
field_is_public: 公开
|
||||
field_parent: 上级项目
|
||||
field_is_in_roadmap: 在路线图中显示
|
||||
field_login: 登录名
|
||||
field_login: 账户/邮箱
|
||||
field_mail_notification: 邮件通知
|
||||
field_admin: 管理员
|
||||
field_last_login_on: 最后登录
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
language: ruby
|
||||
rvm:
|
||||
- 2.1.0
|
||||
- 2.0.0
|
|
@ -0,0 +1 @@
|
|||
--markup markdown --no-private
|
|
@ -0,0 +1,3 @@
|
|||
# Changelog
|
||||
|
||||
See https://github.com/charliesome/better_errors/releases
|
|
@ -0,0 +1,10 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gemspec
|
||||
|
||||
gem "rake"
|
||||
gem "rspec", "2.14.1"
|
||||
gem "binding_of_caller", platforms: :ruby
|
||||
gem "pry", "0.9.12"
|
||||
gem "yard"
|
||||
gem "kramdown"
|
|
@ -0,0 +1,22 @@
|
|||
Copyright (c) 2014 Charlie Somerville
|
||||
|
||||
MIT License
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
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,103 @@
|
|||
# Better Errors [![Gem Version](http://img.shields.io/gem/v/better_errors.svg)](https://rubygems.org/gems/better_errors) [![Build Status](https://travis-ci.org/charliesome/better_errors.svg)](https://travis-ci.org/charliesome/better_errors) [![Code Climate](http://img.shields.io/codeclimate/github/charliesome/better_errors.svg)](https://codeclimate.com/github/charliesome/better_errors)
|
||||
|
||||
Better Errors replaces the standard Rails error page with a much better and more useful error page. It is also usable outside of Rails in any Rack app as Rack middleware.
|
||||
|
||||
![image](http://i.imgur.com/6zBGAAb.png)
|
||||
|
||||
## Features
|
||||
|
||||
* Full stack trace
|
||||
* Source code inspection for all stack frames (with highlighting)
|
||||
* Local and instance variable inspection
|
||||
* Live REPL on every stack frame
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your Gemfile:
|
||||
|
||||
```ruby
|
||||
group :development do
|
||||
gem "better_errors"
|
||||
end
|
||||
```
|
||||
|
||||
If you would like to use Better Errors' **advanced features** (REPL, local/instance variable inspection, pretty stack frame names), you need to add the [`binding_of_caller`](https://github.com/banister/binding_of_caller) gem by [@banisterfiend](http://twitter.com/banisterfiend) to your Gemfile:
|
||||
|
||||
```ruby
|
||||
gem "binding_of_caller"
|
||||
```
|
||||
|
||||
This is an optional dependency however, and Better Errors will work without it.
|
||||
|
||||
_Note: If you discover that Better Errors isn't working - particularly after upgrading from version 0.5.0 or less - be sure to set `config.consider_all_requests_local = true` in `config/environments/development.rb`._
|
||||
|
||||
## Security
|
||||
|
||||
**NOTE:** It is *critical* you put better\_errors in the **development** section. **Do NOT run better_errors in production, or on Internet facing hosts.**
|
||||
|
||||
You will notice that the only machine that gets the Better Errors page is localhost, which means you get the default error page if you are developing on a remote host (or a virtually remote host, such as a Vagrant box). Obviously, the REPL is not something you want to expose to the public, but there may also be other pieces of sensitive information available in the backtrace.
|
||||
|
||||
To poke selective holes in this security mechanism, you can add a line like this to your startup (for example, on Rails it would be `config/environments/development.rb`)
|
||||
|
||||
```ruby
|
||||
BetterErrors::Middleware.allow_ip! ENV['TRUSTED_IP'] if ENV['TRUSTED_IP']
|
||||
```
|
||||
|
||||
Then run Rails like this:
|
||||
|
||||
```shell
|
||||
TRUSTED_IP=66.68.96.220 rails s
|
||||
```
|
||||
|
||||
Note that the `allow_ip!` is actually backed by a `Set`, so you can add more than one IP address or subnet.
|
||||
|
||||
**Tip:** You can find your apparent IP by hitting the old error page's "Show env dump" and looking at "REMOTE_ADDR".
|
||||
|
||||
**VirtualBox:** If you are using VirtualBox and are accessing the guest from your host's browser, you will need to use `allow_ip!` to see the error page.
|
||||
|
||||
## Usage
|
||||
|
||||
If you're using Rails, there's nothing else you need to do.
|
||||
|
||||
If you're not using Rails, you need to insert `BetterErrors::Middleware` into your middleware stack, and optionally set `BetterErrors.application_root` if you'd like Better Errors to abbreviate filenames within your application.
|
||||
|
||||
Here's an example using Sinatra:
|
||||
|
||||
```ruby
|
||||
require "sinatra"
|
||||
require "better_errors"
|
||||
|
||||
configure :development do
|
||||
use BetterErrors::Middleware
|
||||
BetterErrors.application_root = __dir__
|
||||
end
|
||||
|
||||
get "/" do
|
||||
raise "oops"
|
||||
end
|
||||
```
|
||||
|
||||
### Unicorn, Puma, and other multi-worker servers
|
||||
|
||||
Better Errors works by leaving a lot of context in server process memory. If
|
||||
you're using a web server that runs multiple "workers" it's likely that a second
|
||||
request (as happens when you click on a stack frame) will hit a different
|
||||
worker. That worker won't have the necessary context in memory, and you'll see
|
||||
a `Session Expired` message.
|
||||
|
||||
If this is the case for you, consider turning the number of workers to one (1)
|
||||
in `development`. Another option would be to use Webrick, Mongrel, Thin,
|
||||
or another single-process server as your `rails server`, when you are trying
|
||||
to troubleshoot an issue in development.
|
||||
|
||||
## Get in touch!
|
||||
|
||||
If you're using better_errors, I'd love to hear from you. Drop me a line and tell me what you think!
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork it
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||
4. Push to the branch (`git push origin my-new-feature`)
|
||||
5. Create new Pull Request
|
|
@ -0,0 +1,13 @@
|
|||
require "bundler/gem_tasks"
|
||||
require "rspec/core/rake_task"
|
||||
|
||||
namespace :test do
|
||||
RSpec::Core::RakeTask.new(:with_binding_of_caller)
|
||||
|
||||
without_task = RSpec::Core::RakeTask.new(:without_binding_of_caller)
|
||||
without_task.ruby_opts = "-I spec -r without_binding_of_caller"
|
||||
|
||||
task :all => [:with_binding_of_caller, :without_binding_of_caller]
|
||||
end
|
||||
|
||||
task :default => "test:all"
|
|
@ -0,0 +1,27 @@
|
|||
lib = File.expand_path('../lib', __FILE__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
require 'better_errors/version'
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "better_errors"
|
||||
s.version = BetterErrors::VERSION
|
||||
s.authors = ["Charlie Somerville"]
|
||||
s.email = ["charlie@charliesomerville.com"]
|
||||
s.description = %q{Provides a better error page for Rails and other Rack apps. Includes source code inspection, a live REPL and local/instance variable inspection for all stack frames.}
|
||||
s.summary = %q{Better error page for Rails and other Rack apps}
|
||||
s.homepage = "https://github.com/charliesome/better_errors"
|
||||
s.license = "MIT"
|
||||
|
||||
s.files = `git ls-files`.split($/)
|
||||
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
||||
s.require_paths = ["lib"]
|
||||
|
||||
s.required_ruby_version = ">= 2.0.0"
|
||||
|
||||
s.add_dependency "erubis", ">= 2.6.6"
|
||||
s.add_dependency "coderay", ">= 1.0.0"
|
||||
|
||||
# optional dependencies:
|
||||
# s.add_dependency "binding_of_caller"
|
||||
# s.add_dependency "pry"
|
||||
end
|
|
@ -0,0 +1,146 @@
|
|||
require "pp"
|
||||
require "erubis"
|
||||
require "coderay"
|
||||
require "uri"
|
||||
|
||||
require "better_errors/code_formatter"
|
||||
require "better_errors/error_page"
|
||||
require "better_errors/middleware"
|
||||
require "better_errors/raised_exception"
|
||||
require "better_errors/repl"
|
||||
require "better_errors/stack_frame"
|
||||
require "better_errors/version"
|
||||
|
||||
module BetterErrors
|
||||
POSSIBLE_EDITOR_PRESETS = [
|
||||
{ symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
|
||||
{ symbols: [:macvim, :mvim], sniff: /vim/i, url: proc { |file, line| "mvim://open?url=file://#{file}&line=#{line}" } },
|
||||
{ symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
|
||||
{ symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
|
||||
]
|
||||
|
||||
class << self
|
||||
# The path to the root of the application. Better Errors uses this property
|
||||
# to determine if a file in a backtrace should be considered an application
|
||||
# frame. If you are using Better Errors with Rails, you do not need to set
|
||||
# this attribute manually.
|
||||
#
|
||||
# @return [String]
|
||||
attr_accessor :application_root
|
||||
|
||||
# The logger to use when logging exception details and backtraces. If you
|
||||
# are using Better Errors with Rails, you do not need to set this attribute
|
||||
# manually. If this attribute is `nil`, nothing will be logged.
|
||||
#
|
||||
# @return [Logger, nil]
|
||||
attr_accessor :logger
|
||||
|
||||
# @private
|
||||
attr_accessor :binding_of_caller_available
|
||||
|
||||
# @private
|
||||
alias_method :binding_of_caller_available?, :binding_of_caller_available
|
||||
|
||||
# The ignored instance variables.
|
||||
# @return [Array]
|
||||
attr_accessor :ignored_instance_variables
|
||||
end
|
||||
@ignored_instance_variables = []
|
||||
|
||||
# Returns a proc, which when called with a filename and line number argument,
|
||||
# returns a URL to open the filename and line in the selected editor.
|
||||
#
|
||||
# Generates TextMate URLs by default.
|
||||
#
|
||||
# BetterErrors.editor["/some/file", 123]
|
||||
# # => txmt://open?url=file:///some/file&line=123
|
||||
#
|
||||
# @return [Proc]
|
||||
def self.editor
|
||||
@editor
|
||||
end
|
||||
|
||||
# Configures how Better Errors generates open-in-editor URLs.
|
||||
#
|
||||
# @overload BetterErrors.editor=(sym)
|
||||
# Uses one of the preset editor configurations. Valid symbols are:
|
||||
#
|
||||
# * `:textmate`, `:txmt`, `:tm`
|
||||
# * `:sublime`, `:subl`, `:st`
|
||||
# * `:macvim`
|
||||
#
|
||||
# @param [Symbol] sym
|
||||
#
|
||||
# @overload BetterErrors.editor=(str)
|
||||
# Uses `str` as the format string for generating open-in-editor URLs.
|
||||
#
|
||||
# Use `%{file}` and `%{line}` as placeholders for the actual values.
|
||||
#
|
||||
# @example
|
||||
# BetterErrors.editor = "my-editor://open?url=%{file}&line=%{line}"
|
||||
#
|
||||
# @param [String] str
|
||||
#
|
||||
# @overload BetterErrors.editor=(proc)
|
||||
# Uses `proc` to generate open-in-editor URLs. The proc will be called
|
||||
# with `file` and `line` parameters when a URL needs to be generated.
|
||||
#
|
||||
# Your proc should take care to escape `file` appropriately with
|
||||
# `URI.encode_www_form_component` (please note that `URI.escape` is **not**
|
||||
# a suitable substitute.)
|
||||
#
|
||||
# @example
|
||||
# BetterErrors.editor = proc { |file, line|
|
||||
# "my-editor://open?url=#{URI.encode_www_form_component file}&line=#{line}"
|
||||
# }
|
||||
#
|
||||
# @param [Proc] proc
|
||||
#
|
||||
def self.editor=(editor)
|
||||
POSSIBLE_EDITOR_PRESETS.each do |config|
|
||||
if config[:symbols].include?(editor)
|
||||
return self.editor = config[:url]
|
||||
end
|
||||
end
|
||||
|
||||
if editor.is_a? String
|
||||
self.editor = proc { |file, line| editor % { file: URI.encode_www_form_component(file), line: line } }
|
||||
else
|
||||
if editor.respond_to? :call
|
||||
@editor = editor
|
||||
else
|
||||
raise TypeError, "Expected editor to be a valid editor key, a format string or a callable."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Enables experimental Pry support in the inline REPL
|
||||
#
|
||||
# If you encounter problems while using Pry, *please* file a bug report at
|
||||
# https://github.com/charliesome/better_errors/issues
|
||||
def self.use_pry!
|
||||
REPL::PROVIDERS.unshift const: :Pry, impl: "better_errors/repl/pry"
|
||||
end
|
||||
|
||||
# Automatically sniffs a default editor preset based on the EDITOR
|
||||
# environment variable.
|
||||
#
|
||||
# @return [Symbol]
|
||||
def self.default_editor
|
||||
POSSIBLE_EDITOR_PRESETS.detect(-> { {} }) { |config|
|
||||
ENV["EDITOR"] =~ config[:sniff]
|
||||
}[:url] || :textmate
|
||||
end
|
||||
|
||||
BetterErrors.editor = default_editor
|
||||
end
|
||||
|
||||
begin
|
||||
require "binding_of_caller"
|
||||
require "better_errors/exception_extension"
|
||||
BetterErrors.binding_of_caller_available = true
|
||||
rescue LoadError => e
|
||||
BetterErrors.binding_of_caller_available = false
|
||||
end
|
||||
|
||||
require "better_errors/rails" if defined? Rails::Railtie
|
|
@ -0,0 +1,63 @@
|
|||
module BetterErrors
|
||||
# @private
|
||||
class CodeFormatter
|
||||
require "better_errors/code_formatter/html"
|
||||
require "better_errors/code_formatter/text"
|
||||
|
||||
FILE_TYPES = {
|
||||
".rb" => :ruby,
|
||||
"" => :ruby,
|
||||
".html" => :html,
|
||||
".erb" => :erb,
|
||||
".haml" => :haml
|
||||
}
|
||||
|
||||
attr_reader :filename, :line, :context
|
||||
|
||||
def initialize(filename, line, context = 5)
|
||||
@filename = filename
|
||||
@line = line
|
||||
@context = context
|
||||
end
|
||||
|
||||
def output
|
||||
formatted_code
|
||||
rescue Errno::ENOENT, Errno::EINVAL
|
||||
source_unavailable
|
||||
end
|
||||
|
||||
def formatted_code
|
||||
formatted_lines.join
|
||||
end
|
||||
|
||||
def coderay_scanner
|
||||
ext = File.extname(filename)
|
||||
FILE_TYPES[ext] || :text
|
||||
end
|
||||
|
||||
def each_line_of(lines, &blk)
|
||||
line_range.zip(lines).map { |current_line, str|
|
||||
yield (current_line == line), current_line, str
|
||||
}
|
||||
end
|
||||
|
||||
def highlighted_lines
|
||||
CodeRay.scan(context_lines.join, coderay_scanner).div(wrap: nil).lines
|
||||
end
|
||||
|
||||
def context_lines
|
||||
range = line_range
|
||||
source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL
|
||||
end
|
||||
|
||||
def source_lines
|
||||
@source_lines ||= File.readlines(filename)
|
||||
end
|
||||
|
||||
def line_range
|
||||
min = [line - context, 1].max
|
||||
max = [line + context, source_lines.count].min
|
||||
min..max
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
module BetterErrors
|
||||
# @private
|
||||
class CodeFormatter::HTML < CodeFormatter
|
||||
def source_unavailable
|
||||
"<p class='unavailable'>Source is not available</p>"
|
||||
end
|
||||
|
||||
def formatted_lines
|
||||
each_line_of(highlighted_lines) { |highlight, current_line, str|
|
||||
class_name = highlight ? "highlight" : ""
|
||||
sprintf '<pre class="%s">%s</pre>', class_name, str
|
||||
}
|
||||
end
|
||||
|
||||
def formatted_nums
|
||||
each_line_of(highlighted_lines) { |highlight, current_line, str|
|
||||
class_name = highlight ? "highlight" : ""
|
||||
sprintf '<span class="%s">%5d</span>', class_name, current_line
|
||||
}
|
||||
end
|
||||
|
||||
def formatted_code
|
||||
%{<div class="code_linenums">#{formatted_nums.join}</div><div class="code">#{super}</div>}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
module BetterErrors
|
||||
# @private
|
||||
class CodeFormatter::Text < CodeFormatter
|
||||
def source_unavailable
|
||||
"# Source is not available"
|
||||
end
|
||||
|
||||
def formatted_lines
|
||||
each_line_of(context_lines) { |highlight, current_line, str|
|
||||
sprintf '%s %3d %s', (highlight ? '>' : ' '), current_line, str
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,110 @@
|
|||
require "cgi"
|
||||
require "json"
|
||||
require "securerandom"
|
||||
|
||||
module BetterErrors
|
||||
# @private
|
||||
class ErrorPage
|
||||
def self.template_path(template_name)
|
||||
File.expand_path("../templates/#{template_name}.erb", __FILE__)
|
||||
end
|
||||
|
||||
def self.template(template_name)
|
||||
Erubis::EscapedEruby.new(File.read(template_path(template_name)))
|
||||
end
|
||||
|
||||
attr_reader :exception, :env, :repls
|
||||
|
||||
def initialize(exception, env)
|
||||
@exception = RaisedException.new(exception)
|
||||
@env = env
|
||||
@start_time = Time.now.to_f
|
||||
@repls = []
|
||||
end
|
||||
|
||||
def id
|
||||
@id ||= SecureRandom.hex(8)
|
||||
end
|
||||
|
||||
def render(template_name = "main")
|
||||
self.class.template(template_name).result binding
|
||||
end
|
||||
|
||||
def do_variables(opts)
|
||||
index = opts["index"].to_i
|
||||
@frame = backtrace_frames[index]
|
||||
@var_start_time = Time.now.to_f
|
||||
{ html: render("variable_info") }
|
||||
end
|
||||
|
||||
def do_eval(opts)
|
||||
index = opts["index"].to_i
|
||||
code = opts["source"]
|
||||
|
||||
unless binding = backtrace_frames[index].frame_binding
|
||||
return { error: "REPL unavailable in this stack frame" }
|
||||
end
|
||||
|
||||
result, prompt, prefilled_input =
|
||||
(@repls[index] ||= REPL.provider.new(binding)).send_input(code)
|
||||
|
||||
{ result: result,
|
||||
prompt: prompt,
|
||||
prefilled_input: prefilled_input,
|
||||
highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil) }
|
||||
end
|
||||
|
||||
def backtrace_frames
|
||||
exception.backtrace
|
||||
end
|
||||
|
||||
def application_frames
|
||||
backtrace_frames.select(&:application?)
|
||||
end
|
||||
|
||||
def first_frame
|
||||
application_frames.first || backtrace_frames.first
|
||||
end
|
||||
|
||||
private
|
||||
def editor_url(frame)
|
||||
BetterErrors.editor[frame.filename, frame.line]
|
||||
end
|
||||
|
||||
def rack_session
|
||||
env['rack.session']
|
||||
end
|
||||
|
||||
def rails_params
|
||||
env['action_dispatch.request.parameters']
|
||||
end
|
||||
|
||||
def uri_prefix
|
||||
env["SCRIPT_NAME"] || ""
|
||||
end
|
||||
|
||||
def request_path
|
||||
env["PATH_INFO"]
|
||||
end
|
||||
|
||||
def html_formatted_code_block(frame)
|
||||
CodeFormatter::HTML.new(frame.filename, frame.line).output
|
||||
end
|
||||
|
||||
def text_formatted_code_block(frame)
|
||||
CodeFormatter::Text.new(frame.filename, frame.line).output
|
||||
end
|
||||
|
||||
def text_heading(char, str)
|
||||
str + "\n" + char*str.size
|
||||
end
|
||||
|
||||
def inspect_value(obj)
|
||||
CGI.escapeHTML(obj.inspect)
|
||||
rescue NoMethodError
|
||||
"<span class='unsupported'>(object doesn't support inspect)</span>"
|
||||
rescue Exception => e
|
||||
"<span class='unsupported'>(exception was raised in inspect)</span>"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
module BetterErrors
|
||||
module ExceptionExtension
|
||||
prepend_features Exception
|
||||
|
||||
def set_backtrace(*)
|
||||
if caller_locations.none? { |loc| loc.path == __FILE__ }
|
||||
@__better_errors_bindings_stack = binding.callers.drop(1)
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def __better_errors_bindings_stack
|
||||
@__better_errors_bindings_stack || []
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,141 @@
|
|||
require "json"
|
||||
require "ipaddr"
|
||||
require "set"
|
||||
|
||||
module BetterErrors
|
||||
# Better Errors' error handling middleware. Including this in your middleware
|
||||
# stack will show a Better Errors error page for exceptions raised below this
|
||||
# middleware.
|
||||
#
|
||||
# If you are using Ruby on Rails, you do not need to manually insert this
|
||||
# middleware into your middleware stack.
|
||||
#
|
||||
# @example Sinatra
|
||||
# require "better_errors"
|
||||
#
|
||||
# if development?
|
||||
# use BetterErrors::Middleware
|
||||
# end
|
||||
#
|
||||
# @example Rack
|
||||
# require "better_errors"
|
||||
# if ENV["RACK_ENV"] == "development"
|
||||
# use BetterErrors::Middleware
|
||||
# end
|
||||
#
|
||||
class Middleware
|
||||
# The set of IP addresses that are allowed to access Better Errors.
|
||||
#
|
||||
# Set to `{ "127.0.0.1/8", "::1/128" }` by default.
|
||||
ALLOWED_IPS = Set.new
|
||||
|
||||
# Adds an address to the set of IP addresses allowed to access Better
|
||||
# Errors.
|
||||
def self.allow_ip!(addr)
|
||||
ALLOWED_IPS << IPAddr.new(addr)
|
||||
end
|
||||
|
||||
allow_ip! "127.0.0.0/8"
|
||||
allow_ip! "::1/128" rescue nil # windows ruby doesn't have ipv6 support
|
||||
|
||||
# A new instance of BetterErrors::Middleware
|
||||
#
|
||||
# @param app The Rack app/middleware to wrap with Better Errors
|
||||
# @param handler The error handler to use.
|
||||
def initialize(app, handler = ErrorPage)
|
||||
@app = app
|
||||
@handler = handler
|
||||
end
|
||||
|
||||
# Calls the Better Errors middleware
|
||||
#
|
||||
# @param [Hash] env
|
||||
# @return [Array]
|
||||
def call(env)
|
||||
if allow_ip? env
|
||||
better_errors_call env
|
||||
else
|
||||
@app.call env
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allow_ip?(env)
|
||||
# REMOTE_ADDR is not in the rack spec, so some application servers do
|
||||
# not provide it.
|
||||
return true unless env["REMOTE_ADDR"] and !env["REMOTE_ADDR"].strip.empty?
|
||||
ip = IPAddr.new env["REMOTE_ADDR"].split("%").first
|
||||
ALLOWED_IPS.any? { |subnet| subnet.include? ip }
|
||||
end
|
||||
|
||||
def better_errors_call(env)
|
||||
case env["PATH_INFO"]
|
||||
when %r{/__better_errors/(?<id>.+?)/(?<method>\w+)\z}
|
||||
internal_call env, $~
|
||||
when %r{/__better_errors/?\z}
|
||||
show_error_page env
|
||||
else
|
||||
protected_app_call env
|
||||
end
|
||||
end
|
||||
|
||||
def protected_app_call(env)
|
||||
@app.call env
|
||||
rescue Exception => ex
|
||||
@error_page = @handler.new ex, env
|
||||
log_exception
|
||||
show_error_page(env, ex)
|
||||
end
|
||||
|
||||
def show_error_page(env, exception=nil)
|
||||
type, content = if @error_page
|
||||
if text?(env)
|
||||
[ 'plain', @error_page.render('text') ]
|
||||
else
|
||||
[ 'html', @error_page.render ]
|
||||
end
|
||||
else
|
||||
[ 'html', no_errors_page ]
|
||||
end
|
||||
|
||||
status_code = 500
|
||||
if defined? ActionDispatch::ExceptionWrapper
|
||||
status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code
|
||||
end
|
||||
|
||||
[status_code, { "Content-Type" => "text/#{type}; charset=utf-8" }, [content]]
|
||||
end
|
||||
|
||||
def text?(env)
|
||||
env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" ||
|
||||
!env["HTTP_ACCEPT"].to_s.include?('html')
|
||||
end
|
||||
|
||||
def log_exception
|
||||
return unless BetterErrors.logger
|
||||
|
||||
message = "\n#{@error_page.exception.type} - #{@error_page.exception.message}:\n"
|
||||
@error_page.backtrace_frames.each do |frame|
|
||||
message << " #{frame}\n"
|
||||
end
|
||||
|
||||
BetterErrors.logger.fatal message
|
||||
end
|
||||
|
||||
def internal_call(env, opts)
|
||||
if opts[:id] != @error_page.id
|
||||
return [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(error: "Session expired")]]
|
||||
end
|
||||
|
||||
env["rack.input"].rewind
|
||||
response = @error_page.send("do_#{opts[:method]}", JSON.parse(env["rack.input"].read))
|
||||
[200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(response)]]
|
||||
end
|
||||
|
||||
def no_errors_page
|
||||
"<h1>No errors</h1><p>No errors have been recorded yet.</p><hr>" +
|
||||
"<code>Better Errors v#{BetterErrors::VERSION}</code>"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
module BetterErrors
|
||||
# @private
|
||||
class Railtie < Rails::Railtie
|
||||
initializer "better_errors.configure_rails_initialization" do
|
||||
if use_better_errors?
|
||||
insert_middleware
|
||||
BetterErrors.logger = Rails.logger
|
||||
BetterErrors.application_root = Rails.root.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def insert_middleware
|
||||
if defined? ActionDispatch::DebugExceptions
|
||||
app.middleware.insert_after ActionDispatch::DebugExceptions, BetterErrors::Middleware
|
||||
else
|
||||
app.middleware.use BetterErrors::Middleware
|
||||
end
|
||||
end
|
||||
|
||||
def use_better_errors?
|
||||
!Rails.env.production? and app.config.consider_all_requests_local
|
||||
end
|
||||
|
||||
def app
|
||||
Rails.application
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,66 @@
|
|||
# @private
|
||||
module BetterErrors
|
||||
class RaisedException
|
||||
attr_reader :exception, :message, :backtrace
|
||||
|
||||
def initialize(exception)
|
||||
if exception.respond_to?(:original_exception) && exception.original_exception
|
||||
exception = exception.original_exception
|
||||
end
|
||||
|
||||
@exception = exception
|
||||
@message = exception.message
|
||||
|
||||
setup_backtrace
|
||||
massage_syntax_error
|
||||
end
|
||||
|
||||
def type
|
||||
exception.class
|
||||
end
|
||||
|
||||
private
|
||||
def has_bindings?
|
||||
exception.respond_to?(:__better_errors_bindings_stack) && exception.__better_errors_bindings_stack.any?
|
||||
end
|
||||
|
||||
def setup_backtrace
|
||||
if has_bindings?
|
||||
setup_backtrace_from_bindings
|
||||
else
|
||||
setup_backtrace_from_backtrace
|
||||
end
|
||||
end
|
||||
|
||||
def setup_backtrace_from_bindings
|
||||
@backtrace = exception.__better_errors_bindings_stack.map { |binding|
|
||||
file = binding.eval "__FILE__"
|
||||
line = binding.eval "__LINE__"
|
||||
name = binding.frame_description
|
||||
StackFrame.new(file, line, name, binding)
|
||||
}
|
||||
end
|
||||
|
||||
def setup_backtrace_from_backtrace
|
||||
@backtrace = (exception.backtrace || []).map { |frame|
|
||||
if /\A(?<file>.*?):(?<line>\d+)(:in `(?<name>.*)')?/ =~ frame
|
||||
StackFrame.new(file, line.to_i, name)
|
||||
end
|
||||
}.compact
|
||||
end
|
||||
|
||||
def massage_syntax_error
|
||||
case exception.class.to_s
|
||||
when "Haml::SyntaxError"
|
||||
if /\A(.+?):(\d+)/ =~ exception.backtrace.first
|
||||
backtrace.unshift(StackFrame.new($1, $2.to_i, ""))
|
||||
end
|
||||
when "SyntaxError"
|
||||
if /\A(.+?):(\d+): (.*)/m =~ exception.message
|
||||
backtrace.unshift(StackFrame.new($1, $2.to_i, ""))
|
||||
@message = $3
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
module BetterErrors
|
||||
# @private
|
||||
module REPL
|
||||
PROVIDERS = [
|
||||
{ impl: "better_errors/repl/basic",
|
||||
const: :Basic },
|
||||
]
|
||||
|
||||
def self.provider
|
||||
@provider ||= const_get detect[:const]
|
||||
end
|
||||
|
||||
def self.provider=(prov)
|
||||
@provider = prov
|
||||
end
|
||||
|
||||
def self.detect
|
||||
PROVIDERS.find { |prov|
|
||||
test_provider prov
|
||||
}
|
||||
end
|
||||
|
||||
def self.test_provider(provider)
|
||||
require provider[:impl]
|
||||
true
|
||||
rescue LoadError
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
module BetterErrors
|
||||
module REPL
|
||||
class Basic
|
||||
def initialize(binding)
|
||||
@binding = binding
|
||||
end
|
||||
|
||||
def send_input(str)
|
||||
[execute(str), ">>", ""]
|
||||
end
|
||||
|
||||
private
|
||||
def execute(str)
|
||||
"=> #{@binding.eval(str).inspect}\n"
|
||||
rescue Exception => e
|
||||
"!! #{e.inspect rescue e.class.to_s rescue "Exception"}\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,78 @@
|
|||
require "fiber"
|
||||
require "pry"
|
||||
|
||||
module BetterErrors
|
||||
module REPL
|
||||
class Pry
|
||||
class Input
|
||||
def readline
|
||||
Fiber.yield
|
||||
end
|
||||
end
|
||||
|
||||
class Output
|
||||
def initialize
|
||||
@buffer = ""
|
||||
end
|
||||
|
||||
def puts(*args)
|
||||
args.each do |arg|
|
||||
@buffer << "#{arg.chomp}\n"
|
||||
end
|
||||
end
|
||||
|
||||
def tty?
|
||||
false
|
||||
end
|
||||
|
||||
def read_buffer
|
||||
@buffer
|
||||
ensure
|
||||
@buffer = ""
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(binding)
|
||||
@fiber = Fiber.new do
|
||||
@pry.repl binding
|
||||
end
|
||||
@input = Input.new
|
||||
@output = Output.new
|
||||
@pry = ::Pry.new input: @input, output: @output
|
||||
@pry.hooks.clear_all if defined?(@pry.hooks.clear_all)
|
||||
@fiber.resume
|
||||
end
|
||||
|
||||
def send_input(str)
|
||||
local ::Pry.config, color: false, pager: false do
|
||||
@fiber.resume "#{str}\n"
|
||||
[@output.read_buffer, *prompt]
|
||||
end
|
||||
end
|
||||
|
||||
def prompt
|
||||
if indent = @pry.instance_variable_get(:@indent) and !indent.indent_level.empty?
|
||||
["..", indent.indent_level]
|
||||
else
|
||||
[">>", ""]
|
||||
end
|
||||
rescue
|
||||
[">>", ""]
|
||||
end
|
||||
|
||||
private
|
||||
def local(obj, attrs)
|
||||
old_attrs = {}
|
||||
attrs.each do |k, v|
|
||||
old_attrs[k] = obj.send k
|
||||
obj.send "#{k}=", v
|
||||
end
|
||||
yield
|
||||
ensure
|
||||
old_attrs.each do |k, v|
|
||||
obj.send "#{k}=", v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,111 @@
|
|||
require "set"
|
||||
|
||||
module BetterErrors
|
||||
# @private
|
||||
class StackFrame
|
||||
def self.from_exception(exception)
|
||||
RaisedException.new(exception).backtrace
|
||||
end
|
||||
|
||||
attr_reader :filename, :line, :name, :frame_binding
|
||||
|
||||
def initialize(filename, line, name, frame_binding = nil)
|
||||
@filename = filename
|
||||
@line = line
|
||||
@name = name
|
||||
@frame_binding = frame_binding
|
||||
|
||||
set_pretty_method_name if frame_binding
|
||||
end
|
||||
|
||||
def application?
|
||||
if root = BetterErrors.application_root
|
||||
filename.index(root) == 0 && filename.index("#{root}/vendor") != 0
|
||||
end
|
||||
end
|
||||
|
||||
def application_path
|
||||
filename[(BetterErrors.application_root.length+1)..-1]
|
||||
end
|
||||
|
||||
def gem?
|
||||
Gem.path.any? { |path| filename.index(path) == 0 }
|
||||
end
|
||||
|
||||
def gem_path
|
||||
if path = Gem.path.detect { |path| filename.index(path) == 0 }
|
||||
gem_name_and_version, path = filename.sub("#{path}/gems/", "").split("/", 2)
|
||||
/(?<gem_name>.+)-(?<gem_version>[\w.]+)/ =~ gem_name_and_version
|
||||
"#{gem_name} (#{gem_version}) #{path}"
|
||||
end
|
||||
end
|
||||
|
||||
def class_name
|
||||
@class_name
|
||||
end
|
||||
|
||||
def method_name
|
||||
@method_name || @name
|
||||
end
|
||||
|
||||
def context
|
||||
if gem?
|
||||
:gem
|
||||
elsif application?
|
||||
:application
|
||||
else
|
||||
:dunno
|
||||
end
|
||||
end
|
||||
|
||||
def pretty_path
|
||||
case context
|
||||
when :application; application_path
|
||||
when :gem; gem_path
|
||||
else filename
|
||||
end
|
||||
end
|
||||
|
||||
def local_variables
|
||||
return {} unless frame_binding
|
||||
frame_binding.eval("local_variables").each_with_object({}) do |name, hash|
|
||||
if defined?(frame_binding.local_variable_get)
|
||||
hash[name] = frame_binding.local_variable_get(name)
|
||||
else
|
||||
hash[name] = frame_binding.eval(name.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def instance_variables
|
||||
return {} unless frame_binding
|
||||
Hash[visible_instance_variables.map { |x|
|
||||
[x, frame_binding.eval(x.to_s)]
|
||||
}]
|
||||
end
|
||||
|
||||
def visible_instance_variables
|
||||
frame_binding.eval("instance_variables") - BetterErrors.ignored_instance_variables
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{pretty_path}:#{line}:in `#{name}'"
|
||||
end
|
||||
|
||||
private
|
||||
def set_pretty_method_name
|
||||
name =~ /\A(block (\([^)]+\) )?in )?/
|
||||
recv = frame_binding.eval("self")
|
||||
|
||||
return unless method_name = frame_binding.eval("::Kernel.__method__")
|
||||
|
||||
if Module === recv
|
||||
@class_name = "#{$1}#{recv}"
|
||||
@method_name = ".#{method_name}"
|
||||
else
|
||||
@class_name = "#{$1}#{Kernel.instance_method(:class).bind(recv).call}"
|
||||
@method_name = "##{method_name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,21 @@
|
|||
<%== text_heading("=", "%s at %s" % [exception.type, request_path]) %>
|
||||
|
||||
> <%== exception.message %>
|
||||
<% if backtrace_frames.any? %>
|
||||
|
||||
<%== text_heading("-", "%s, line %i" % [first_frame.pretty_path, first_frame.line]) %>
|
||||
|
||||
``` ruby
|
||||
<%== text_formatted_code_block(first_frame) %>```
|
||||
|
||||
App backtrace
|
||||
-------------
|
||||
|
||||
<%== application_frames.map { |s| " - #{s}" }.join("\n") %>
|
||||
|
||||
Full backtrace
|
||||
--------------
|
||||
|
||||
<%== backtrace_frames.map { |s| " - #{s}" }.join("\n") %>
|
||||
|
||||
<% end %>
|
|
@ -0,0 +1,70 @@
|
|||
<header class="trace_info clearfix">
|
||||
<div class="title">
|
||||
<h2 class="name"><%= @frame.name %></h2>
|
||||
<div class="location"><span class="filename"><a href="<%= editor_url(@frame) %>"><%= @frame.pretty_path %></a></span></div>
|
||||
</div>
|
||||
<div class="code_block clearfix">
|
||||
<%== html_formatted_code_block @frame %>
|
||||
</div>
|
||||
|
||||
<% if BetterErrors.binding_of_caller_available? && @frame.frame_binding %>
|
||||
<div class="repl">
|
||||
<div class="console">
|
||||
<pre></pre>
|
||||
<div class="prompt"><span>>></span> <input/></div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</header>
|
||||
|
||||
<% if BetterErrors.binding_of_caller_available? && @frame.frame_binding %>
|
||||
<div class="hint">
|
||||
This is a live shell. Type in here.
|
||||
</div>
|
||||
|
||||
<div class="variable_info"></div>
|
||||
<% end %>
|
||||
|
||||
<% unless BetterErrors.binding_of_caller_available? %>
|
||||
<div class="hint">
|
||||
<strong>Tip:</strong> add <code>gem "binding_of_caller"</code> to your Gemfile to enable the REPL and local/instance variable inspection.
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="sub">
|
||||
<h3>Request info</h3>
|
||||
<div class='inset variables'>
|
||||
<table class="var_table">
|
||||
<% if rails_params %>
|
||||
<tr><td class="name">Request parameters</td><td><pre><%== inspect_value rails_params %></pre></td></tr>
|
||||
<% end %>
|
||||
<% if rack_session %>
|
||||
<tr><td class="name">Rack session</td><td><pre><%== inspect_value rack_session %></pre></td></tr>
|
||||
<% end %>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sub">
|
||||
<h3>Local Variables</h3>
|
||||
<div class='inset variables'>
|
||||
<table class="var_table">
|
||||
<% @frame.local_variables.each do |name, value| %>
|
||||
<tr><td class="name"><%= name %></td><td><pre><%== inspect_value value %></pre></td></tr>
|
||||
<% end %>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sub">
|
||||
<h3>Instance Variables</h3>
|
||||
<div class="inset variables">
|
||||
<table class="var_table">
|
||||
<% @frame.instance_variables.each do |name, value| %>
|
||||
<tr><td class="name"><%= name %></td><td><pre><%== inspect_value value %></pre></td></tr>
|
||||
<% end %>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <%= Time.now.to_f - @var_start_time %> seconds -->
|
|
@ -0,0 +1,3 @@
|
|||
module BetterErrors
|
||||
VERSION = "1.1.0"
|
||||
end
|
|
@ -0,0 +1,92 @@
|
|||
require "spec_helper"
|
||||
|
||||
module BetterErrors
|
||||
describe CodeFormatter do
|
||||
let(:filename) { File.expand_path("../support/my_source.rb", __FILE__) }
|
||||
|
||||
let(:formatter) { CodeFormatter.new(filename, 8) }
|
||||
|
||||
it "picks an appropriate scanner" do
|
||||
formatter.coderay_scanner.should == :ruby
|
||||
end
|
||||
|
||||
it "shows 5 lines of context" do
|
||||
formatter.line_range.should == (3..13)
|
||||
|
||||
formatter.context_lines.should == [
|
||||
"three\n",
|
||||
"four\n",
|
||||
"five\n",
|
||||
"six\n",
|
||||
"seven\n",
|
||||
"eight\n",
|
||||
"nine\n",
|
||||
"ten\n",
|
||||
"eleven\n",
|
||||
"twelve\n",
|
||||
"thirteen\n"
|
||||
]
|
||||
end
|
||||
|
||||
it "works when the line is right on the edge" do
|
||||
formatter = CodeFormatter.new(filename, 20)
|
||||
formatter.line_range.should == (15..20)
|
||||
end
|
||||
|
||||
describe CodeFormatter::HTML do
|
||||
it "highlights the erroring line" do
|
||||
formatter = CodeFormatter::HTML.new(filename, 8)
|
||||
formatter.output.should =~ /highlight.*eight/
|
||||
end
|
||||
|
||||
it "works when the line is right on the edge" do
|
||||
formatter = CodeFormatter::HTML.new(filename, 20)
|
||||
formatter.output.should_not == formatter.source_unavailable
|
||||
end
|
||||
|
||||
it "doesn't barf when the lines don't make any sense" do
|
||||
formatter = CodeFormatter::HTML.new(filename, 999)
|
||||
formatter.output.should == formatter.source_unavailable
|
||||
end
|
||||
|
||||
it "doesn't barf when the file doesn't exist" do
|
||||
formatter = CodeFormatter::HTML.new("fkdguhskd7e l", 1)
|
||||
formatter.output.should == formatter.source_unavailable
|
||||
end
|
||||
end
|
||||
|
||||
describe CodeFormatter::Text do
|
||||
it "highlights the erroring line" do
|
||||
formatter = CodeFormatter::Text.new(filename, 8)
|
||||
formatter.output.should == <<-TEXT.gsub(/^ /, "")
|
||||
3 three
|
||||
4 four
|
||||
5 five
|
||||
6 six
|
||||
7 seven
|
||||
> 8 eight
|
||||
9 nine
|
||||
10 ten
|
||||
11 eleven
|
||||
12 twelve
|
||||
13 thirteen
|
||||
TEXT
|
||||
end
|
||||
|
||||
it "works when the line is right on the edge" do
|
||||
formatter = CodeFormatter::Text.new(filename, 20)
|
||||
formatter.output.should_not == formatter.source_unavailable
|
||||
end
|
||||
|
||||
it "doesn't barf when the lines don't make any sense" do
|
||||
formatter = CodeFormatter::Text.new(filename, 999)
|
||||
formatter.output.should == formatter.source_unavailable
|
||||
end
|
||||
|
||||
it "doesn't barf when the file doesn't exist" do
|
||||
formatter = CodeFormatter::Text.new("fkdguhskd7e l", 1)
|
||||
formatter.output.should == formatter.source_unavailable
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,76 @@
|
|||
require "spec_helper"
|
||||
|
||||
module BetterErrors
|
||||
describe ErrorPage do
|
||||
let!(:exception) { raise ZeroDivisionError, "you divided by zero you silly goose!" rescue $! }
|
||||
|
||||
let(:error_page) { ErrorPage.new exception, { "PATH_INFO" => "/some/path" } }
|
||||
|
||||
let(:response) { error_page.render }
|
||||
|
||||
let(:empty_binding) {
|
||||
local_a = :value_for_local_a
|
||||
local_b = :value_for_local_b
|
||||
|
||||
@inst_c = :value_for_inst_c
|
||||
@inst_d = :value_for_inst_d
|
||||
|
||||
binding
|
||||
}
|
||||
|
||||
it "includes the error message" do
|
||||
response.should include("you divided by zero you silly goose!")
|
||||
end
|
||||
|
||||
it "includes the request path" do
|
||||
response.should include("/some/path")
|
||||
end
|
||||
|
||||
it "includes the exception class" do
|
||||
response.should include("ZeroDivisionError")
|
||||
end
|
||||
|
||||
context "variable inspection" do
|
||||
let(:exception) { empty_binding.eval("raise") rescue $! }
|
||||
|
||||
if BetterErrors.binding_of_caller_available?
|
||||
it "shows local variables" do
|
||||
html = error_page.do_variables("index" => 0)[:html]
|
||||
html.should include("local_a")
|
||||
html.should include(":value_for_local_a")
|
||||
html.should include("local_b")
|
||||
html.should include(":value_for_local_b")
|
||||
end
|
||||
else
|
||||
it "tells the user to add binding_of_caller to their gemfile to get fancy features" do
|
||||
html = error_page.do_variables("index" => 0)[:html]
|
||||
html.should include(%{gem "binding_of_caller"})
|
||||
end
|
||||
end
|
||||
|
||||
it "shows instance variables" do
|
||||
html = error_page.do_variables("index" => 0)[:html]
|
||||
html.should include("inst_c")
|
||||
html.should include(":value_for_inst_c")
|
||||
html.should include("inst_d")
|
||||
html.should include(":value_for_inst_d")
|
||||
end
|
||||
|
||||
it "shows filter instance variables" do
|
||||
BetterErrors.stub(:ignored_instance_variables).and_return([ :@inst_d ])
|
||||
html = error_page.do_variables("index" => 0)[:html]
|
||||
html.should include("inst_c")
|
||||
html.should include(":value_for_inst_c")
|
||||
html.should_not include('<td class="name">@inst_d</td>')
|
||||
html.should_not include("<pre>:value_for_inst_d</pre>")
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't die if the source file is not a real filename" do
|
||||
exception.stub(:backtrace).and_return([
|
||||
"<internal:prelude>:10:in `spawn_rack_application'"
|
||||
])
|
||||
response.should include("Source unavailable")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,146 @@
|
|||
require "spec_helper"
|
||||
|
||||
module BetterErrors
|
||||
describe Middleware do
|
||||
let(:app) { Middleware.new(->env { ":)" }) }
|
||||
let(:exception) { RuntimeError.new("oh no :(") }
|
||||
|
||||
it "passes non-error responses through" do
|
||||
app.call({}).should == ":)"
|
||||
end
|
||||
|
||||
it "calls the internal methods" do
|
||||
app.should_receive :internal_call
|
||||
app.call("PATH_INFO" => "/__better_errors/1/preform_awesomness")
|
||||
end
|
||||
|
||||
it "calls the internal methods on any subfolder path" do
|
||||
app.should_receive :internal_call
|
||||
app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/1/preform_awesomness")
|
||||
end
|
||||
|
||||
it "shows the error page" do
|
||||
app.should_receive :show_error_page
|
||||
app.call("PATH_INFO" => "/__better_errors/")
|
||||
end
|
||||
|
||||
it "shows the error page on any subfolder path" do
|
||||
app.should_receive :show_error_page
|
||||
app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/")
|
||||
end
|
||||
|
||||
it "doesn't show the error page to a non-local address" do
|
||||
app.should_not_receive :better_errors_call
|
||||
app.call("REMOTE_ADDR" => "1.2.3.4")
|
||||
end
|
||||
|
||||
it "shows to a whitelisted IP" do
|
||||
BetterErrors::Middleware.allow_ip! '77.55.33.11'
|
||||
app.should_receive :better_errors_call
|
||||
app.call("REMOTE_ADDR" => "77.55.33.11")
|
||||
end
|
||||
|
||||
it "doesn't blow up when given a blank REMOTE_ADDR" do
|
||||
expect { app.call("REMOTE_ADDR" => " ") }.to_not raise_error
|
||||
end
|
||||
|
||||
it "doesn't blow up when given an IP address with a zone index" do
|
||||
expect { app.call("REMOTE_ADDR" => "0:0:0:0:0:0:0:1%0" ) }.to_not raise_error
|
||||
end
|
||||
|
||||
context "when requesting the /__better_errors manually" do
|
||||
let(:app) { Middleware.new(->env { ":)" }) }
|
||||
|
||||
it "shows that no errors have been recorded" do
|
||||
status, headers, body = app.call("PATH_INFO" => "/__better_errors")
|
||||
body.join.should match /No errors have been recorded yet./
|
||||
end
|
||||
|
||||
it "shows that no errors have been recorded on any subfolder path" do
|
||||
status, headers, body = app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors")
|
||||
body.join.should match /No errors have been recorded yet./
|
||||
end
|
||||
end
|
||||
|
||||
context "when handling an error" do
|
||||
let(:app) { Middleware.new(->env { raise exception }) }
|
||||
|
||||
it "returns status 500" do
|
||||
status, headers, body = app.call({})
|
||||
|
||||
status.should == 500
|
||||
end
|
||||
|
||||
context "original_exception" do
|
||||
class OriginalExceptionException < Exception
|
||||
attr_reader :original_exception
|
||||
|
||||
def initialize(message, original_exception = nil)
|
||||
super(message)
|
||||
@original_exception = original_exception
|
||||
end
|
||||
end
|
||||
|
||||
it "shows Original Exception if it responds_to and has an original_exception" do
|
||||
app = Middleware.new(->env {
|
||||
raise OriginalExceptionException.new("Other Exception", Exception.new("Original Exception"))
|
||||
})
|
||||
|
||||
status, _, body = app.call({})
|
||||
|
||||
status.should == 500
|
||||
body.join.should_not match(/Other Exception/)
|
||||
body.join.should match(/Original Exception/)
|
||||
end
|
||||
|
||||
it "won't crash if the exception responds_to but doesn't have an original_exception" do
|
||||
app = Middleware.new(->env {
|
||||
raise OriginalExceptionException.new("Other Exception")
|
||||
})
|
||||
|
||||
status, _, body = app.call({})
|
||||
|
||||
status.should == 500
|
||||
body.join.should match(/Other Exception/)
|
||||
end
|
||||
end
|
||||
|
||||
it "returns ExceptionWrapper's status_code" do
|
||||
ad_ew = double("ActionDispatch::ExceptionWrapper")
|
||||
ad_ew.stub('new').with({}, exception ){ double("ExceptionWrapper", status_code: 404) }
|
||||
stub_const('ActionDispatch::ExceptionWrapper', ad_ew)
|
||||
|
||||
status, headers, body = app.call({})
|
||||
|
||||
status.should == 404
|
||||
end
|
||||
|
||||
it "returns UTF-8 error pages" do
|
||||
status, headers, body = app.call({})
|
||||
|
||||
headers["Content-Type"].should match /charset=utf-8/
|
||||
end
|
||||
|
||||
it "returns text pages by default" do
|
||||
status, headers, body = app.call({})
|
||||
|
||||
headers["Content-Type"].should match /text\/plain/
|
||||
end
|
||||
|
||||
it "returns HTML pages by default" do
|
||||
# Chrome's 'Accept' header looks similar this.
|
||||
status, headers, body = app.call("HTTP_ACCEPT" => "text/html,application/xhtml+xml;q=0.9,*/*")
|
||||
|
||||
headers["Content-Type"].should match /text\/html/
|
||||
end
|
||||
|
||||
it "logs the exception" do
|
||||
logger = Object.new
|
||||
logger.should_receive :fatal
|
||||
BetterErrors.stub(:logger).and_return(logger)
|
||||
|
||||
app.call({})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,52 @@
|
|||
require "spec_helper"
|
||||
|
||||
module BetterErrors
|
||||
describe RaisedException do
|
||||
let(:exception) { RuntimeError.new("whoops") }
|
||||
subject { RaisedException.new(exception) }
|
||||
|
||||
its(:exception) { should == exception }
|
||||
its(:message) { should == "whoops" }
|
||||
its(:type) { should == RuntimeError }
|
||||
|
||||
context "when the exception wraps another exception" do
|
||||
let(:original_exception) { RuntimeError.new("something went wrong!") }
|
||||
let(:exception) { double(:original_exception => original_exception) }
|
||||
|
||||
its(:exception) { should == original_exception }
|
||||
its(:message) { should == "something went wrong!" }
|
||||
end
|
||||
|
||||
context "when the exception is a syntax error" do
|
||||
let(:exception) { SyntaxError.new("foo.rb:123: you made a typo!") }
|
||||
|
||||
its(:message) { should == "you made a typo!" }
|
||||
its(:type) { should == SyntaxError }
|
||||
|
||||
it "has the right filename and line number in the backtrace" do
|
||||
subject.backtrace.first.filename.should == "foo.rb"
|
||||
subject.backtrace.first.line.should == 123
|
||||
end
|
||||
end
|
||||
|
||||
context "when the exception is a HAML syntax error" do
|
||||
before do
|
||||
stub_const("Haml::SyntaxError", Class.new(SyntaxError))
|
||||
end
|
||||
|
||||
let(:exception) {
|
||||
Haml::SyntaxError.new("you made a typo!").tap do |ex|
|
||||
ex.set_backtrace(["foo.rb:123", "haml/internals/blah.rb:123456"])
|
||||
end
|
||||
}
|
||||
|
||||
its(:message) { should == "you made a typo!" }
|
||||
its(:type) { should == Haml::SyntaxError }
|
||||
|
||||
it "has the right filename and line number in the backtrace" do
|
||||
subject.backtrace.first.filename.should == "foo.rb"
|
||||
subject.backtrace.first.line.should == 123
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
require "spec_helper"
|
||||
require "better_errors/repl/basic"
|
||||
require "better_errors/repl/shared_examples"
|
||||
|
||||
module BetterErrors
|
||||
module REPL
|
||||
describe Basic do
|
||||
let(:fresh_binding) {
|
||||
local_a = 123
|
||||
binding
|
||||
}
|
||||
|
||||
let(:repl) { Basic.new fresh_binding }
|
||||
|
||||
it_behaves_like "a REPL provider"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
require "spec_helper"
|
||||
require "pry"
|
||||
require "better_errors/repl/pry"
|
||||
require "better_errors/repl/shared_examples"
|
||||
|
||||
module BetterErrors
|
||||
module REPL
|
||||
describe Pry do
|
||||
let(:fresh_binding) {
|
||||
local_a = 123
|
||||
binding
|
||||
}
|
||||
|
||||
let(:repl) { Pry.new fresh_binding }
|
||||
|
||||
it "does line continuation" do
|
||||
output, prompt, filled = repl.send_input ""
|
||||
output.should == "=> nil\n"
|
||||
prompt.should == ">>"
|
||||
filled.should == ""
|
||||
|
||||
output, prompt, filled = repl.send_input "def f(x)"
|
||||
output.should == ""
|
||||
prompt.should == ".."
|
||||
filled.should == " "
|
||||
|
||||
output, prompt, filled = repl.send_input "end"
|
||||
if RUBY_VERSION >= "2.1.0"
|
||||
output.should == "=> :f\n"
|
||||
else
|
||||
output.should == "=> nil\n"
|
||||
end
|
||||
prompt.should == ">>"
|
||||
filled.should == ""
|
||||
end
|
||||
|
||||
it_behaves_like "a REPL provider"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
shared_examples_for "a REPL provider" do
|
||||
it "evaluates ruby code in a given context" do
|
||||
repl.send_input("local_a = 456")
|
||||
fresh_binding.eval("local_a").should == 456
|
||||
end
|
||||
|
||||
it "returns a tuple of output and the new prompt" do
|
||||
output, prompt = repl.send_input("1 + 2")
|
||||
output.should == "=> 3\n"
|
||||
prompt.should == ">>"
|
||||
end
|
||||
|
||||
it "doesn't barf if the code throws an exception" do
|
||||
output, prompt = repl.send_input("raise Exception")
|
||||
output.should include "Exception: Exception"
|
||||
prompt.should == ">>"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,157 @@
|
|||
require "spec_helper"
|
||||
|
||||
module BetterErrors
|
||||
describe StackFrame do
|
||||
context "#application?" do
|
||||
it "is true for application filenames" do
|
||||
BetterErrors.stub(:application_root).and_return("/abc/xyz")
|
||||
frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
|
||||
|
||||
frame.application?.should be_true
|
||||
end
|
||||
|
||||
it "is false for everything else" do
|
||||
BetterErrors.stub(:application_root).and_return("/abc/xyz")
|
||||
frame = StackFrame.new("/abc/nope", 123, "foo")
|
||||
|
||||
frame.application?.should be_false
|
||||
end
|
||||
|
||||
it "doesn't care if no application_root is set" do
|
||||
frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
|
||||
|
||||
frame.application?.should be_false
|
||||
end
|
||||
end
|
||||
|
||||
context "#gem?" do
|
||||
it "is true for gem filenames" do
|
||||
Gem.stub(:path).and_return(["/abc/xyz"])
|
||||
frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo")
|
||||
|
||||
frame.gem?.should be_true
|
||||
end
|
||||
|
||||
it "is false for everything else" do
|
||||
Gem.stub(:path).and_return(["/abc/xyz"])
|
||||
frame = StackFrame.new("/abc/nope", 123, "foo")
|
||||
|
||||
frame.gem?.should be_false
|
||||
end
|
||||
end
|
||||
|
||||
context "#application_path" do
|
||||
it "chops off the application root" do
|
||||
BetterErrors.stub(:application_root).and_return("/abc/xyz")
|
||||
frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
|
||||
|
||||
frame.application_path.should == "app/controllers/crap_controller.rb"
|
||||
end
|
||||
end
|
||||
|
||||
context "#gem_path" do
|
||||
it "chops of the gem path and stick (gem) there" do
|
||||
Gem.stub(:path).and_return(["/abc/xyz"])
|
||||
frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo")
|
||||
|
||||
frame.gem_path.should == "whatever (1.2.3) lib/whatever.rb"
|
||||
end
|
||||
|
||||
it "prioritizes gem path over application path" do
|
||||
BetterErrors.stub(:application_root).and_return("/abc/xyz")
|
||||
Gem.stub(:path).and_return(["/abc/xyz/vendor"])
|
||||
frame = StackFrame.new("/abc/xyz/vendor/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo")
|
||||
|
||||
frame.gem_path.should == "whatever (1.2.3) lib/whatever.rb"
|
||||
end
|
||||
end
|
||||
|
||||
context "#pretty_path" do
|
||||
it "returns #application_path for application paths" do
|
||||
BetterErrors.stub(:application_root).and_return("/abc/xyz")
|
||||
frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index")
|
||||
frame.pretty_path.should == frame.application_path
|
||||
end
|
||||
|
||||
it "returns #gem_path for gem paths" do
|
||||
Gem.stub(:path).and_return(["/abc/xyz"])
|
||||
frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo")
|
||||
|
||||
frame.pretty_path.should == frame.gem_path
|
||||
end
|
||||
end
|
||||
|
||||
it "special cases SyntaxErrors" do
|
||||
begin
|
||||
eval(%{ raise SyntaxError, "you wrote bad ruby!" }, nil, "my_file.rb", 123)
|
||||
rescue SyntaxError => syntax_error
|
||||
end
|
||||
frames = StackFrame.from_exception(syntax_error)
|
||||
frames.first.filename.should == "my_file.rb"
|
||||
frames.first.line.should == 123
|
||||
end
|
||||
|
||||
it "doesn't blow up if no method name is given" do
|
||||
error = StandardError.allocate
|
||||
|
||||
error.stub(:backtrace).and_return(["foo.rb:123"])
|
||||
frames = StackFrame.from_exception(error)
|
||||
frames.first.filename.should == "foo.rb"
|
||||
frames.first.line.should == 123
|
||||
|
||||
error.stub(:backtrace).and_return(["foo.rb:123: this is an error message"])
|
||||
frames = StackFrame.from_exception(error)
|
||||
frames.first.filename.should == "foo.rb"
|
||||
frames.first.line.should == 123
|
||||
end
|
||||
|
||||
it "ignores a backtrace line if its format doesn't make any sense at all" do
|
||||
error = StandardError.allocate
|
||||
error.stub(:backtrace).and_return(["foo.rb:123:in `foo'", "C:in `find'", "bar.rb:123:in `bar'"])
|
||||
frames = StackFrame.from_exception(error)
|
||||
frames.count.should == 2
|
||||
end
|
||||
|
||||
it "doesn't blow up if a filename contains a colon" do
|
||||
error = StandardError.allocate
|
||||
error.stub(:backtrace).and_return(["crap:filename.rb:123"])
|
||||
frames = StackFrame.from_exception(error)
|
||||
frames.first.filename.should == "crap:filename.rb"
|
||||
end
|
||||
|
||||
it "doesn't blow up with a BasicObject as frame binding" do
|
||||
obj = BasicObject.new
|
||||
def obj.my_binding
|
||||
::Kernel.binding
|
||||
end
|
||||
frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index", obj.my_binding)
|
||||
frame.class_name.should == 'BasicObject'
|
||||
end
|
||||
|
||||
it "sets method names properly" do
|
||||
obj = "string"
|
||||
def obj.my_method
|
||||
begin
|
||||
raise "foo"
|
||||
rescue => err
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
frame = StackFrame.from_exception(obj.my_method).first
|
||||
if BetterErrors.binding_of_caller_available?
|
||||
frame.method_name.should == "#my_method"
|
||||
frame.class_name.should == "String"
|
||||
else
|
||||
frame.method_name.should == "my_method"
|
||||
frame.class_name.should == nil
|
||||
end
|
||||
end
|
||||
|
||||
if RUBY_ENGINE == "java"
|
||||
it "doesn't blow up on a native Java exception" do
|
||||
expect { StackFrame.from_exception(java.lang.Exception.new) }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
eleven
|
||||
twelve
|
||||
thirteen
|
||||
fourteen
|
||||
fifteen
|
||||
sixteen
|
||||
seventeen
|
||||
eighteen
|
||||
nineteen
|
||||
twenty
|
|
@ -0,0 +1,73 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe BetterErrors do
|
||||
context ".editor" do
|
||||
it "defaults to textmate" do
|
||||
subject.editor["foo.rb", 123].should == "txmt://open?url=file://foo.rb&line=123"
|
||||
end
|
||||
|
||||
it "url escapes the filename" do
|
||||
subject.editor["&.rb", 0].should == "txmt://open?url=file://%26.rb&line=0"
|
||||
end
|
||||
|
||||
[:emacs, :emacsclient].each do |editor|
|
||||
it "uses emacs:// scheme when set to #{editor.inspect}" do
|
||||
subject.editor = editor
|
||||
subject.editor[].should start_with "emacs://"
|
||||
end
|
||||
end
|
||||
|
||||
[:macvim, :mvim].each do |editor|
|
||||
it "uses mvim:// scheme when set to #{editor.inspect}" do
|
||||
subject.editor = editor
|
||||
subject.editor[].should start_with "mvim://"
|
||||
end
|
||||
end
|
||||
|
||||
[:sublime, :subl, :st].each do |editor|
|
||||
it "uses subl:// scheme when set to #{editor.inspect}" do
|
||||
subject.editor = editor
|
||||
subject.editor[].should start_with "subl://"
|
||||
end
|
||||
end
|
||||
|
||||
[:textmate, :txmt, :tm].each do |editor|
|
||||
it "uses txmt:// scheme when set to #{editor.inspect}" do
|
||||
subject.editor = editor
|
||||
subject.editor[].should start_with "txmt://"
|
||||
end
|
||||
end
|
||||
|
||||
["emacsclient", "/usr/local/bin/emacsclient"].each do |editor|
|
||||
it "uses emacs:// scheme when EDITOR=#{editor}" do
|
||||
ENV["EDITOR"] = editor
|
||||
subject.editor = subject.default_editor
|
||||
subject.editor[].should start_with "emacs://"
|
||||
end
|
||||
end
|
||||
|
||||
["mvim -f", "/usr/local/bin/mvim -f"].each do |editor|
|
||||
it "uses mvim:// scheme when EDITOR=#{editor}" do
|
||||
ENV["EDITOR"] = editor
|
||||
subject.editor = subject.default_editor
|
||||
subject.editor[].should start_with "mvim://"
|
||||
end
|
||||
end
|
||||
|
||||
["subl -w", "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl"].each do |editor|
|
||||
it "uses mvim:// scheme when EDITOR=#{editor}" do
|
||||
ENV["EDITOR"] = editor
|
||||
subject.editor = subject.default_editor
|
||||
subject.editor[].should start_with "subl://"
|
||||
end
|
||||
end
|
||||
|
||||
["mate -w", "/usr/bin/mate -w"].each do |editor|
|
||||
it "uses txmt:// scheme when EDITOR=#{editor}" do
|
||||
ENV["EDITOR"] = editor
|
||||
subject.editor = subject.default_editor
|
||||
subject.editor[].should start_with "txmt://"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
$: << File.expand_path("../../lib", __FILE__)
|
||||
|
||||
ENV["EDITOR"] = nil
|
||||
|
||||
require "better_errors"
|
|
@ -0,0 +1,9 @@
|
|||
module Kernel
|
||||
alias_method :require_with_binding_of_caller, :require
|
||||
|
||||
def require(feature)
|
||||
raise LoadError if feature == "binding_of_caller"
|
||||
|
||||
require_with_binding_of_caller(feature)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
language: ruby
|
||||
rvm:
|
||||
- 1.9.3
|
||||
- 2.0.0
|
||||
- 2.1.1
|
||||
bundler_args: ""
|
||||
services:
|
||||
- redis
|
||||
- memcached
|
|
@ -0,0 +1,181 @@
|
|||
28-June-2012 - Sam
|
||||
|
||||
* Started change log
|
||||
* Corrected profiler so it properly captures POST requests (was supressing non 200s)
|
||||
* Amended Rack.MiniProfiler.config[:user_provider] to use ip addres for identity
|
||||
* Fixed bug where unviewed missing ids never got cleared
|
||||
* Supress all '/assets/' in the rails tie (makes debugging easier)
|
||||
* record_sql was mega buggy
|
||||
* added MemcacheStore
|
||||
|
||||
9-July-2012 - Sam
|
||||
|
||||
* Cleaned up mechanism for profiling in production, all you need to do now
|
||||
is call Rack::MiniProfiler.authorize_request to get profiling working in
|
||||
production
|
||||
* Added option to display full backtraces pp=full-backtrace
|
||||
* Cleaned up railties, got rid of the post authorize callback
|
||||
* Version 0.1.3
|
||||
|
||||
12-July-2012 - Sam
|
||||
|
||||
* Fixed incorrect profiling steps (was not indenting or measuring start time right
|
||||
* Implemented native PG and MySql2 interceptors, this gives way more accurate times
|
||||
* Refactored context so its a proper class and not a hash
|
||||
* Added some more client probing built in to rails
|
||||
* More tests
|
||||
|
||||
18-July-2012 - Sam
|
||||
|
||||
* Added First Paint time for chrome
|
||||
* Bug fix to ensure non Rails installs have mini profiler
|
||||
* Version 0.1.7
|
||||
|
||||
30-July-2012 - Sam
|
||||
|
||||
* Made compliant with ancient versions of Rack (including Rack used by Rails2)
|
||||
* Fixed broken share link
|
||||
* Fixed crashes on startup (in MemoryStore and FileStore)
|
||||
* Version 0.1.8
|
||||
* Unicode fix
|
||||
* Version 0.1.9
|
||||
|
||||
7-August-2012 - Sam
|
||||
|
||||
* Added option to disable profiler for the current session (pp=disable / pp=enable)
|
||||
* yajl compatability contributed by Sven Riedel
|
||||
|
||||
10-August-2012 - Sam
|
||||
|
||||
* Added basic prepared statement profiling for postgres
|
||||
|
||||
20-August-2012 - Sam
|
||||
|
||||
* 1.12.pre
|
||||
* Cap X-MiniProfiler-Ids at 10, otherwise the header can get killed
|
||||
|
||||
3-September-2012 - Sam
|
||||
|
||||
* 1.13.pre
|
||||
* pg gem prepared statements were not being logged correctly
|
||||
* added setting config.backtrace_ignores = [] - an array of regexes that match on caller lines that get ignored
|
||||
* added setting config.backtrace_includes = [] - an array of regexes that get included in the trace by default
|
||||
* cleaned up the way client settings are stored
|
||||
* made pp=full-backtrace "sticky"
|
||||
* added pp=normal-backtrace to clear the "sticky" state
|
||||
* change "pp=sample" to work with "caller" no need for stack trace gem
|
||||
|
||||
4-September-2012 - Sam
|
||||
|
||||
* 1.15.pre
|
||||
* fixed annoying bug where client settings were not sticking
|
||||
* fixed long standing issue with Rack::ConditionalGet stopping MiniProfiler from working properly
|
||||
|
||||
5-September-2012 - Sam
|
||||
|
||||
* 1.16
|
||||
* fixed long standing problem specs (issue with memory store)
|
||||
* fixed issue where profiler would be dumped when you got a 404 in production (and any time rails is bypassed)
|
||||
* implemented stacktrace properly
|
||||
|
||||
9-September-2012 - Sam
|
||||
|
||||
* 1.17
|
||||
* pp=sample was bust unless stacktrace was installed
|
||||
|
||||
10-September-2012 - Sam
|
||||
|
||||
* 1.19
|
||||
* fix compat issue with 1.8.7
|
||||
|
||||
12-September-2012 - Sam
|
||||
|
||||
* 1.20
|
||||
* Added pp=profile-gc , it allows you to profile the GC in Ruby 1.9.3
|
||||
|
||||
17-September-2012
|
||||
* 1.21
|
||||
* New MemchacedStore
|
||||
* Rails 4 support
|
||||
|
||||
17-September-2012
|
||||
* Allow rack-mini-profiler to be sourced from github
|
||||
* Extracted the pp=profile-gc-time out, the object space profiler needs to disable gc
|
||||
|
||||
20-September-2012
|
||||
* 1.22
|
||||
* Fix permission issue in the gem
|
||||
|
||||
8-April-2013
|
||||
* 1.24
|
||||
* Flame Graph Support see: http://samsaffron.com/archive/2013/03/19/flame-graphs-in-ruby-miniprofiler
|
||||
* Fix file retention leak in file_store
|
||||
* New toggle_shortcut and start_hidden options
|
||||
* Fix for AngularJS support and MooTools
|
||||
* More robust gc profiling
|
||||
* Mongoid support
|
||||
* Fix for html5 implicit body tags
|
||||
* script tag initialized via data-attributes
|
||||
* new - Rack::MiniProfiler.counter counter_name {}
|
||||
* Allow usage of existing jQuery if its already loaded
|
||||
* Fix pp=enable
|
||||
* 1.8.7 support ... grrr
|
||||
* Net:HTTP profiling
|
||||
* pre authorize to run in all non development? and production? modes
|
||||
|
||||
8-April-2013
|
||||
* 1.25
|
||||
* Missed flamegraph.html from build
|
||||
|
||||
11-April-2013
|
||||
* 1.26
|
||||
* (minor) allow Rack::MiniProfilerRails.initialize!(Rails.application), for post config intialization
|
||||
|
||||
26-June-2013
|
||||
* 1.27
|
||||
* Disable global ajax handlers on MP requests @JP
|
||||
* Add Rack::MiniProfiler.config.backtrace_threshold_ms
|
||||
* jQuery 2.0 support
|
||||
|
||||
18-July-2013
|
||||
* 1.28
|
||||
* diagnostics in abstract storage was raising not implemented killing
|
||||
?pp=env and others
|
||||
* SOLR xml unescaped by mistake
|
||||
|
||||
20-August-2013
|
||||
* 1.29
|
||||
* Bugfix: SOLR patching had an incorrect monkey patch
|
||||
* Implemented exception tracing using TracePoint see pp=trace-exceptions
|
||||
|
||||
30-August-2013
|
||||
|
||||
* 1.30
|
||||
* Feature: Added Rack::MiniProfiler.counter_method(klass,name) for injecting counters
|
||||
* Bug: Counters were not shifting the table correctly
|
||||
|
||||
3-September-2013
|
||||
|
||||
* Ripped out flamegraph so it can be isolated into a gem
|
||||
* Flamegraph now has much increased fidelity
|
||||
* Ripped out pp=sample it just was never really used
|
||||
|
||||
17-September-2013 - Ross Wilson
|
||||
* Instead of supressing all "/assets/" requests we now check the configured
|
||||
config.assets.prefix path since developers can rename the path to serve Asset Pipeline
|
||||
files from
|
||||
|
||||
12-December-2013 - Sam Saffron
|
||||
* Version 0.9.0.pre (bumped up to reflect the stability of the project)
|
||||
* Improved reports for pp=profile-gc
|
||||
* pp=flamegraph&flamegraph_sample_rate=1 , allow you to specify sampling rates
|
||||
|
||||
13-March-2014 - Sam Saffron
|
||||
* Version 0.9.1
|
||||
* Added back Ruby 1.8 support (thanks Malet)
|
||||
* Corrected Rails 3.0 support (thanks Zlatko)
|
||||
* Corrected fix possible XSS (admin only)
|
||||
* Amend Railstie so MiniProfiler can be launched with action view or action controller (Thanks Akira)
|
||||
* Corrected Sql patching to avoid setting instance vars on nil which is frozen (thanks Andy, huoxito)
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
source 'http://rubygems.org'
|
||||
|
||||
gemspec
|
|
@ -0,0 +1,271 @@
|
|||
# rack-mini-profiler
|
||||
|
||||
[![Code Climate](https://codeclimate.com/github/MiniProfiler/rack-mini-profiler.png)](https://codeclimate.com/github/MiniProfiler/rack-mini-profiler) [![Build Status](https://travis-ci.org/MiniProfiler/rack-mini-profiler.png)](https://travis-ci.org/MiniProfiler/rack-mini-profiler)
|
||||
|
||||
Middleware that displays speed badge for every html page. Designed to work both in production and in development.
|
||||
|
||||
#### Features
|
||||
|
||||
* database profiling. Currently supports Mysql2, Postgres, and Mongoid3 (with fallback support to ActiveRecord)
|
||||
|
||||
#### Learn more
|
||||
|
||||
* [Visit our community](http://community.miniprofiler.com)
|
||||
* [Watch the RailsCast](http://railscasts.com/episodes/368-miniprofiler)
|
||||
* [Read about Flame graphs in rack-mini-profiler](http://samsaffron.com/archive/2013/03/19/flame-graphs-in-ruby-miniprofiler)
|
||||
* [Read the announcement posts from 2012](http://samsaffron.com/archive/2012/07/12/miniprofiler-ruby-edition)
|
||||
|
||||
## rack-mini-profiler needs your help
|
||||
|
||||
We have decided to restructure our repository so there is a central UI repo and the various language implementation have their own.
|
||||
|
||||
**WE NEED HELP.**
|
||||
|
||||
- Setting up a build that reuses https://github.com/MiniProfiler/ui
|
||||
- Migrating the internal data structures [per the spec](https://github.com/MiniProfiler/ui)
|
||||
- Cleaning up the [horrendous class structure that is using strings as keys and crazy non-objects](https://github.com/MiniProfiler/rack-mini-profiler/blob/master/lib/mini_profiler/sql_timer_struct.rb#L36-L44)
|
||||
|
||||
If you feel like taking on any of this start an issue and update us on your progress.
|
||||
|
||||
## Installation
|
||||
|
||||
Install/add to Gemfile
|
||||
|
||||
```ruby
|
||||
gem 'rack-mini-profiler'
|
||||
```
|
||||
|
||||
NOTE: Be sure to require rack_mini_profiler below the `pg` and `mysql` gems in your Gemfile. rack_mini_profiler will identify these gems if they are loaded to insert instrumentation. If included too early no SQL will show up.
|
||||
|
||||
#### Rails
|
||||
|
||||
All you have to do is include the Gem and you're good to go in development. See notes below for use in production.
|
||||
|
||||
#### Rails and manual initialization
|
||||
|
||||
In case you need to make sure rack_mini_profiler initialized after all other gems.
|
||||
Or you want to execute some code before rack_mini_profiler required.
|
||||
|
||||
```ruby
|
||||
gem 'rack-mini-profiler', require: false
|
||||
```
|
||||
Note the `require: false` part - if omitted, it will cause the Railtie for the mino-profiler to
|
||||
be loaded outright, and an attempt to re-initialize it manually will raise an exception.
|
||||
|
||||
Then put initialize code in file like `config/initializers/rack_profiler.rb`
|
||||
|
||||
```ruby
|
||||
if Rails.env == 'development'
|
||||
require 'rack-mini-profiler'
|
||||
|
||||
# initialization is skipped so trigger it
|
||||
Rack::MiniProfilerRails.initialize!(Rails.application)
|
||||
end
|
||||
```
|
||||
|
||||
#### Rack Builder
|
||||
|
||||
```ruby
|
||||
require 'rack-mini-profiler'
|
||||
builder = Rack::Builder.new do
|
||||
use Rack::MiniProfiler
|
||||
|
||||
map('/') { run get }
|
||||
end
|
||||
```
|
||||
|
||||
#### Sinatra
|
||||
|
||||
```ruby
|
||||
require 'rack-mini-profiler'
|
||||
class MyApp < Sinatra::Base
|
||||
use Rack::MiniProfiler
|
||||
end
|
||||
```
|
||||
|
||||
### Flamegraphs
|
||||
|
||||
To generate [flamegraphs](http://samsaffron.com/archive/2013/03/19/flame-graphs-in-ruby-miniprofiler):
|
||||
|
||||
* add the **flamegraph** gem to your Gemfile
|
||||
* visit a page in your app with `?pp=flamegraph`
|
||||
|
||||
Flamegraph generation is supported in MRI 2.0 and 2.1 only.
|
||||
|
||||
|
||||
## Access control in production
|
||||
|
||||
rack-mini-profiler is designed with production profiling in mind. To enable that just run `Rack::MiniProfiler.authorize_request` once you know a request is allowed to profile.
|
||||
|
||||
```ruby
|
||||
# A hook in your ApplicationController
|
||||
def authorize
|
||||
if current_user.is_admin?
|
||||
Rack::MiniProfiler.authorize_request
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Various aspects of rack-mini-profiler's behavior can be configured when your app boots.
|
||||
For example in a Rails app, this should be done in an initializer:
|
||||
**config/initializers/mini_profiler.rb**
|
||||
|
||||
### Storage
|
||||
|
||||
rack-mini-profiler stores its results so they can be shared later and aren't lost at the end of the request.
|
||||
|
||||
There are 4 storage options: `MemoryStore`, `RedisStore`, `MemcacheStore`, and `FileStore`.
|
||||
|
||||
`FileStore` is the default in Rails environments and will write files to `tmp/miniprofiler/*`. `MemoryStore` is the default otherwise.
|
||||
|
||||
```ruby
|
||||
# set MemoryStore
|
||||
Rack::MiniProfiler.config.storage = Rack::MiniProfiler::MemoryStore
|
||||
|
||||
# set RedisStore
|
||||
if Rails.env.production?
|
||||
uri = URI.parse(ENV["REDIS_SERVER_URL"])
|
||||
Rack::MiniProfiler.config.storage_options = { :host => uri.host, :port => uri.port, :password => uri.password }
|
||||
Rack::MiniProfiler.config.storage = Rack::MiniProfiler::RedisStore
|
||||
end
|
||||
```
|
||||
|
||||
MemoryStore stores results in a processes heap - something that does not work well in a multi process environment.
|
||||
FileStore stores results in the file system - something that may not work well in a multi machine environment.
|
||||
RedisStore/MemcacheStore work in multi process and multi machine environments (RedisStore only saves results for up to 24 hours so it won't continue to fill up Redis).
|
||||
|
||||
Additionally you may implement an AbstractStore for your own provider.
|
||||
|
||||
### User result segregation
|
||||
|
||||
MiniProfiler will attempt to keep all user results isolated, out-of-the-box the user provider uses the ip address:
|
||||
|
||||
```ruby
|
||||
Rack::MiniProfiler.config.user_provider = Proc.new{|env| Rack::Request.new(env).ip}
|
||||
```
|
||||
|
||||
You can override (something that is very important in a multi-machine production setup):
|
||||
|
||||
```ruby
|
||||
Rack::MiniProfiler.config.user_provider = Proc.new{ |env| CurrentUser.get(env) }
|
||||
```
|
||||
|
||||
The string this function returns should be unique for each user on the system (for anonymous you may need to fall back to ip address)
|
||||
|
||||
### Configuration Options
|
||||
|
||||
You can set configuration options using the configuration accessor on `Rack::MiniProfiler`.
|
||||
For example:
|
||||
|
||||
```ruby
|
||||
Rack::MiniProfiler.config.position = 'right'
|
||||
Rack::MiniProfiler.config.start_hidden = true
|
||||
```
|
||||
The available configuration options are:
|
||||
|
||||
* pre_authorize_cb - A lambda callback you can set to determine whether or not mini_profiler should be visible on a given request. Default in a Rails environment is only on in development mode. If in a Rack app, the default is always on.
|
||||
* position - Can either be 'right' or 'left'. Default is 'left'.
|
||||
* skip_schema_queries - Whether or not you want to log the queries about the schema of your tables. Default is 'false', 'true' in rails development.
|
||||
* auto_inject (default true) - when false the miniprofiler script is not injected in the page
|
||||
* backtrace_filter - a regex you can use to filter out unwanted lines from the backtraces
|
||||
* toggle_shortcut (default Alt+P) - a jquery.hotkeys.js-style keyboard shortcut, used to toggle the mini_profiler's visibility. See http://code.google.com/p/js-hotkeys/ for more info.
|
||||
* start_hidden (default false) - Whether or not you want the mini_profiler to be visible when loading a page
|
||||
* backtrace_threshold_ms (default zero) - Minimum SQL query elapsed time before a backtrace is recorded. Backtrace recording can take a couple of milliseconds on rubies earlier than 2.0, impacting performance for very small queries.
|
||||
* flamegraph_sample_rate (default 0.5ms) - How often fast_stack should get stack trace info to generate flamegraphs
|
||||
|
||||
### Custom middleware ordering (required if using `Rack::Deflate` with Rails)
|
||||
|
||||
If you are using `Rack::Deflate` with rails and rack-mini-profiler in its default configuration,
|
||||
`Rack::MiniProfiler` will be injected (as always) at position 0 in the middleware stack. This
|
||||
will result in it attempting to inject html into the already-compressed response body. To fix this,
|
||||
the middleware ordering must be overriden.
|
||||
|
||||
To do this, first add `, require: false` to the gemfile entry for rack-mini-profiler.
|
||||
This will prevent the railtie from running. Then, customize the initialization
|
||||
in the initializer like so:
|
||||
|
||||
```ruby
|
||||
require 'rack-mini-profiler'
|
||||
|
||||
Rack::MiniProfilerRails.initialize!(Rails.application)
|
||||
|
||||
Rails.application.middleware.delete(Rack::MiniProfiler)
|
||||
Rails.application.middleware.insert_after(Rack::Deflater, Rack::MiniProfiler)
|
||||
```
|
||||
|
||||
Deleting the middleware and then reinserting it is a bit inelegant, but
|
||||
a sufficient and costless solution. It is possible that rack-mini-profiler might
|
||||
support this scenario more directly if it is found that
|
||||
there is significant need for this confriguration or that
|
||||
the above recipe causes problems.
|
||||
|
||||
|
||||
## Special query strings
|
||||
|
||||
If you include the query string `pp=help` at the end of your request you will see the various options available. You can use these options to extend or contract the amount of diagnostics rack-mini-profiler gathers.
|
||||
|
||||
|
||||
## Rails 2.X support
|
||||
|
||||
To get MiniProfiler working with Rails 2.3.X you need to do the initialization manually as well as monkey patch away an incompatibility between activesupport and json_pure.
|
||||
|
||||
Add the following code to your environment.rb (or just in a specific environment such as development.rb) for initialization and configuration of MiniProfiler.
|
||||
|
||||
```ruby
|
||||
# configure and initialize MiniProfiler
|
||||
require 'rack-mini-profiler'
|
||||
c = ::Rack::MiniProfiler.config
|
||||
c.pre_authorize_cb = lambda { |env|
|
||||
Rails.env.development? || Rails.env.production?
|
||||
}
|
||||
tmp = Rails.root.to_s + "/tmp/miniprofiler"
|
||||
FileUtils.mkdir_p(tmp) unless File.exists?(tmp)
|
||||
c.storage_options = {:path => tmp}
|
||||
c.storage = ::Rack::MiniProfiler::FileStore
|
||||
config.middleware.use(::Rack::MiniProfiler)
|
||||
::Rack::MiniProfiler.profile_method(ActionController::Base, :process) {|action| "Executing action: #{action}"}
|
||||
::Rack::MiniProfiler.profile_method(ActionView::Template, :render) {|x,y| "Rendering: #{path_without_format_and_extension}"}
|
||||
|
||||
# monkey patch away an activesupport and json_pure incompatability
|
||||
# http://pivotallabs.com/users/alex/blog/articles/1332-monkey-patch-of-the-day-activesupport-vs-json-pure-vs-ruby-1-8
|
||||
if JSON.const_defined?(:Pure)
|
||||
class JSON::Pure::Generator::State
|
||||
include ActiveSupport::CoreExtensions::Hash::Except
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Running the Specs
|
||||
|
||||
```
|
||||
$ rake build
|
||||
$ rake spec
|
||||
```
|
||||
|
||||
Additionally you can also run `autotest` if you like.
|
||||
|
||||
## Licence
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Sam Saffron
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
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,46 @@
|
|||
# Rakefile
|
||||
require 'rubygems'
|
||||
require 'bundler'
|
||||
Bundler.setup(:default, :test)
|
||||
|
||||
task :default => [:spec]
|
||||
|
||||
require 'rspec/core'
|
||||
require 'rspec/core/rake_task'
|
||||
RSpec::Core::RakeTask.new(:spec) do |spec|
|
||||
spec.pattern = FileList['spec/**/*_spec.rb']
|
||||
end
|
||||
|
||||
desc "builds a gem"
|
||||
task :build => :update_asset_version do
|
||||
`gem build rack-mini-profiler.gemspec 1>&2`
|
||||
end
|
||||
|
||||
desc "compile less"
|
||||
task :compile_less => :copy_files do
|
||||
`lessc lib/html/includes.less > lib/html/includes.css`
|
||||
end
|
||||
|
||||
desc "update asset version file"
|
||||
task :update_asset_version => :compile_less do
|
||||
require 'digest/md5'
|
||||
h = []
|
||||
Dir.glob('lib/html/*.{js,html,css,tmpl}').each do |f|
|
||||
h << Digest::MD5.hexdigest(::File.read(f))
|
||||
end
|
||||
File.open('lib/mini_profiler/version.rb','w') do |f|
|
||||
f.write \
|
||||
"module Rack
|
||||
class MiniProfiler
|
||||
VERSION = '#{Digest::MD5.hexdigest(h.sort.join(''))}'.freeze
|
||||
end
|
||||
end"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
desc "copy files from other parts of the tree"
|
||||
task :copy_files do
|
||||
# TODO grab files from MiniProfiler/UI
|
||||
end
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
Autotest.add_discovery { "rspec2" }
|
||||
|
|
@ -0,0 +1,451 @@
|
|||
.profiler-result,
|
||||
.profiler-queries {
|
||||
color: #555;
|
||||
line-height: 1;
|
||||
font-size: 12px;
|
||||
}
|
||||
.profiler-result pre,
|
||||
.profiler-queries pre,
|
||||
.profiler-result code,
|
||||
.profiler-queries code,
|
||||
.profiler-result label,
|
||||
.profiler-queries label,
|
||||
.profiler-result table,
|
||||
.profiler-queries table,
|
||||
.profiler-result tbody,
|
||||
.profiler-queries tbody,
|
||||
.profiler-result thead,
|
||||
.profiler-queries thead,
|
||||
.profiler-result tfoot,
|
||||
.profiler-queries tfoot,
|
||||
.profiler-result tr,
|
||||
.profiler-queries tr,
|
||||
.profiler-result th,
|
||||
.profiler-queries th,
|
||||
.profiler-result td,
|
||||
.profiler-queries td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
max-height: none;
|
||||
}
|
||||
.profiler-result table,
|
||||
.profiler-queries table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
.profiler-result a,
|
||||
.profiler-queries a,
|
||||
.profiler-result a:hover,
|
||||
.profiler-queries a:hover {
|
||||
cursor: pointer;
|
||||
color: #0077cc;
|
||||
}
|
||||
.profiler-result a,
|
||||
.profiler-queries a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.profiler-result a:hover,
|
||||
.profiler-queries a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.profiler-result {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
.profiler-result .profiler-toggle-duration-with-children {
|
||||
float: right;
|
||||
}
|
||||
.profiler-result table.profiler-client-timings {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.profiler-result .profiler-label {
|
||||
color: #555555;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.profiler-result .profiler-unit {
|
||||
color: #aaaaaa;
|
||||
}
|
||||
.profiler-result .profiler-trivial {
|
||||
display: none;
|
||||
}
|
||||
.profiler-result .profiler-trivial td,
|
||||
.profiler-result .profiler-trivial td * {
|
||||
color: #aaaaaa !important;
|
||||
}
|
||||
.profiler-result pre,
|
||||
.profiler-result code,
|
||||
.profiler-result .profiler-number,
|
||||
.profiler-result .profiler-unit {
|
||||
font-family: Consolas, monospace, serif;
|
||||
}
|
||||
.profiler-result .profiler-number {
|
||||
color: #111111;
|
||||
}
|
||||
.profiler-result .profiler-info {
|
||||
text-align: right;
|
||||
}
|
||||
.profiler-result .profiler-info .profiler-name {
|
||||
float: left;
|
||||
}
|
||||
.profiler-result .profiler-info .profiler-server-time {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.profiler-result .profiler-timings th {
|
||||
background-color: #fff;
|
||||
color: #aaaaaa;
|
||||
text-align: right;
|
||||
}
|
||||
.profiler-result .profiler-timings th,
|
||||
.profiler-result .profiler-timings td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.profiler-result .profiler-timings .profiler-duration-with-children {
|
||||
display: none;
|
||||
}
|
||||
.profiler-result .profiler-timings .profiler-duration {
|
||||
font-family: Consolas, monospace, serif;
|
||||
color: #111111;
|
||||
text-align: right;
|
||||
}
|
||||
.profiler-result .profiler-timings .profiler-indent {
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
.profiler-result .profiler-timings .profiler-queries-show .profiler-number,
|
||||
.profiler-result .profiler-timings .profiler-queries-show .profiler-unit {
|
||||
color: #0077cc;
|
||||
}
|
||||
.profiler-result .profiler-timings .profiler-queries-duration {
|
||||
padding-left: 6px;
|
||||
}
|
||||
.profiler-result .profiler-timings .profiler-percent-in-sql {
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
.profiler-result .profiler-timings tfoot td {
|
||||
padding-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
.profiler-result .profiler-timings tfoot td a {
|
||||
font-size: 95%;
|
||||
display: inline-block;
|
||||
margin-left: 12px;
|
||||
}
|
||||
.profiler-result .profiler-timings tfoot td a:first-child {
|
||||
float: left;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.profiler-result .profiler-timings tfoot td a.profiler-custom-link {
|
||||
float: left;
|
||||
}
|
||||
.profiler-result .profiler-queries {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
.profiler-result .profiler-queries .profiler-stack-trace {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.profiler-result .profiler-queries pre {
|
||||
font-family: Consolas, monospace, serif;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.profiler-result .profiler-queries th {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #555;
|
||||
font-weight: bold;
|
||||
padding: 15px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.profiler-result .profiler-queries td {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
}
|
||||
.profiler-result .profiler-queries td:last-child {
|
||||
padding-right: 25px;
|
||||
}
|
||||
.profiler-result .profiler-queries .profiler-odd td {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
.profiler-result .profiler-queries .profiler-since-start,
|
||||
.profiler-result .profiler-queries .profiler-duration {
|
||||
text-align: right;
|
||||
}
|
||||
.profiler-result .profiler-queries .profiler-info div {
|
||||
text-align: right;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.profiler-result .profiler-queries .profiler-gap-info,
|
||||
.profiler-result .profiler-queries .profiler-gap-info td {
|
||||
background-color: #ccc;
|
||||
}
|
||||
.profiler-result .profiler-queries .profiler-gap-info .profiler-unit {
|
||||
color: #777;
|
||||
}
|
||||
.profiler-result .profiler-queries .profiler-gap-info .profiler-info {
|
||||
text-align: right;
|
||||
}
|
||||
.profiler-result .profiler-queries .profiler-gap-info.profiler-trivial-gaps {
|
||||
display: none;
|
||||
}
|
||||
.profiler-result .profiler-queries .profiler-trivial-gap-container {
|
||||
text-align: center;
|
||||
}
|
||||
.profiler-result .profiler-queries .str {
|
||||
color: #800000;
|
||||
}
|
||||
.profiler-result .profiler-queries .kwd {
|
||||
color: #00008b;
|
||||
}
|
||||
.profiler-result .profiler-queries .com {
|
||||
color: #808080;
|
||||
}
|
||||
.profiler-result .profiler-queries .typ {
|
||||
color: #2b91af;
|
||||
}
|
||||
.profiler-result .profiler-queries .lit {
|
||||
color: #800000;
|
||||
}
|
||||
.profiler-result .profiler-queries .pun {
|
||||
color: #000000;
|
||||
}
|
||||
.profiler-result .profiler-queries .pln {
|
||||
color: #000000;
|
||||
}
|
||||
.profiler-result .profiler-queries .tag {
|
||||
color: #800000;
|
||||
}
|
||||
.profiler-result .profiler-queries .atn {
|
||||
color: #ff0000;
|
||||
}
|
||||
.profiler-result .profiler-queries .atv {
|
||||
color: #0000ff;
|
||||
}
|
||||
.profiler-result .profiler-queries .dec {
|
||||
color: #800080;
|
||||
}
|
||||
.profiler-result .profiler-warning,
|
||||
.profiler-result .profiler-warning *,
|
||||
.profiler-result .profiler-warning .profiler-queries-show,
|
||||
.profiler-result .profiler-warning .profiler-queries-show .profiler-unit {
|
||||
color: #f00;
|
||||
}
|
||||
.profiler-result .profiler-warning:hover,
|
||||
.profiler-result .profiler-warning *:hover,
|
||||
.profiler-result .profiler-warning .profiler-queries-show:hover,
|
||||
.profiler-result .profiler-warning .profiler-queries-show .profiler-unit:hover {
|
||||
color: #f00;
|
||||
}
|
||||
.profiler-result .profiler-nuclear {
|
||||
color: #f00;
|
||||
font-weight: bold;
|
||||
padding-right: 2px;
|
||||
}
|
||||
.profiler-result .profiler-nuclear:hover {
|
||||
color: #f00;
|
||||
}
|
||||
.profiler-results {
|
||||
z-index: 2147483643;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
}
|
||||
.profiler-results.profiler-left {
|
||||
left: 0px;
|
||||
}
|
||||
.profiler-results.profiler-left.profiler-no-controls .profiler-result:last-child .profiler-button,
|
||||
.profiler-results.profiler-left .profiler-controls {
|
||||
-webkit-border-bottom-right-radius: 10px;
|
||||
-moz-border-radius-bottomright: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
.profiler-results.profiler-left .profiler-button,
|
||||
.profiler-results.profiler-left .profiler-controls {
|
||||
border-right: 1px solid #888888;
|
||||
}
|
||||
.profiler-results.profiler-right {
|
||||
right: 0px;
|
||||
}
|
||||
.profiler-results.profiler-right.profiler-no-controls .profiler-result:last-child .profiler-button,
|
||||
.profiler-results.profiler-right .profiler-controls {
|
||||
-webkit-border-bottom-left-radius: 10px;
|
||||
-moz-border-radius-bottomleft: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
.profiler-results.profiler-right .profiler-button,
|
||||
.profiler-results.profiler-right .profiler-controls {
|
||||
border-left: 1px solid #888888;
|
||||
}
|
||||
.profiler-results .profiler-button,
|
||||
.profiler-results .profiler-controls {
|
||||
display: none;
|
||||
z-index: 2147483640;
|
||||
border-bottom: 1px solid #888888;
|
||||
background-color: #fff;
|
||||
padding: 4px 7px;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
.profiler-results .profiler-button.profiler-button-active,
|
||||
.profiler-results .profiler-controls.profiler-button-active {
|
||||
background-color: maroon;
|
||||
}
|
||||
.profiler-results .profiler-button.profiler-button-active .profiler-number,
|
||||
.profiler-results .profiler-controls.profiler-button-active .profiler-number,
|
||||
.profiler-results .profiler-button.profiler-button-active .profiler-nuclear,
|
||||
.profiler-results .profiler-controls.profiler-button-active .profiler-nuclear {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
.profiler-results .profiler-button.profiler-button-active .profiler-unit,
|
||||
.profiler-results .profiler-controls.profiler-button-active .profiler-unit {
|
||||
color: #fff;
|
||||
font-weight: normal;
|
||||
}
|
||||
.profiler-results .profiler-controls {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-family: Consolas, monospace, serif;
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
}
|
||||
.profiler-results .profiler-controls span {
|
||||
border-right: 1px solid #aaaaaa;
|
||||
padding-right: 5px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.profiler-results .profiler-controls span:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
.profiler-results .profiler-popup {
|
||||
display: none;
|
||||
z-index: 2147483641;
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
border: 1px solid #aaa;
|
||||
padding: 5px 10px;
|
||||
text-align: left;
|
||||
line-height: 18px;
|
||||
overflow: auto;
|
||||
-moz-box-shadow: 0px 1px 15px #555555;
|
||||
-webkit-box-shadow: 0px 1px 15px #555555;
|
||||
box-shadow: 0px 1px 15px #555555;
|
||||
}
|
||||
.profiler-results .profiler-popup .profiler-info {
|
||||
margin-bottom: 3px;
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.profiler-results .profiler-popup .profiler-info .profiler-name {
|
||||
font-size: 110%;
|
||||
font-weight: bold;
|
||||
}
|
||||
.profiler-results .profiler-popup .profiler-info .profiler-name .profiler-overall-duration {
|
||||
display: none;
|
||||
}
|
||||
.profiler-results .profiler-popup .profiler-info .profiler-server-time {
|
||||
font-size: 95%;
|
||||
}
|
||||
.profiler-results .profiler-popup .profiler-timings th,
|
||||
.profiler-results .profiler-popup .profiler-timings td {
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
.profiler-results .profiler-popup .profiler-timings th {
|
||||
font-size: 95%;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
.profiler-results .profiler-popup .profiler-timings .profiler-label {
|
||||
max-width: 275px;
|
||||
}
|
||||
.profiler-results .profiler-queries {
|
||||
display: none;
|
||||
z-index: 2147483643;
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
background-color: #fff;
|
||||
}
|
||||
.profiler-results .profiler-queries th {
|
||||
font-size: 17px;
|
||||
}
|
||||
.profiler-results.profiler-min .profiler-result {
|
||||
display: none;
|
||||
}
|
||||
.profiler-results.profiler-min .profiler-controls span {
|
||||
display: none;
|
||||
}
|
||||
.profiler-results.profiler-min .profiler-controls .profiler-min-max {
|
||||
border-right: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
.profiler-queries-bg {
|
||||
z-index: 2147483642;
|
||||
display: none;
|
||||
background: #000;
|
||||
opacity: 0.7;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
min-width: 100%;
|
||||
}
|
||||
.profiler-result-full .profiler-result {
|
||||
width: 950px;
|
||||
margin: 30px auto;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-button {
|
||||
display: none;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-popup .profiler-info {
|
||||
font-size: 25px;
|
||||
border-bottom: 1px solid #aaaaaa;
|
||||
padding-bottom: 3px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-popup .profiler-info .profiler-overall-duration {
|
||||
padding-right: 20px;
|
||||
font-size: 80%;
|
||||
color: #888;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-popup .profiler-timings td,
|
||||
.profiler-result-full .profiler-result .profiler-popup .profiler-timings th {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-popup .profiler-timings th {
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-popup .profiler-timings td {
|
||||
font-size: 14px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-popup .profiler-timings td:first-child {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-popup .profiler-timings .profiler-label {
|
||||
max-width: 550px;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-queries {
|
||||
margin: 25px 0;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-queries table {
|
||||
width: 100%;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-queries th {
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
line-height: 20px;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-queries td {
|
||||
padding: 15px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.profiler-result-full .profiler-result .profiler-queries .profiler-info div {
|
||||
text-align: right;
|
||||
margin-bottom: 5px;
|
||||
}
|
|
@ -0,0 +1,960 @@
|
|||
"use strict";
|
||||
var MiniProfiler = (function () {
|
||||
var $;
|
||||
|
||||
var options,
|
||||
container,
|
||||
controls,
|
||||
fetchedIds = [],
|
||||
fetchingIds = [], // so we never pull down a profiler twice
|
||||
ajaxStartTime
|
||||
;
|
||||
|
||||
var hasLocalStorage = function () {
|
||||
try {
|
||||
return 'localStorage' in window && window['localStorage'] !== null;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
var getVersionedKey = function (keyPrefix) {
|
||||
return keyPrefix + '-' + options.version;
|
||||
};
|
||||
|
||||
var save = function (keyPrefix, value) {
|
||||
if (!hasLocalStorage()) { return; }
|
||||
|
||||
// clear old keys with this prefix, if any
|
||||
for (var i = 0; i < localStorage.length; i++) {
|
||||
if ((localStorage.key(i) || '').indexOf(keyPrefix) > -1) {
|
||||
localStorage.removeItem(localStorage.key(i));
|
||||
}
|
||||
}
|
||||
|
||||
// save under this version
|
||||
localStorage[getVersionedKey(keyPrefix)] = value;
|
||||
};
|
||||
|
||||
var load = function (keyPrefix) {
|
||||
if (!hasLocalStorage()) { return null; }
|
||||
|
||||
return localStorage[getVersionedKey(keyPrefix)];
|
||||
};
|
||||
|
||||
var fetchTemplates = function (success) {
|
||||
var key = 'templates',
|
||||
cached = load(key);
|
||||
|
||||
if (cached) {
|
||||
$('body').append(cached);
|
||||
success();
|
||||
}
|
||||
else {
|
||||
$.get(options.path + 'includes.tmpl?v=' + options.version, function (data) {
|
||||
if (data) {
|
||||
save(key, data);
|
||||
$('body').append(data);
|
||||
success();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var getClientPerformance = function() {
|
||||
return window.performance == null ? null : window.performance;
|
||||
};
|
||||
|
||||
var fetchResults = function (ids) {
|
||||
var clientPerformance, clientProbes, i, j, p, id, idx;
|
||||
|
||||
for (i = 0; i < ids.length; i++) {
|
||||
id = ids[i];
|
||||
|
||||
clientPerformance = null;
|
||||
clientProbes = null;
|
||||
|
||||
if (window.mPt) {
|
||||
clientProbes = mPt.results();
|
||||
for (j = 0; j < clientProbes.length; j++) {
|
||||
clientProbes[j].d = clientProbes[j].d.getTime();
|
||||
}
|
||||
mPt.flush();
|
||||
}
|
||||
|
||||
if (id == options.currentId) {
|
||||
|
||||
clientPerformance = getClientPerformance();
|
||||
|
||||
if (clientPerformance != null) {
|
||||
// ie is buggy strip out functions
|
||||
var copy = { navigation: {}, timing: {} };
|
||||
|
||||
var timing = $.extend({}, clientPerformance.timing);
|
||||
|
||||
for (p in timing) {
|
||||
if (timing.hasOwnProperty(p) && !$.isFunction(timing[p])) {
|
||||
copy.timing[p] = timing[p];
|
||||
}
|
||||
}
|
||||
if (clientPerformance.navigation) {
|
||||
copy.navigation.redirectCount = clientPerformance.navigation.redirectCount;
|
||||
}
|
||||
clientPerformance = copy;
|
||||
|
||||
// hack to add chrome timings
|
||||
if (window.chrome && window.chrome.loadTimes) {
|
||||
var chromeTimes = window.chrome.loadTimes();
|
||||
if (chromeTimes.firstPaintTime) {
|
||||
clientPerformance.timing["First Paint Time"] = Math.round(chromeTimes.firstPaintTime * 1000);
|
||||
}
|
||||
if (chromeTimes.firstPaintTime) {
|
||||
clientPerformance.timing["First Paint After Load Time"] = Math.round(chromeTimes.firstPaintAfterLoadTime * 1000);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} else if (ajaxStartTime != null && clientProbes && clientProbes.length > 0) {
|
||||
clientPerformance = { timing: { navigationStart: ajaxStartTime.getTime() } };
|
||||
ajaxStartTime = null;
|
||||
}
|
||||
|
||||
if ($.inArray(id, fetchedIds) < 0 && $.inArray(id, fetchingIds) < 0) {
|
||||
idx = fetchingIds.push(id) - 1;
|
||||
|
||||
$.ajax({
|
||||
url: options.path + 'results',
|
||||
data: { id: id, clientPerformance: clientPerformance, clientProbes: clientProbes, popup: 1 },
|
||||
dataType: 'json',
|
||||
global: false,
|
||||
type: 'POST',
|
||||
success: function (json) {
|
||||
fetchedIds.push(id);
|
||||
if (json != "hidden") {
|
||||
buttonShow(json);
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
fetchingIds.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var renderTemplate = function (json) {
|
||||
return $('#profilerTemplate').tmpl(json);
|
||||
};
|
||||
|
||||
var buttonShow = function (json) {
|
||||
var result = renderTemplate(json);
|
||||
|
||||
if (controls)
|
||||
result.insertBefore(controls);
|
||||
else
|
||||
result.appendTo(container);
|
||||
|
||||
var button = result.find('.profiler-button'),
|
||||
popup = result.find('.profiler-popup');
|
||||
|
||||
// button will appear in corner with the total profiling duration - click to show details
|
||||
button.click(function () { buttonClick(button, popup); });
|
||||
|
||||
// small duration steps and the column with aggregate durations are hidden by default; allow toggling
|
||||
toggleHidden(popup);
|
||||
|
||||
// lightbox in the queries
|
||||
popup.find('.profiler-queries-show').click(function () { queriesShow($(this), result); });
|
||||
|
||||
// limit count
|
||||
if (container.find('.profiler-result').length > options.maxTracesToShow)
|
||||
container.find('.profiler-result').first().remove();
|
||||
button.show();
|
||||
};
|
||||
|
||||
var toggleHidden = function (popup) {
|
||||
var trivial = popup.find('.profiler-toggle-trivial');
|
||||
var childrenTime = popup.find('.profiler-toggle-duration-with-children');
|
||||
var trivialGaps = popup.parent().find('.profiler-toggle-trivial-gaps');
|
||||
|
||||
var toggleIt = function (node) {
|
||||
var link = $(node),
|
||||
klass = "profiler-" + link.attr('class').substr('profiler-toggle-'.length),
|
||||
isHidden = link.text().indexOf('show') > -1;
|
||||
|
||||
popup.parent().find('.' + klass).toggle(isHidden);
|
||||
link.text(link.text().replace(isHidden ? 'show' : 'hide', isHidden ? 'hide' : 'show'));
|
||||
|
||||
popupPreventHorizontalScroll(popup);
|
||||
};
|
||||
|
||||
childrenTime.add(trivial).add(trivialGaps).click(function () {
|
||||
toggleIt(this);
|
||||
});
|
||||
|
||||
// if option is set or all our timings are trivial, go ahead and show them
|
||||
if (options.showTrivial || trivial.data('show-on-load')) {
|
||||
toggleIt(trivial);
|
||||
}
|
||||
// if option is set, go ahead and show time with children
|
||||
if (options.showChildrenTime) {
|
||||
toggleIt(childrenTime);
|
||||
}
|
||||
};
|
||||
|
||||
var buttonClick = function (button, popup) {
|
||||
// we're toggling this button/popup
|
||||
if (popup.is(':visible')) {
|
||||
popupHide(button, popup);
|
||||
}
|
||||
else {
|
||||
var visiblePopups = container.find('.profiler-popup:visible'),
|
||||
theirButtons = visiblePopups.siblings('.profiler-button');
|
||||
|
||||
// hide any other popups
|
||||
popupHide(theirButtons, visiblePopups);
|
||||
|
||||
// before showing the one we clicked
|
||||
popupShow(button, popup);
|
||||
}
|
||||
};
|
||||
|
||||
var popupShow = function (button, popup) {
|
||||
button.addClass('profiler-button-active');
|
||||
|
||||
popupSetDimensions(button, popup);
|
||||
|
||||
popup.show();
|
||||
|
||||
popupPreventHorizontalScroll(popup);
|
||||
};
|
||||
|
||||
var popupSetDimensions = function (button, popup) {
|
||||
var top = button.position().top - 1, // position next to the button we clicked
|
||||
windowHeight = $(window).height(),
|
||||
maxHeight = windowHeight - top - 40; // make sure the popup doesn't extend below the fold
|
||||
|
||||
popup
|
||||
.css({ 'top': top, 'max-height': maxHeight })
|
||||
.css(options.renderPosition, button.outerWidth() - 3); // move left or right, based on config
|
||||
};
|
||||
|
||||
var popupPreventHorizontalScroll = function (popup) {
|
||||
var childrenHeight = 0;
|
||||
|
||||
popup.children().each(function () { childrenHeight += $(this).height(); });
|
||||
|
||||
popup.css({ 'padding-right': childrenHeight > popup.height() ? 40 : 10 });
|
||||
};
|
||||
|
||||
var popupHide = function (button, popup) {
|
||||
button.removeClass('profiler-button-active');
|
||||
popup.hide();
|
||||
};
|
||||
|
||||
var queriesShow = function (link, result) {
|
||||
|
||||
var px = 30,
|
||||
win = $(window),
|
||||
width = win.width() - 2 * px,
|
||||
height = win.height() - 2 * px,
|
||||
queries = result.find('.profiler-queries');
|
||||
|
||||
// opaque background
|
||||
$('<div class="profiler-queries-bg"/>').appendTo('body').css({ 'height': $(document).height() }).show();
|
||||
|
||||
// center the queries and ensure long content is scrolled
|
||||
queries.css({ 'top': px, 'max-height': height, 'width': width }).css(options.renderPosition, px)
|
||||
.find('table').css({ 'width': width });
|
||||
|
||||
// have to show everything before we can get a position for the first query
|
||||
queries.show();
|
||||
|
||||
queriesScrollIntoView(link, queries, queries);
|
||||
|
||||
// syntax highlighting
|
||||
prettyPrint();
|
||||
};
|
||||
|
||||
var queriesScrollIntoView = function (link, queries, whatToScroll) {
|
||||
var id = link.closest('tr').attr('data-timing-id'),
|
||||
cells = queries.find('tr[data-timing-id="' + id + '"] td');
|
||||
|
||||
// ensure they're in view
|
||||
whatToScroll.scrollTop(whatToScroll.scrollTop() + cells.first().position().top - 100);
|
||||
|
||||
// highlight and then fade back to original bg color; do it ourselves to prevent any conflicts w/ jquery.UI or other implementations of Resig's color plugin
|
||||
cells.each(function () {
|
||||
var cell = $(this),
|
||||
highlightHex = '#FFFFBB',
|
||||
highlightRgb = getRGB(highlightHex),
|
||||
originalRgb = getRGB(cell.css('background-color')),
|
||||
getColorDiff = function (fx, i) {
|
||||
// adapted from John Resig's color plugin: http://plugins.jquery.com/project/color
|
||||
return Math.max(Math.min(parseInt((fx.pos * (originalRgb[i] - highlightRgb[i])) + highlightRgb[i], 10), 255), 0);
|
||||
};
|
||||
|
||||
// we need to animate some other property to piggy-back on the step function, so I choose you, opacity!
|
||||
cell.css({ 'opacity': 1, 'background-color': highlightHex })
|
||||
.animate({ 'opacity': 1 }, { duration: 2000, step: function (now, fx) {
|
||||
fx.elem.style.backgroundColor = "rgb(" + [getColorDiff(fx, 0), getColorDiff(fx, 1), getColorDiff(fx, 2)].join(",") + ")";
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Color Conversion functions from highlightFade
|
||||
// By Blair Mitchelmore
|
||||
// http://jquery.offput.ca/highlightFade/
|
||||
// Parse strings looking for color tuples [255,255,255]
|
||||
var getRGB = function (color) {
|
||||
var result;
|
||||
|
||||
// Check if we're already dealing with an array of colors
|
||||
if (color && color.constructor == Array && color.length == 3) return color;
|
||||
|
||||
// Look for rgb(num,num,num)
|
||||
if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)) return [parseInt(result[1]), parseInt(result[2]), parseInt(result[3])];
|
||||
|
||||
// Look for rgb(num%,num%,num%)
|
||||
if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)) return [parseFloat(result[1]) * 2.55, parseFloat(result[2]) * 2.55, parseFloat(result[3]) * 2.55];
|
||||
|
||||
// Look for #a0b1c2
|
||||
if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)) return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
|
||||
|
||||
// Look for #fff
|
||||
if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)) return [parseInt(result[1] + result[1], 16), parseInt(result[2] + result[2], 16), parseInt(result[3] + result[3], 16)];
|
||||
|
||||
// Look for rgba(0, 0, 0, 0) == transparent in Safari 3
|
||||
if (result = /rgba\(0, 0, 0, 0\)/.exec(color)) return colors['transparent'];
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
var bindDocumentEvents = function () {
|
||||
$(document).bind('click keyup', function (e) {
|
||||
|
||||
// this happens on every keystroke, and :visible is crazy expensive in IE <9
|
||||
// and in this case, the display:none check is sufficient.
|
||||
var popup = $('.profiler-popup').filter(function () { return $(this).css("display") !== "none"; });
|
||||
|
||||
if (!popup.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var button = popup.siblings('.profiler-button'),
|
||||
queries = popup.closest('.profiler-result').find('.profiler-queries'),
|
||||
bg = $('.profiler-queries-bg'),
|
||||
isEscPress = e.type == 'keyup' && e.which == 27,
|
||||
hidePopup = false,
|
||||
hideQueries = false;
|
||||
|
||||
if (bg.is(':visible')) {
|
||||
hideQueries = isEscPress || (e.type == 'click' && !$.contains(queries[0], e.target) && !$.contains(popup[0], e.target));
|
||||
}
|
||||
else if (popup.is(':visible')) {
|
||||
hidePopup = isEscPress || (e.type == 'click' && !$.contains(popup[0], e.target) && !$.contains(button[0], e.target) && button[0] != e.target);
|
||||
}
|
||||
|
||||
if (hideQueries) {
|
||||
bg.remove();
|
||||
queries.hide();
|
||||
}
|
||||
|
||||
if (hidePopup) {
|
||||
popupHide(button, popup);
|
||||
}
|
||||
});
|
||||
$(document).bind('keydown', options.toggleShortcut, function(e) {
|
||||
$('.profiler-results').toggle();
|
||||
});
|
||||
};
|
||||
|
||||
var initFullView = function () {
|
||||
|
||||
// first, get jquery tmpl, then render and bind handlers
|
||||
fetchTemplates(function () {
|
||||
|
||||
// profiler will be defined in the full page's head
|
||||
renderTemplate(profiler).appendTo(container);
|
||||
|
||||
var popup = $('.profiler-popup');
|
||||
|
||||
toggleHidden(popup);
|
||||
|
||||
prettyPrint();
|
||||
|
||||
// since queries are already shown, just highlight and scroll when clicking a "1 sql" link
|
||||
popup.find('.profiler-queries-show').click(function () {
|
||||
queriesScrollIntoView($(this), $('.profiler-queries'), $(document));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var initControls = function (container) {
|
||||
if (options.showControls) {
|
||||
controls = $('<div class="profiler-controls"><span class="profiler-min-max">m</span><span class="profiler-clear">c</span></div>').appendTo(container);
|
||||
|
||||
$('.profiler-controls .profiler-min-max').click(function () {
|
||||
container.toggleClass('profiler-min');
|
||||
});
|
||||
|
||||
container.hover(function () {
|
||||
if ($(this).hasClass('profiler-min')) {
|
||||
$(this).find('.profiler-min-max').show();
|
||||
}
|
||||
},
|
||||
function () {
|
||||
if ($(this).hasClass('profiler-min')) {
|
||||
$(this).find('.profiler-min-max').hide();
|
||||
}
|
||||
});
|
||||
|
||||
$('.profiler-controls .profiler-clear').click(function () {
|
||||
container.find('.profiler-result').remove();
|
||||
});
|
||||
}
|
||||
else {
|
||||
container.addClass('profiler-no-controls');
|
||||
}
|
||||
};
|
||||
|
||||
var initPopupView = function () {
|
||||
|
||||
if (options.authorized) {
|
||||
// all fetched profilings will go in here
|
||||
container = $('<div class="profiler-results"/>').appendTo('body');
|
||||
|
||||
// MiniProfiler.RenderIncludes() sets which corner to render in - default is upper left
|
||||
container.addClass("profiler-" + options.renderPosition);
|
||||
|
||||
//initialize the controls
|
||||
initControls(container);
|
||||
|
||||
// we'll render results json via a jquery.tmpl - after we get the templates, we'll fetch the initial json to populate it
|
||||
fetchTemplates(function () {
|
||||
// get master page profiler results
|
||||
fetchResults(options.ids);
|
||||
});
|
||||
if (options.startHidden) container.hide();
|
||||
}
|
||||
else {
|
||||
fetchResults(options.ids);
|
||||
}
|
||||
|
||||
var jQueryAjaxComplete = function (e, xhr, settings) {
|
||||
if (xhr) {
|
||||
// should be an array of strings, e.g. ["008c4813-9bd7-443d-9376-9441ec4d6a8c","16ff377b-8b9c-4c20-a7b5-97cd9fa7eea7"]
|
||||
var stringIds = xhr.getResponseHeader('X-MiniProfiler-Ids');
|
||||
if (stringIds) {
|
||||
var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds);
|
||||
fetchResults(ids);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// fetch profile results for any ajax calls
|
||||
// note, this does not use $ cause we want to hook into the main jQuery
|
||||
if (jQuery && jQuery(document) && jQuery(document).ajaxComplete) {
|
||||
jQuery(document).ajaxComplete(jQueryAjaxComplete);
|
||||
}
|
||||
|
||||
if (jQuery && jQuery(document).ajaxStart)
|
||||
jQuery(document).ajaxStart(function () { ajaxStartTime = new Date(); });
|
||||
|
||||
// fetch results after ASP Ajax calls
|
||||
if (typeof (Sys) != 'undefined' && typeof (Sys.WebForms) != 'undefined' && typeof (Sys.WebForms.PageRequestManager) != 'undefined') {
|
||||
// Get the instance of PageRequestManager.
|
||||
var PageRequestManager = Sys.WebForms.PageRequestManager.getInstance();
|
||||
|
||||
PageRequestManager.add_endRequest(function (sender, args) {
|
||||
if (args) {
|
||||
var response = args.get_response();
|
||||
if (response.get_responseAvailable() && response._xmlHttpRequest != null) {
|
||||
var stringIds = args.get_response().getResponseHeader('X-MiniProfiler-Ids');
|
||||
if (stringIds) {
|
||||
var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds);
|
||||
fetchResults(ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// more Asp.Net callbacks
|
||||
if (typeof (WebForm_ExecuteCallback) == "function") {
|
||||
WebForm_ExecuteCallback = (function (callbackObject) {
|
||||
// Store original function
|
||||
var original = WebForm_ExecuteCallback;
|
||||
|
||||
return function (callbackObject) {
|
||||
original(callbackObject);
|
||||
|
||||
var stringIds = callbackObject.xmlRequest.getResponseHeader('X-MiniProfiler-Ids');
|
||||
if (stringIds) {
|
||||
var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds);
|
||||
fetchResults(ids);
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
}
|
||||
|
||||
// also fetch results after ExtJS requests, in case it is being used
|
||||
if (typeof (Ext) != 'undefined' && typeof (Ext.Ajax) != 'undefined' && typeof (Ext.Ajax.on) != 'undefined') {
|
||||
// Ext.Ajax is a singleton, so we just have to attach to its 'requestcomplete' event
|
||||
Ext.Ajax.on('requestcomplete', function (e, xhr, settings) {
|
||||
//iframed file uploads don't have headers
|
||||
if (!xhr || !xhr.getResponseHeader) {
|
||||
return;
|
||||
}
|
||||
|
||||
var stringIds = xhr.getResponseHeader('X-MiniProfiler-Ids');
|
||||
if (stringIds) {
|
||||
var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds);
|
||||
fetchResults(ids);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof (MooTools) != 'undefined' && typeof (Request) != 'undefined') {
|
||||
Request.prototype.addEvents({
|
||||
onComplete: function() {
|
||||
var stringIds = this.xhr.getResponseHeader('X-MiniProfiler-Ids');
|
||||
if (stringIds) {
|
||||
var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds);
|
||||
fetchResults(ids);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// add support for AngularJS, which use the basic XMLHttpRequest object.
|
||||
if (window.angular && typeof (XMLHttpRequest) != 'undefined') {
|
||||
var _send = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.send = function sendReplacement(data) {
|
||||
this._onreadystatechange = this.onreadystatechange;
|
||||
|
||||
this.onreadystatechange = function onReadyStateChangeReplacement() {
|
||||
if (this.readyState == 4) {
|
||||
var stringIds = this.getResponseHeader('X-MiniProfiler-Ids');
|
||||
if (stringIds) {
|
||||
var ids = typeof JSON != 'undefined' ? JSON.parse(stringIds) : eval(stringIds);
|
||||
fetchResults(ids);
|
||||
}
|
||||
}
|
||||
|
||||
return this._onreadystatechange.apply(this, arguments);
|
||||
};
|
||||
|
||||
return _send.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
// some elements want to be hidden on certain doc events
|
||||
bindDocumentEvents();
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
init: function () {
|
||||
var script = document.getElementById('mini-profiler');
|
||||
if (!script || !script.getAttribute) return;
|
||||
|
||||
options = (function () {
|
||||
var version = script.getAttribute('data-version');
|
||||
var path = script.getAttribute('data-path');
|
||||
|
||||
var currentId = script.getAttribute('data-current-id');
|
||||
|
||||
var ids = script.getAttribute('data-ids');
|
||||
if (ids) ids = ids.split(',');
|
||||
|
||||
var position = script.getAttribute('data-position');
|
||||
|
||||
var toggleShortcut = script.getAttribute('data-toggle-shortcut');
|
||||
|
||||
if (script.getAttribute('data-max-traces')) {
|
||||
var maxTraces = parseInt(script.getAttribute('data-max-traces'), 10);
|
||||
}
|
||||
|
||||
if (script.getAttribute('data-trivial') === 'true') var trivial = true;
|
||||
if (script.getAttribute('data-children') == 'true') var children = true;
|
||||
if (script.getAttribute('data-controls') == 'true') var controls = true;
|
||||
if (script.getAttribute('data-authorized') == 'true') var authorized = true;
|
||||
if (script.getAttribute('data-start-hidden') == 'true') var startHidden = true;
|
||||
|
||||
return {
|
||||
ids: ids,
|
||||
path: path,
|
||||
version: version,
|
||||
renderPosition: position,
|
||||
showTrivial: trivial,
|
||||
showChildrenTime: children,
|
||||
maxTracesToShow: maxTraces,
|
||||
showControls: controls,
|
||||
currentId: currentId,
|
||||
authorized: authorized,
|
||||
toggleShortcut: toggleShortcut,
|
||||
startHidden: startHidden
|
||||
};
|
||||
})();
|
||||
|
||||
var doInit = function () {
|
||||
// when rendering a shared, full page, this div will exist
|
||||
container = $('.profiler-result-full');
|
||||
if (container.length) {
|
||||
if (window.location.href.indexOf("&trivial=1") > 0) {
|
||||
options.showTrivial = true;
|
||||
}
|
||||
initFullView();
|
||||
}
|
||||
else {
|
||||
initPopupView();
|
||||
}
|
||||
};
|
||||
|
||||
// this preserves debugging
|
||||
var load = function (s, f) {
|
||||
var sc = document.createElement("script");
|
||||
sc.async = "async";
|
||||
sc.type = "text/javascript";
|
||||
sc.src = s;
|
||||
var done = false;
|
||||
sc.onload = sc.onreadystatechange = function (_, abort) {
|
||||
if (!sc.readyState || /loaded|complete/.test(sc.readyState)) {
|
||||
if (!abort && !done) { done = true; f(); }
|
||||
}
|
||||
};
|
||||
document.getElementsByTagName('head')[0].appendChild(sc);
|
||||
};
|
||||
|
||||
var wait = 0;
|
||||
var finish = false;
|
||||
var deferInit = function() {
|
||||
if (finish) return;
|
||||
if (window.performance && window.performance.timing && window.performance.timing.loadEventEnd === 0 && wait < 10000) {
|
||||
setTimeout(deferInit, 100);
|
||||
wait += 100;
|
||||
} else {
|
||||
finish = true;
|
||||
init();
|
||||
}
|
||||
};
|
||||
|
||||
var init = function() {
|
||||
if (options.authorized) {
|
||||
var url = options.path + "includes.css?v=" + options.version;
|
||||
if (document.createStyleSheet) {
|
||||
document.createStyleSheet(url);
|
||||
} else {
|
||||
$('head').append($('<link rel="stylesheet" type="text/css" href="' + url + '" />'));
|
||||
}
|
||||
if (!$.tmpl) {
|
||||
load(options.path + 'jquery.tmpl.js?v=' + options.version, doInit);
|
||||
} else {
|
||||
doInit();
|
||||
}
|
||||
} else {
|
||||
doInit();
|
||||
}
|
||||
|
||||
// jquery.hotkeys.js
|
||||
// https://github.com/jeresig/jquery.hotkeys/blob/master/jquery.hotkeys.js
|
||||
|
||||
(function(d){function h(g){if("string"===typeof g.data){var h=g.handler,j=g.data.toLowerCase().split(" ");g.handler=function(b){if(!(this!==b.target&&(/textarea|select/i.test(b.target.nodeName)||"text"===b.target.type))){var c="keypress"!==b.type&&d.hotkeys.specialKeys[b.which],e=String.fromCharCode(b.which).toLowerCase(),a="",f={};b.altKey&&"alt"!==c&&(a+="alt+");b.ctrlKey&&"ctrl"!==c&&(a+="ctrl+");b.metaKey&&(!b.ctrlKey&&"meta"!==c)&&(a+="meta+");b.shiftKey&&"shift"!==c&&(a+="shift+");c?f[a+c]=
|
||||
!0:(f[a+e]=!0,f[a+d.hotkeys.shiftNums[e]]=!0,"shift+"===a&&(f[d.hotkeys.shiftNums[e]]=!0));c=0;for(e=j.length;c<e;c++)if(f[j[c]])return h.apply(this,arguments)}}}}d.hotkeys={version:"0.8",specialKeys:{8:"backspace",9:"tab",13:"return",16:"shift",17:"ctrl",18:"alt",19:"pause",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"insert",46:"del",96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9",106:"*",107:"+",
|
||||
109:"-",110:".",111:"/",112:"f1",113:"f2",114:"f3",115:"f4",116:"f5",117:"f6",118:"f7",119:"f8",120:"f9",121:"f10",122:"f11",123:"f12",144:"numlock",145:"scroll",191:"/",224:"meta"},shiftNums:{"`":"~",1:"!",2:"@",3:"#",4:"$",5:"%",6:"^",7:"&",8:"*",9:"(","0":")","-":"_","=":"+",";":": ","'":'"',",":"<",".":">","/":"?","\\":"|"}};d.each(["keydown","keyup","keypress"],function(){d.event.special[this]={add:h}})})(MiniProfiler.jQuery);
|
||||
|
||||
};
|
||||
|
||||
var major, minor;
|
||||
if (typeof(jQuery) == 'function') {
|
||||
var jQueryVersion = jQuery.fn.jquery.split('.');
|
||||
major = parseInt(jQueryVersion[0], 10);
|
||||
minor = parseInt(jQueryVersion[1], 10);
|
||||
}
|
||||
|
||||
|
||||
if (major === 2 || (major === 1 && minor >= 7)) {
|
||||
MiniProfiler.jQuery = $ = jQuery;
|
||||
$(deferInit);
|
||||
} else {
|
||||
load(options.path + "jquery.1.7.1.js?v=" + options.version, function() {
|
||||
MiniProfiler.jQuery = $ = jQuery.noConflict(true);
|
||||
$(deferInit);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getClientTimingByName: function (clientTiming, name) {
|
||||
|
||||
for (var i = 0; i < clientTiming.Timings.length; i++) {
|
||||
if (clientTiming.Timings[i].Name == name) {
|
||||
return clientTiming.Timings[i];
|
||||
}
|
||||
}
|
||||
return { Name: name, Duration: "", Start: "" };
|
||||
},
|
||||
|
||||
renderDate: function (jsonDate) { // JavaScriptSerializer sends dates as /Date(1308024322065)/
|
||||
if (jsonDate) {
|
||||
return (typeof jsonDate === 'string') ? new Date(parseInt(jsonDate.replace("/Date(", "").replace(")/", ""), 10)).toUTCString() : jsonDate;
|
||||
}
|
||||
},
|
||||
|
||||
renderIndent: function (depth) {
|
||||
var result = '';
|
||||
for (var i = 0; i < depth; i++) {
|
||||
result += ' ';
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
renderExecuteType: function (typeId) {
|
||||
// see StackExchange.Profiling.ExecuteType enum
|
||||
switch (typeId) {
|
||||
case 0: return 'None';
|
||||
case 1: return 'NonQuery';
|
||||
case 2: return 'Scalar';
|
||||
case 3: return 'Reader';
|
||||
}
|
||||
},
|
||||
|
||||
shareUrl: function (id) {
|
||||
return options.path + 'results?id=' + id;
|
||||
},
|
||||
|
||||
getClientTimings: function (clientTimings) {
|
||||
var list = [];
|
||||
var t;
|
||||
|
||||
if (!clientTimings.Timings) return [];
|
||||
|
||||
for (var i = 0; i < clientTimings.Timings.length; i++) {
|
||||
t = clientTimings.Timings[i];
|
||||
var trivial = t.Name != "Dom Complete" && t.Name != "Response" && t.Name != "First Paint Time";
|
||||
trivial = t.Duration < 2 ? trivial : false;
|
||||
list.push(
|
||||
{
|
||||
isTrivial: trivial,
|
||||
name: t.Name,
|
||||
duration: t.Duration,
|
||||
start: t.Start
|
||||
});
|
||||
}
|
||||
|
||||
list.sort(function (a, b) { return a.start - b.start; });
|
||||
return list;
|
||||
},
|
||||
|
||||
getSqlTimings: function (root) {
|
||||
var result = [],
|
||||
addToResults = function (timing) {
|
||||
if (timing.SqlTimings) {
|
||||
for (var i = 0, sqlTiming; i < timing.SqlTimings.length; i++) {
|
||||
sqlTiming = timing.SqlTimings[i];
|
||||
|
||||
// HACK: add info about the parent Timing to each SqlTiming so UI can render
|
||||
sqlTiming.ParentTimingName = timing.Name;
|
||||
result.push(sqlTiming);
|
||||
}
|
||||
}
|
||||
|
||||
if (timing.Children) {
|
||||
for (var i = 0; i < timing.Children.length; i++) {
|
||||
addToResults(timing.Children[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// start adding at the root and recurse down
|
||||
addToResults(root);
|
||||
|
||||
var removeDuration = function(list, duration) {
|
||||
|
||||
var newList = [];
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
|
||||
var item = list[i];
|
||||
if (duration.start > item.start) {
|
||||
if (duration.start > item.finish) {
|
||||
newList.push(item);
|
||||
continue;
|
||||
}
|
||||
newList.push({ start: item.start, finish: duration.start });
|
||||
}
|
||||
|
||||
if (duration.finish < item.finish) {
|
||||
if (duration.finish < item.start) {
|
||||
newList.push(item);
|
||||
continue;
|
||||
}
|
||||
newList.push({ start: duration.finish, finish: item.finish });
|
||||
}
|
||||
}
|
||||
|
||||
return newList;
|
||||
};
|
||||
|
||||
var processTimes = function (elem, parent) {
|
||||
var duration = { start: elem.StartMilliseconds, finish: (elem.StartMilliseconds + elem.DurationMilliseconds) };
|
||||
elem.richTiming = [duration];
|
||||
if (parent != null) {
|
||||
elem.parent = parent;
|
||||
elem.parent.richTiming = removeDuration(elem.parent.richTiming, duration);
|
||||
}
|
||||
|
||||
if (elem.Children) {
|
||||
for (var i = 0; i < elem.Children.length; i++) {
|
||||
processTimes(elem.Children[i], elem);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
processTimes(root, null);
|
||||
|
||||
// sort results by time
|
||||
result.sort(function (a, b) { return a.StartMilliseconds - b.StartMilliseconds; });
|
||||
|
||||
var determineOverlap = function(gap, node) {
|
||||
var overlap = 0;
|
||||
for (var i = 0; i < node.richTiming.length; i++) {
|
||||
var current = node.richTiming[i];
|
||||
if (current.start > gap.finish) {
|
||||
break;
|
||||
}
|
||||
if (current.finish < gap.start) {
|
||||
continue;
|
||||
}
|
||||
|
||||
overlap += Math.min(gap.finish, current.finish) - Math.max(gap.start, current.start);
|
||||
}
|
||||
return overlap;
|
||||
};
|
||||
|
||||
var determineGap = function (gap, node, match) {
|
||||
var overlap = determineOverlap(gap, node);
|
||||
if (match == null || overlap > match.duration) {
|
||||
match = { name: node.Name, duration: overlap };
|
||||
}
|
||||
else if (match.name == node.Name) {
|
||||
match.duration += overlap;
|
||||
}
|
||||
|
||||
if (node.Children) {
|
||||
for (var i = 0; i < node.Children.length; i++) {
|
||||
match = determineGap(gap, node.Children[i], match);
|
||||
}
|
||||
}
|
||||
return match;
|
||||
};
|
||||
|
||||
var time = 0;
|
||||
var prev = null;
|
||||
$.each(result, function () {
|
||||
this.prevGap = {
|
||||
duration: (this.StartMilliseconds - time).toFixed(2),
|
||||
start: time,
|
||||
finish: this.StartMilliseconds
|
||||
};
|
||||
|
||||
this.prevGap.topReason = determineGap(this.prevGap, root, null);
|
||||
|
||||
time = this.StartMilliseconds + this.DurationMilliseconds;
|
||||
prev = this;
|
||||
});
|
||||
|
||||
|
||||
if (result.length > 0) {
|
||||
var me = result[result.length - 1];
|
||||
me.nextGap = {
|
||||
duration: (root.DurationMilliseconds - time).toFixed(2),
|
||||
start: time,
|
||||
finish: root.DurationMilliseconds
|
||||
};
|
||||
me.nextGap.topReason = determineGap(me.nextGap, root, null);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
getSqlTimingsCount: function (root) {
|
||||
var result = 0,
|
||||
countSql = function (timing) {
|
||||
if (timing.SqlTimings) {
|
||||
result += timing.SqlTimings.length;
|
||||
}
|
||||
|
||||
if (timing.Children) {
|
||||
for (var i = 0; i < timing.Children.length; i++) {
|
||||
countSql(timing.Children[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
countSql(root);
|
||||
return result;
|
||||
},
|
||||
|
||||
fetchResultsExposed: function (ids) {
|
||||
return fetchResults(ids);
|
||||
},
|
||||
|
||||
formatDuration: function (duration) {
|
||||
return (duration || 0).toFixed(1);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
MiniProfiler.init();
|
||||
|
||||
if (typeof prettyPrint === "undefined") {
|
||||
|
||||
// prettify.js
|
||||
// http://code.google.com/p/google-code-prettify/
|
||||
|
||||
window.PR_SHOULD_USE_CONTINUATION=true;window.PR_TAB_WIDTH=8;window.PR_normalizedHtml=window.PR=window.prettyPrintOne=window.prettyPrint=void 0;window._pr_isIE6=function(){var y=navigator&&navigator.userAgent&&navigator.userAgent.match(/\bMSIE ([678])\./);y=y?+y[1]:false;window._pr_isIE6=function(){return y};return y};
|
||||
(function(){function y(b){return b.replace(L,"&").replace(M,"<").replace(N,">")}function H(b,f,i){switch(b.nodeType){case 1:var o=b.tagName.toLowerCase();f.push("<",o);var l=b.attributes,n=l.length;if(n){if(i){for(var r=[],j=n;--j>=0;)r[j]=l[j];r.sort(function(q,m){return q.name<m.name?-1:q.name===m.name?0:1});l=r}for(j=0;j<n;++j){r=l[j];r.specified&&f.push(" ",r.name.toLowerCase(),'="',r.value.replace(L,"&").replace(M,"<").replace(N,">").replace(X,"""),'"')}}f.push(">");
|
||||
for(l=b.firstChild;l;l=l.nextSibling)H(l,f,i);if(b.firstChild||!/^(?:br|link|img)$/.test(o))f.push("</",o,">");break;case 3:case 4:f.push(y(b.nodeValue));break}}function O(b){function f(c){if(c.charAt(0)!=="\\")return c.charCodeAt(0);switch(c.charAt(1)){case "b":return 8;case "t":return 9;case "n":return 10;case "v":return 11;case "f":return 12;case "r":return 13;case "u":case "x":return parseInt(c.substring(2),16)||c.charCodeAt(1);case "0":case "1":case "2":case "3":case "4":case "5":case "6":case "7":return parseInt(c.substring(1),
|
||||
8);default:return c.charCodeAt(1)}}function i(c){if(c<32)return(c<16?"\\x0":"\\x")+c.toString(16);c=String.fromCharCode(c);if(c==="\\"||c==="-"||c==="["||c==="]")c="\\"+c;return c}function o(c){var d=c.substring(1,c.length-1).match(RegExp("\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]","g"));c=[];for(var a=[],k=d[0]==="^",e=k?1:0,h=d.length;e<h;++e){var g=d[e];switch(g){case "\\B":case "\\b":case "\\D":case "\\d":case "\\S":case "\\s":case "\\W":case "\\w":c.push(g);
|
||||
continue}g=f(g);var s;if(e+2<h&&"-"===d[e+1]){s=f(d[e+2]);e+=2}else s=g;a.push([g,s]);if(!(s<65||g>122)){s<65||g>90||a.push([Math.max(65,g)|32,Math.min(s,90)|32]);s<97||g>122||a.push([Math.max(97,g)&-33,Math.min(s,122)&-33])}}a.sort(function(v,w){return v[0]-w[0]||w[1]-v[1]});d=[];g=[NaN,NaN];for(e=0;e<a.length;++e){h=a[e];if(h[0]<=g[1]+1)g[1]=Math.max(g[1],h[1]);else d.push(g=h)}a=["["];k&&a.push("^");a.push.apply(a,c);for(e=0;e<d.length;++e){h=d[e];a.push(i(h[0]));if(h[1]>h[0]){h[1]+1>h[0]&&a.push("-");
|
||||
a.push(i(h[1]))}}a.push("]");return a.join("")}function l(c){for(var d=c.source.match(RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g")),a=d.length,k=[],e=0,h=0;e<a;++e){var g=d[e];if(g==="(")++h;else if("\\"===g.charAt(0))if((g=+g.substring(1))&&g<=h)k[g]=-1}for(e=1;e<k.length;++e)if(-1===k[e])k[e]=++n;for(h=e=0;e<a;++e){g=d[e];if(g==="("){++h;if(k[h]===undefined)d[e]="(?:"}else if("\\"===
|
||||
g.charAt(0))if((g=+g.substring(1))&&g<=h)d[e]="\\"+k[h]}for(h=e=0;e<a;++e)if("^"===d[e]&&"^"!==d[e+1])d[e]="";if(c.ignoreCase&&r)for(e=0;e<a;++e){g=d[e];c=g.charAt(0);if(g.length>=2&&c==="[")d[e]=o(g);else if(c!=="\\")d[e]=g.replace(/[a-zA-Z]/g,function(s){s=s.charCodeAt(0);return"["+String.fromCharCode(s&-33,s|32)+"]"})}return d.join("")}for(var n=0,r=false,j=false,q=0,m=b.length;q<m;++q){var t=b[q];if(t.ignoreCase)j=true;else if(/[a-z]/i.test(t.source.replace(/\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi,
|
||||
""))){r=true;j=false;break}}var p=[];q=0;for(m=b.length;q<m;++q){t=b[q];if(t.global||t.multiline)throw Error(""+t);p.push("(?:"+l(t)+")")}return RegExp(p.join("|"),j?"gi":"g")}function Y(b){var f=0;return function(i){for(var o=null,l=0,n=0,r=i.length;n<r;++n)switch(i.charAt(n)){case "\t":o||(o=[]);o.push(i.substring(l,n));l=b-f%b;for(f+=l;l>=0;l-=16)o.push(" ".substring(0,l));l=n+1;break;case "\n":f=0;break;default:++f}if(!o)return i;o.push(i.substring(l));return o.join("")}}function I(b,
|
||||
f,i,o){if(f){b={source:f,c:b};i(b);o.push.apply(o,b.d)}}function B(b,f){var i={},o;(function(){for(var r=b.concat(f),j=[],q={},m=0,t=r.length;m<t;++m){var p=r[m],c=p[3];if(c)for(var d=c.length;--d>=0;)i[c.charAt(d)]=p;p=p[1];c=""+p;if(!q.hasOwnProperty(c)){j.push(p);q[c]=null}}j.push(/[\0-\uffff]/);o=O(j)})();var l=f.length;function n(r){for(var j=r.c,q=[j,z],m=0,t=r.source.match(o)||[],p={},c=0,d=t.length;c<d;++c){var a=t[c],k=p[a],e=void 0,h;if(typeof k==="string")h=false;else{var g=i[a.charAt(0)];
|
||||
if(g){e=a.match(g[1]);k=g[0]}else{for(h=0;h<l;++h){g=f[h];if(e=a.match(g[1])){k=g[0];break}}e||(k=z)}if((h=k.length>=5&&"lang-"===k.substring(0,5))&&!(e&&typeof e[1]==="string")){h=false;k=P}h||(p[a]=k)}g=m;m+=a.length;if(h){h=e[1];var s=a.indexOf(h),v=s+h.length;if(e[2]){v=a.length-e[2].length;s=v-h.length}k=k.substring(5);I(j+g,a.substring(0,s),n,q);I(j+g+s,h,Q(k,h),q);I(j+g+v,a.substring(v),n,q)}else q.push(j+g,k)}r.d=q}return n}function x(b){var f=[],i=[];if(b.tripleQuotedStrings)f.push([A,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,
|
||||
null,"'\""]);else b.multiLineStrings?f.push([A,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"]):f.push([A,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"]);b.verbatimStrings&&i.push([A,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null]);if(b.hashComments)if(b.cStyleComments){f.push([C,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"]);i.push([A,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,
|
||||
null])}else f.push([C,/^#[^\r\n]*/,null,"#"]);if(b.cStyleComments){i.push([C,/^\/\/[^\r\n]*/,null]);i.push([C,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}b.regexLiterals&&i.push(["lang-regex",RegExp("^"+Z+"(/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/)")]);b=b.keywords.replace(/^\s+|\s+$/g,"");b.length&&i.push([R,RegExp("^(?:"+b.replace(/\s+/g,"|")+")\\b"),null]);f.push([z,/^\s+/,null," \r\n\t\u00a0"]);i.push([J,/^@[a-z_$][a-z_$@0-9]*/i,null],[S,/^@?[A-Z]+[a-z][A-Za-z_$@0-9]*/,
|
||||
null],[z,/^[a-z_$][a-z_$@0-9]*/i,null],[J,/^(?:0x[a-f0-9]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+\-]?\d+)?)[a-z]*/i,null,"0123456789"],[E,/^.[^\s\w\.$@\'\"\`\/\#]*/,null]);return B(f,i)}function $(b){function f(D){if(D>r){if(j&&j!==q){n.push("</span>");j=null}if(!j&&q){j=q;n.push('<span class="',j,'">')}var T=y(p(i.substring(r,D))).replace(e?d:c,"$1 ");e=k.test(T);n.push(T.replace(a,s));r=D}}var i=b.source,o=b.g,l=b.d,n=[],r=0,j=null,q=null,m=0,t=0,p=Y(window.PR_TAB_WIDTH),c=/([\r\n ]) /g,
|
||||
d=/(^| ) /gm,a=/\r\n?|\n/g,k=/[ \r\n]$/,e=true,h=window._pr_isIE6();h=h?b.b.tagName==="PRE"?h===6?" \r\n":h===7?" <br>\r":" \r":" <br />":"<br />";var g=b.b.className.match(/\blinenums\b(?::(\d+))?/),s;if(g){for(var v=[],w=0;w<10;++w)v[w]=h+'</li><li class="L'+w+'">';var F=g[1]&&g[1].length?g[1]-1:0;n.push('<ol class="linenums"><li class="L',F%10,'"');F&&n.push(' value="',F+1,'"');n.push(">");s=function(){var D=v[++F%10];return j?"</span>"+D+'<span class="'+j+'">':D}}else s=h;
|
||||
for(;;)if(m<o.length?t<l.length?o[m]<=l[t]:true:false){f(o[m]);if(j){n.push("</span>");j=null}n.push(o[m+1]);m+=2}else if(t<l.length){f(l[t]);q=l[t+1];t+=2}else break;f(i.length);j&&n.push("</span>");g&&n.push("</li></ol>");b.a=n.join("")}function u(b,f){for(var i=f.length;--i>=0;){var o=f[i];if(G.hasOwnProperty(o))"console"in window&&console.warn("cannot override language handler %s",o);else G[o]=b}}function Q(b,f){b&&G.hasOwnProperty(b)||(b=/^\s*</.test(f)?"default-markup":"default-code");return G[b]}
|
||||
function U(b){var f=b.f,i=b.e;b.a=f;try{var o,l=f.match(aa);f=[];var n=0,r=[];if(l)for(var j=0,q=l.length;j<q;++j){var m=l[j];if(m.length>1&&m.charAt(0)==="<"){if(!ba.test(m))if(ca.test(m)){f.push(m.substring(9,m.length-3));n+=m.length-12}else if(da.test(m)){f.push("\n");++n}else if(m.indexOf(V)>=0&&m.replace(/\s(\w+)\s*=\s*(?:\"([^\"]*)\"|'([^\']*)'|(\S+))/g,' $1="$2$3$4"').match(/[cC][lL][aA][sS][sS]=\"[^\"]*\bnocode\b/)){var t=m.match(W)[2],p=1,c;c=j+1;a:for(;c<q;++c){var d=l[c].match(W);if(d&&
|
||||
d[2]===t)if(d[1]==="/"){if(--p===0)break a}else++p}if(c<q){r.push(n,l.slice(j,c+1).join(""));j=c}else r.push(n,m)}else r.push(n,m)}else{var a;p=m;var k=p.indexOf("&");if(k<0)a=p;else{for(--k;(k=p.indexOf("&#",k+1))>=0;){var e=p.indexOf(";",k);if(e>=0){var h=p.substring(k+3,e),g=10;if(h&&h.charAt(0)==="x"){h=h.substring(1);g=16}var s=parseInt(h,g);isNaN(s)||(p=p.substring(0,k)+String.fromCharCode(s)+p.substring(e+1))}}a=p.replace(ea,"<").replace(fa,">").replace(ga,"'").replace(ha,'"').replace(ia," ").replace(ja,
|
||||
"&")}f.push(a);n+=a.length}}o={source:f.join(""),h:r};var v=o.source;b.source=v;b.c=0;b.g=o.h;Q(i,v)(b);$(b)}catch(w){if("console"in window)console.log(w&&w.stack?w.stack:w)}}var A="str",R="kwd",C="com",S="typ",J="lit",E="pun",z="pln",P="src",V="nocode",Z=function(){for(var b=["!","!=","!==","#","%","%=","&","&&","&&=","&=","(","*","*=","+=",",","-=","->","/","/=",":","::",";","<","<<","<<=","<=","=","==","===",">",">=",">>",">>=",">>>",">>>=","?","@","[","^","^=","^^","^^=","{","|","|=","||","||=",
|
||||
"~","break","case","continue","delete","do","else","finally","instanceof","return","throw","try","typeof"],f="(?:^^|[+-]",i=0;i<b.length;++i)f+="|"+b[i].replace(/([^=<>:&a-z])/g,"\\$1");f+=")\\s*";return f}(),L=/&/g,M=/</g,N=/>/g,X=/\"/g,ea=/</g,fa=/>/g,ga=/'/g,ha=/"/g,ja=/&/g,ia=/ /g,ka=/[\r\n]/g,K=null,aa=RegExp("[^<]+|<!--[\\s\\S]*?--\>|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>|</?[a-zA-Z](?:[^>\"']|'[^']*'|\"[^\"]*\")*>|<","g"),ba=/^<\!--/,ca=/^<!\[CDATA\[/,da=/^<br\b/i,W=/^<(\/?)([a-zA-Z][a-zA-Z0-9]*)/,
|
||||
la=x({keywords:"break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof alignof align_union asm axiom bool concept concept_map const_cast constexpr decltype dynamic_cast explicit export friend inline late_check mutable namespace nullptr reinterpret_cast static_assert static_cast template typeid typename using virtual wchar_t where break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof abstract boolean byte extends final finally implements import instanceof null native package strictfp super synchronized throws transient as base by checked decimal delegate descending event fixed foreach from group implicit in interface internal into is lock object out override orderby params partial readonly ref sbyte sealed stackalloc string select uint ulong unchecked unsafe ushort var break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof debugger eval export function get null set undefined var with Infinity NaN caller delete die do dump elsif eval exit foreach for goto if import last local my next no our print package redo require sub undef unless until use wantarray while BEGIN END break continue do else for if return while and as assert class def del elif except exec finally from global import in is lambda nonlocal not or pass print raise try with yield False True None break continue do else for if return while alias and begin case class def defined elsif end ensure false in module next nil not or redo rescue retry self super then true undef unless until when yield BEGIN END break continue do else for if return while case done elif esac eval fi function in local set then until ",
|
||||
hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true}),G={};u(la,["default-code"]);u(B([],[[z,/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],[C,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[E,/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup",
|
||||
"htm","html","mxml","xhtml","xml","xsl"]);u(B([[z,/^[\s]+/,null," \t\r\n"],["atv",/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[E,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],
|
||||
["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);u(B([],[["atv",/^[\s\S]+/]]),["uq.val"]);u(x({keywords:"break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof alignof align_union asm axiom bool concept concept_map const_cast constexpr decltype dynamic_cast explicit export friend inline late_check mutable namespace nullptr reinterpret_cast static_assert static_cast template typeid typename using virtual wchar_t where ",
|
||||
hashComments:true,cStyleComments:true}),["c","cc","cpp","cxx","cyc","m"]);u(x({keywords:"null true false"}),["json"]);u(x({keywords:"break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof abstract boolean byte extends final finally implements import instanceof null native package strictfp super synchronized throws transient as base by checked decimal delegate descending event fixed foreach from group implicit in interface internal into is lock object out override orderby params partial readonly ref sbyte sealed stackalloc string select uint ulong unchecked unsafe ushort var ",
|
||||
hashComments:true,cStyleComments:true,verbatimStrings:true}),["cs"]);u(x({keywords:"break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof abstract boolean byte extends final finally implements import instanceof null native package strictfp super synchronized throws transient ",
|
||||
cStyleComments:true}),["java"]);u(x({keywords:"break continue do else for if return while case done elif esac eval fi function in local set then until ",hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);u(x({keywords:"break continue do else for if return while and as assert class def del elif except exec finally from global import in is lambda nonlocal not or pass print raise try with yield False True None ",hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);
|
||||
u(x({keywords:"caller delete die do dump elsif eval exit foreach for goto if import last local my next no our print package redo require sub undef unless until use wantarray while BEGIN END ",hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);u(x({keywords:"break continue do else for if return while alias and begin case class def defined elsif end ensure false in module next nil not or redo rescue retry self super then true undef unless until when yield BEGIN END ",hashComments:true,
|
||||
multiLineStrings:true,regexLiterals:true}),["rb"]);u(x({keywords:"break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof debugger eval export function get null set undefined var with Infinity NaN ",cStyleComments:true,regexLiterals:true}),["js"]);u(B([],[[A,/^[\s\S]+/]]),
|
||||
["regex"]);window.PR_normalizedHtml=H;window.prettyPrintOne=function(b,f){var i={f:b,e:f};U(i);return i.a};window.prettyPrint=function(b){function f(){for(var t=window.PR_SHOULD_USE_CONTINUATION?j.now()+250:Infinity;q<o.length&&j.now()<t;q++){var p=o[q];if(p.className&&p.className.indexOf("prettyprint")>=0){var c=p.className.match(/\blang-(\w+)\b/);if(c)c=c[1];for(var d=false,a=p.parentNode;a;a=a.parentNode)if((a.tagName==="pre"||a.tagName==="code"||a.tagName==="xmp")&&a.className&&a.className.indexOf("prettyprint")>=
|
||||
0){d=true;break}if(!d){a=p;if(null===K){d=document.createElement("PRE");d.appendChild(document.createTextNode('<!DOCTYPE foo PUBLIC "foo bar">\n<foo />'));K=!/</.test(d.innerHTML)}if(K){d=a.innerHTML;if("XMP"===a.tagName)d=y(d);else{a=a;if("PRE"===a.tagName)a=true;else if(ka.test(d)){var k="";if(a.currentStyle)k=a.currentStyle.whiteSpace;else if(window.getComputedStyle)k=window.getComputedStyle(a,null).whiteSpace;a=!k||k==="pre"}else a=true;a||(d=d.replace(/(<br\s*\/?>)[\r\n]+/g,"$1").replace(/(?:[\r\n]+[ \t]*)+/g,
|
||||
" "))}d=d}else{d=[];for(a=a.firstChild;a;a=a.nextSibling)H(a,d);d=d.join("")}d=d.replace(/(?:\r\n?|\n)$/,"");m={f:d,e:c,b:p};U(m);if(p=m.a){c=m.b;if("XMP"===c.tagName){d=document.createElement("PRE");for(a=0;a<c.attributes.length;++a){k=c.attributes[a];if(k.specified)if(k.name.toLowerCase()==="class")d.className=k.value;else d.setAttribute(k.name,k.value)}d.innerHTML=p;c.parentNode.replaceChild(d,c)}else c.innerHTML=p}}}}if(q<o.length)setTimeout(f,250);else b&&b()}for(var i=[document.getElementsByTagName("pre"),
|
||||
document.getElementsByTagName("code"),document.getElementsByTagName("xmp")],o=[],l=0;l<i.length;++l)for(var n=0,r=i[l].length;n<r;++n)o.push(i[l][n]);i=null;var j=Date;j.now||(j={now:function(){return(new Date).getTime()}});var q=0,m;f()};window.PR={combinePrefixPatterns:O,createSimpleLexer:B,registerLangHandler:u,sourceDecorator:x,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:C,PR_DECLARATION:"dec",PR_KEYWORD:R,PR_LITERAL:J,PR_NOCODE:V,PR_PLAIN:z,PR_PUNCTUATION:E,PR_SOURCE:P,PR_STRING:A,
|
||||
PR_TAG:"tag",PR_TYPE:S}})()
|
||||
|
||||
;
|
||||
|
||||
// lang-sql.js
|
||||
// http://code.google.com/p/google-code-prettify/
|
||||
|
||||
PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xA0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:[^\"\\]|\\.)*"|'(?:[^\'\\]|\\.)*')/,null,"\"'"]],[["com",/^(?:--[^\r\n]*|\/\*[\s\S]*?(?:\*\/|$))/],["kwd",/^(?:ADD|ALL|ALTER|AND|ANY|AS|ASC|AUTHORIZATION|BACKUP|BEGIN|BETWEEN|BREAK|BROWSE|BULK|BY|CASCADE|CASE|CHECK|CHECKPOINT|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMN|COMMIT|COMPUTE|CONSTRAINT|CONTAINS|CONTAINSTABLE|CONTINUE|CONVERT|CREATE|CROSS|CURRENT|CURRENT_DATE|CURRENT_TIME|CURRENT_TIMESTAMP|CURRENT_USER|CURSOR|DATABASE|DBCC|DEALLOCATE|DECLARE|DEFAULT|DELETE|DENY|DESC|DISK|DISTINCT|DISTRIBUTED|DOUBLE|DROP|DUMMY|DUMP|ELSE|END|ERRLVL|ESCAPE|EXCEPT|EXEC|EXECUTE|EXISTS|EXIT|FETCH|FILE|FILLFACTOR|FOR|FOREIGN|FREETEXT|FREETEXTTABLE|FROM|FULL|FUNCTION|GOTO|GRANT|GROUP|HAVING|HOLDLOCK|IDENTITY|IDENTITYCOL|IDENTITY_INSERT|IF|IN|INDEX|INNER|INSERT|INTERSECT|INTO|IS|JOIN|KEY|KILL|LEFT|LIKE|LINENO|LOAD|NATIONAL|NOCHECK|NONCLUSTERED|NOT|NULL|NULLIF|OF|OFF|OFFSETS|ON|OPEN|OPENDATASOURCE|OPENQUERY|OPENROWSET|OPENXML|OPTION|OR|ORDER|OUTER|OVER|PERCENT|PLAN|PRECISION|PRIMARY|PRINT|PROC|PROCEDURE|PUBLIC|RAISERROR|READ|READTEXT|RECONFIGURE|REFERENCES|REPLICATION|RESTORE|RESTRICT|RETURN|REVOKE|RIGHT|ROLLBACK|ROWCOUNT|ROWGUIDCOL|RULE|SAVE|SCHEMA|SELECT|SESSION_USER|SET|SETUSER|SHUTDOWN|SOME|STATISTICS|SYSTEM_USER|TABLE|TEXTSIZE|THEN|TO|TOP|TRAN|TRANSACTION|TRIGGER|TRUNCATE|TSEQUAL|UNION|UNIQUE|UPDATE|UPDATETEXT|USE|USER|VALUES|VARYING|VIEW|WAITFOR|WHEN|WHERE|WHILE|WITH|WRITETEXT)(?=[^\w-]|$)/i,
|
||||
null],["lit",/^[+-]?(?:0x[\da-f]+|(?:(?:\.\d+|\d+(?:\.\d*)?)(?:e[+\-]?\d+)?))/i],["pln",/^[a-z_][\w-]*/i],["pun",/^[^\w\t\n\r \xA0\"\'][^\w\t\n\r \xA0+\-\"\']*/]]),["sql"])
|
||||
|
||||
;
|
||||
}
|
|
@ -0,0 +1,471 @@
|
|||
.box-shadow(@dx, @dy, @radius, @color) {
|
||||
-moz-box-shadow: @dx @dy @radius @color;
|
||||
-webkit-box-shadow: @dx @dy @radius @color;
|
||||
box-shadow: @dx @dy @radius @color;
|
||||
}
|
||||
|
||||
@anchorColor: #0077CC;
|
||||
@buttonBorderColor: #888;
|
||||
@numberColor: #111;
|
||||
@textColor: #555;
|
||||
@mutedColor: #aaa;
|
||||
@normalFonts: Helvetica, Arial, sans-serif;
|
||||
@codeFonts: Consolas, monospace, serif;
|
||||
@zindex:2147483640; // near 32bit max 2147483647
|
||||
|
||||
// do some resets
|
||||
.profiler-result, .profiler-queries {
|
||||
color:#555;
|
||||
line-height:1;
|
||||
font-size:12px;
|
||||
|
||||
pre, code, label, table, tbody, thead, tfoot, tr, th, td {
|
||||
margin:0;
|
||||
padding:0;
|
||||
border:0;
|
||||
font-size:100%;
|
||||
font:inherit;
|
||||
vertical-align:baseline;
|
||||
background-color:transparent;
|
||||
overflow:visible;
|
||||
max-height:none;
|
||||
}
|
||||
table {
|
||||
border-collapse:collapse;
|
||||
border-spacing:0;
|
||||
}
|
||||
a, a:hover {
|
||||
cursor:pointer;
|
||||
color:@anchorColor;
|
||||
}
|
||||
a {
|
||||
text-decoration:none;
|
||||
&:hover {
|
||||
text-decoration:underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// styles shared between popup view and full view
|
||||
.profiler-result
|
||||
{
|
||||
|
||||
.profiler-toggle-duration-with-children
|
||||
{
|
||||
float: right;
|
||||
}
|
||||
|
||||
table.profiler-client-timings
|
||||
{
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
font-family:@normalFonts;
|
||||
|
||||
.profiler-label {
|
||||
color:@textColor;
|
||||
overflow:hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.profiler-unit {
|
||||
color:@mutedColor;
|
||||
}
|
||||
|
||||
.profiler-trivial {
|
||||
display:none;
|
||||
td, td * {
|
||||
color:@mutedColor !important;
|
||||
}
|
||||
}
|
||||
|
||||
pre, code, .profiler-number, .profiler-unit {
|
||||
font-family:@codeFonts;
|
||||
}
|
||||
|
||||
.profiler-number {
|
||||
color:@numberColor;
|
||||
}
|
||||
|
||||
.profiler-info {
|
||||
text-align:right;
|
||||
.profiler-name {
|
||||
float:left;
|
||||
}
|
||||
.profiler-server-time {
|
||||
white-space:nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-timings {
|
||||
th {
|
||||
background-color:#fff;
|
||||
color:@mutedColor;
|
||||
text-align:right;
|
||||
}
|
||||
th, td {
|
||||
white-space:nowrap;
|
||||
}
|
||||
.profiler-duration-with-children {
|
||||
display:none;
|
||||
}
|
||||
.profiler-duration {
|
||||
.profiler-number;
|
||||
text-align:right;
|
||||
}
|
||||
.profiler-indent {
|
||||
letter-spacing:4px;
|
||||
}
|
||||
.profiler-queries-show {
|
||||
.profiler-number, .profiler-unit {
|
||||
color:@anchorColor;
|
||||
}
|
||||
}
|
||||
.profiler-queries-duration {
|
||||
padding-left:6px;
|
||||
}
|
||||
.profiler-percent-in-sql {
|
||||
white-space:nowrap;
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
td {
|
||||
padding-top:10px;
|
||||
text-align:right;
|
||||
|
||||
a {
|
||||
font-size:95%;
|
||||
display:inline-block;
|
||||
margin-left:12px;
|
||||
|
||||
&:first-child {
|
||||
float:left;
|
||||
margin-left:0px;
|
||||
}
|
||||
&.profiler-custom-link {
|
||||
float:left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-queries {
|
||||
font-family:@normalFonts;
|
||||
|
||||
.profiler-stack-trace {
|
||||
margin-bottom:15px;
|
||||
}
|
||||
pre {
|
||||
font-family:@codeFonts;
|
||||
white-space:pre-wrap;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color:#fff;
|
||||
border-bottom:1px solid #555;
|
||||
font-weight:bold;
|
||||
padding:15px;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
padding:15px;
|
||||
text-align:left;
|
||||
background-color:#fff;
|
||||
|
||||
&:last-child {
|
||||
padding-right:25px; // compensate for scrollbars
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-odd td {
|
||||
background-color:#e5e5e5;
|
||||
}
|
||||
|
||||
.profiler-since-start, .profiler-duration {
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
.profiler-info div {
|
||||
text-align:right;
|
||||
margin-bottom:5px;
|
||||
}
|
||||
|
||||
.profiler-gap-info, .profiler-gap-info td { background-color: #ccc;}
|
||||
.profiler-gap-info {
|
||||
.profiler-unit {color: #777;}
|
||||
.profiler-info {text-align: right}
|
||||
&.profiler-trivial-gaps {display: none}
|
||||
}
|
||||
|
||||
.profiler-trivial-gap-container { text-align: center;}
|
||||
|
||||
// prettify colors
|
||||
.str{color:maroon}
|
||||
.kwd{color:#00008b}
|
||||
.com{color:gray}
|
||||
.typ{color:#2b91af}
|
||||
.lit{color:maroon}
|
||||
.pun{color:#000}
|
||||
.pln{color:#000}
|
||||
.tag{color:maroon}
|
||||
.atn{color:red}
|
||||
.atv{color:blue}
|
||||
.dec{color:purple}
|
||||
}
|
||||
|
||||
.profiler-warning, .profiler-warning *, .profiler-warning .profiler-queries-show, .profiler-warning .profiler-queries-show .profiler-unit { // i'm no good at css
|
||||
color:#f00;
|
||||
&:hover {
|
||||
color:#f00;
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-nuclear {
|
||||
.profiler-warning;
|
||||
font-weight:bold;
|
||||
padding-right:2px;
|
||||
}
|
||||
}
|
||||
|
||||
// ajaxed-in results will be appended to this
|
||||
.profiler-results
|
||||
{
|
||||
z-index:@zindex + 3;
|
||||
position:fixed;
|
||||
top:0px;
|
||||
|
||||
@radius:10px;
|
||||
|
||||
&.profiler-left {
|
||||
left:0px;
|
||||
|
||||
&.profiler-no-controls .profiler-result:last-child .profiler-button, .profiler-controls {
|
||||
-webkit-border-bottom-right-radius: @radius;
|
||||
-moz-border-radius-bottomright: @radius;
|
||||
border-bottom-right-radius: @radius;
|
||||
}
|
||||
|
||||
.profiler-button, .profiler-controls {
|
||||
border-right: 1px solid @buttonBorderColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.profiler-right {
|
||||
right:0px;
|
||||
|
||||
&.profiler-no-controls .profiler-result:last-child .profiler-button, .profiler-controls {
|
||||
-webkit-border-bottom-left-radius: @radius;
|
||||
-moz-border-radius-bottomleft: @radius;
|
||||
border-bottom-left-radius: @radius;
|
||||
}
|
||||
|
||||
.profiler-button, .profiler-controls {
|
||||
border-left: 1px solid @buttonBorderColor;
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-button, .profiler-controls {
|
||||
display:none;
|
||||
z-index:@zindex;
|
||||
border-bottom: 1px solid @buttonBorderColor;
|
||||
background-color:#fff;
|
||||
padding: 4px 7px;
|
||||
text-align:right;
|
||||
cursor:pointer;
|
||||
|
||||
&.profiler-button-active {
|
||||
background-color:maroon;
|
||||
|
||||
.profiler-number, .profiler-nuclear {
|
||||
color:#fff;
|
||||
font-weight:bold;
|
||||
}
|
||||
.profiler-unit {
|
||||
color:#fff;
|
||||
font-weight:normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-controls {
|
||||
display: block;
|
||||
font-size:12px;
|
||||
font-family: @codeFonts;
|
||||
cursor:default;
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
border-right: 1px solid @mutedColor;
|
||||
padding-right: 5px;
|
||||
margin-right: 5px;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-popup {
|
||||
display:none;
|
||||
z-index:@zindex + 1;
|
||||
position:absolute;
|
||||
background-color:#fff;
|
||||
border: 1px solid #aaa;
|
||||
padding:5px 10px;
|
||||
text-align:left;
|
||||
line-height:18px;
|
||||
overflow:auto;
|
||||
|
||||
.box-shadow(0px, 1px, 15px, #555);
|
||||
|
||||
.profiler-info {
|
||||
margin-bottom:3px;
|
||||
padding-bottom:2px;
|
||||
border-bottom:1px solid #ddd;
|
||||
|
||||
.profiler-name {
|
||||
font-size:110%;
|
||||
font-weight:bold;
|
||||
.profiler-overall-duration {
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
.profiler-server-time {
|
||||
font-size:95%;
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-timings {
|
||||
|
||||
th, td {
|
||||
padding-left:6px;
|
||||
padding-right:6px;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size:95%;
|
||||
padding-bottom:3px;
|
||||
}
|
||||
|
||||
.profiler-label {
|
||||
max-width:275px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-queries {
|
||||
display:none;
|
||||
z-index:@zindex + 3;
|
||||
position:absolute;
|
||||
overflow-y:auto;
|
||||
overflow-x:auto;
|
||||
background-color:#fff;
|
||||
|
||||
th {
|
||||
font-size:17px;
|
||||
}
|
||||
}
|
||||
|
||||
&.profiler-min .profiler-result {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.profiler-min .profiler-controls span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.profiler-min .profiler-controls .profiler-min-max {
|
||||
border-right: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
// popup results' queries will be displayed in front of this
|
||||
.profiler-queries-bg {
|
||||
z-index:@zindex + 2;
|
||||
display:none;
|
||||
background:#000;
|
||||
opacity:0.7;
|
||||
position:absolute;
|
||||
top:0px;
|
||||
left:0px;
|
||||
min-width:100%;
|
||||
}
|
||||
|
||||
// used when viewing a shared, full page result
|
||||
.profiler-result-full {
|
||||
.profiler-result {
|
||||
|
||||
width:950px;
|
||||
margin:30px auto;
|
||||
|
||||
.profiler-button {
|
||||
display:none;
|
||||
}
|
||||
|
||||
.profiler-popup {
|
||||
|
||||
.profiler-info {
|
||||
font-size: 25px;
|
||||
border-bottom:1px solid @mutedColor;
|
||||
padding-bottom:3px;
|
||||
margin-bottom:25px;
|
||||
|
||||
.profiler-overall-duration {
|
||||
padding-right:20px;
|
||||
font-size:80%;
|
||||
color:#888;
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-timings {
|
||||
td, th {
|
||||
padding-left:8px;
|
||||
padding-right:8px;
|
||||
}
|
||||
th {
|
||||
padding-bottom:7px;
|
||||
}
|
||||
td {
|
||||
font-size:14px;
|
||||
padding-bottom:4px;
|
||||
|
||||
&:first-child {
|
||||
padding-left:10px;
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-label {
|
||||
max-width:550px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profiler-queries {
|
||||
margin:25px 0;
|
||||
table {
|
||||
width:100%;
|
||||
}
|
||||
th {
|
||||
font-size:16px;
|
||||
color:#555;
|
||||
line-height:20px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding:15px 10px;
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
.profiler-info div {
|
||||
text-align:right;
|
||||
margin-bottom:5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
<script id="profilerTemplate" type="text/x-jquery-tmpl">
|
||||
|
||||
<div class="profiler-result">
|
||||
|
||||
<div class="profiler-button {{if HasDuplicateSqlTimings}}profiler-warning{{/if}}">
|
||||
{{if HasDuplicateSqlTimings}}<span class="profiler-nuclear">!</span>{{/if}}
|
||||
<span class="profiler-number">
|
||||
${MiniProfiler.formatDuration(DurationMilliseconds)} <span class="profiler-unit">ms</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="profiler-popup">
|
||||
<div class="profiler-info">
|
||||
<span class="profiler-name">
|
||||
${Name} <span class="profiler-overall-duration">(${MiniProfiler.formatDuration(DurationMilliseconds)} ms)</span>
|
||||
</span>
|
||||
<span class="profiler-server-time">${MachineName} on ${MiniProfiler.renderDate(Started)}</span>
|
||||
</div>
|
||||
<div class="profiler-output">
|
||||
<table class="profiler-timings">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>duration (ms)</th>
|
||||
<th class="profiler-duration-with-children">with children (ms)</th>
|
||||
<th class="time-from-start">from start (ms)</th>
|
||||
{{if HasSqlTimings}}
|
||||
<th colspan="2">query time (ms)</th>
|
||||
{{/if}}
|
||||
{{each CustomTimingNames}}
|
||||
<th colspan="2">${$value.toLowerCase()} (ms)</th>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{tmpl({timing:Root, page:this.data}) "#timingTemplate"}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
{{if !ClientTimings}}
|
||||
{{tmpl "#linksTemplate"}}
|
||||
{{/if}}
|
||||
<a class="profiler-toggle-duration-with-children" title="toggles column with aggregate child durations">show time with children</a>
|
||||
</td>
|
||||
{{if HasSqlTimings}}
|
||||
<td colspan="2" class="profiler-number profiler-percent-in-sql" title="${MiniProfiler.getSqlTimingsCount(Root)} queries spent ${MiniProfiler.formatDuration(DurationMillisecondsInSql)} ms of total request time">
|
||||
${MiniProfiler.formatDuration(DurationMillisecondsInSql / DurationMilliseconds * 100)}
|
||||
<span class="profiler-unit">% in sql</span>
|
||||
</td>
|
||||
{{/if}}
|
||||
{{each CustomTimingNames}}
|
||||
<td colspan="2" class="profiler-number profiler-percentage-in-sql" title="${CustomTimingStats[$value].Count} ${$value.toLowerCase()} invocations spent ${MiniProfiler.formatDuration(CustomTimingStats[$value].Duration)} ms of total request time">
|
||||
${MiniProfiler.formatDuration(CustomTimingStats[$value].Duration / DurationMilliseconds * 100)}
|
||||
<span class="profiler-unit">% in ${$value.toLowerCase()}</span>
|
||||
</td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
{{if ClientTimings}}
|
||||
<table class="profiler-timings profiler-client-timings">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>client event</th>
|
||||
<th>duration (ms)</th>
|
||||
<th>from start (ms)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{each MiniProfiler.getClientTimings(ClientTimings)}}
|
||||
<tr class="{{if $value.isTrivial }}profiler-trivial{{/if}}">
|
||||
<td class="profiler-label">${$value.name}</td>
|
||||
<td class="profiler-duration">
|
||||
{{if $value.duration >= 0}}
|
||||
<span class="profiler-unit"></span>${MiniProfiler.formatDuration($value.duration)}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="profiler-duration time-from-start">
|
||||
<span class="profiler-unit">+</span>${MiniProfiler.formatDuration($value.start)}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<td colspan="3">
|
||||
{{tmpl "#linksTemplate"}}
|
||||
</td>
|
||||
</tfoot>
|
||||
</table>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if HasSqlTimings}}
|
||||
<div class="profiler-queries">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:right">step<br />time from start<br />query type<br />duration</th>
|
||||
<th style="text-align:left">call stack<br />query</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{each(i, s) MiniProfiler.getSqlTimings(Root)}}
|
||||
{{tmpl({ g:s.prevGap }) "#sqlGapTemplate"}}
|
||||
{{tmpl({ i:i, s:s }) "#sqlTimingTemplate"}}
|
||||
{{if s.nextGap}}
|
||||
{{tmpl({ g:s.nextGap }) "#sqlGapTemplate"}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="profiler-trivial-gap-container">
|
||||
<a class="profiler-toggle-trivial-gaps" href="#">show trivial gaps</a>
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
|
||||
</script>
|
||||
|
||||
<script id="linksTemplate" type="text/x-jquery-tmpl">
|
||||
<a href="${MiniProfiler.shareUrl(Id)}" class="profiler-share-profiler-results" target="_blank">share</a>
|
||||
{{if CustomLink}}
|
||||
<a href="${CustomLink}" class="profiler-custom-link" target="_blank">${CustomLinkName}</a>
|
||||
{{/if}}
|
||||
{{if HasTrivialTimings}}
|
||||
<a class="profiler-toggle-trivial" data-show-on-load="${HasAllTrivialTimings}" title="toggles any rows with < ${TrivialDurationThresholdMilliseconds} ms">
|
||||
show trivial
|
||||
</a>
|
||||
{{/if}}
|
||||
</script>
|
||||
|
||||
<script id="timingTemplate" type="text/x-jquery-tmpl">
|
||||
|
||||
<tr class="{{if timing.IsTrivial }}profiler-trivial{{/if}}" data-timing-id="${timing.Id}">
|
||||
<td class="profiler-label" title="{{if timing.Name && timing.Name.length > 45 }}${timing.Name}{{/if}}">
|
||||
<span class="profiler-indent">${MiniProfiler.renderIndent(timing.Depth)}</span> ${timing.Name.slice(0,45)}{{if timing.Name && timing.Name.length > 45 }}...{{/if}}
|
||||
</td>
|
||||
<td class="profiler-duration" title="duration of this step without any children's durations">
|
||||
${MiniProfiler.formatDuration(timing.DurationWithoutChildrenMilliseconds)}
|
||||
</td>
|
||||
<td class="profiler-duration profiler-duration-with-children" title="duration of this step and its children">
|
||||
${MiniProfiler.formatDuration(timing.DurationMilliseconds)}
|
||||
</td>
|
||||
<td class="profiler-duration time-from-start" title="time elapsed since profiling started">
|
||||
<span class="profiler-unit">+</span>${MiniProfiler.formatDuration(timing.StartMilliseconds)}
|
||||
</td>
|
||||
|
||||
{{if timing.HasSqlTimings}}
|
||||
<td class="profiler-duration {{if timing.HasDuplicateSqlTimings}}profiler-warning{{/if}}" title="{{if timing.HasDuplicateSqlTimings}}duplicate queries detected - {{/if}}{{if timing.ExecutedReaders > 0 || timing.ExecutedScalars > 0 || timing.ExecutedNonQueries > 0}}${timing.ExecutedReaders} reader, ${timing.ExecutedScalars} scalar, ${timing.ExecutedNonQueries} non-query statements executed{{/if}}">
|
||||
<a class="profiler-queries-show">
|
||||
{{if timing.HasDuplicateSqlTimings}}<span class="profiler-nuclear">!</span>{{/if}}
|
||||
${timing.SqlTimings.length} <span class="profiler-unit">sql</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="profiler-duration" title="aggregate duration of all queries in this step (excludes children)">
|
||||
${MiniProfiler.formatDuration(timing.SqlTimingsDurationMilliseconds)}
|
||||
</td>
|
||||
{{else}}
|
||||
<td colspan="2"></td>
|
||||
{{/if}}
|
||||
|
||||
{{each page.CustomTimingNames}}
|
||||
{{if timing.CustomTimings && timing.CustomTimings[$value]}}
|
||||
<td class="profiler-duration" title="aggregate number of all ${$value.toLowerCase()} invocations in this step (excludes children)">
|
||||
${timing.CustomTimings[$value].length} ${$value.toLowerCase()}
|
||||
</td>
|
||||
<td class="profiler-duration" title="aggregate duration of all ${$value.toLowerCase()} invocations in this step (excludes children)">
|
||||
${MiniProfiler.formatDuration(timing.CustomTimingStats[$value].Duration)}
|
||||
</td>
|
||||
{{else}}
|
||||
<td colspan="2"></td>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
</tr>
|
||||
|
||||
{{if timing.HasChildren}}
|
||||
{{each timing.Children}}
|
||||
{{tmpl({timing: $value, page: page}) "#timingTemplate"}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
</script>
|
||||
|
||||
<script id="sqlTimingTemplate" type="text/x-jquery-tmpl">
|
||||
|
||||
<tr class="{{if i % 2 == 1}}profiler-odd{{/if}}" data-timing-id="${s.ParentTimingId}">
|
||||
<td class="profiler-info">
|
||||
<div>${s.ParentTimingName}</div>
|
||||
<div class="profiler-number"><span class="profiler-unit">T+</span>${MiniProfiler.formatDuration(s.StartMilliseconds)} <span class="profiler-unit">ms</span></div>
|
||||
<div>
|
||||
{{if s.IsDuplicate}}<span class="profiler-warning">DUPLICATE</span>{{/if}}
|
||||
${MiniProfiler.renderExecuteType(s.ExecuteType)}
|
||||
</div>
|
||||
<div title="{{if s.ExecuteType == 3}}first result fetched: ${s.FirstFetchDurationMilliseconds}ms{{/if}}">${MiniProfiler.formatDuration(s.DurationMilliseconds)} <span class="profiler-unit">ms</span></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="query">
|
||||
<pre class="profiler-stack-trace">${s.StackTraceSnippet}</pre>
|
||||
<pre class="prettyprint lang-sql"><code>${s.FormattedCommandString} </code></pre>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</script>
|
||||
|
||||
<script id="sqlGapTemplate" type="text/x-jquery-tmpl">
|
||||
|
||||
<tr class="profiler-gap-info{{if g.duration < 4}} profiler-trivial-gaps{{/if}}">
|
||||
<td class="profiler-info">
|
||||
${g.duration} <span class="profiler-unit">ms</span>
|
||||
</td>
|
||||
<td class="query">
|
||||
<div>${g.topReason.name} — ${g.topReason.duration.toFixed(2)} <span class="profiler-unit">ms</span></div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</script>
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,486 @@
|
|||
/*
|
||||
* jQuery Templating Plugin
|
||||
* Copyright 2010, John Resig
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
*/
|
||||
(function (jQuery, undefined) {
|
||||
var oldManip = jQuery.fn.domManip, tmplItmAtt = "_tmplitem", htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,
|
||||
newTmplItems = {}, wrappedItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} }, itemKey = 0, cloneIndex = 0, stack = [];
|
||||
|
||||
function newTmplItem(options, parentItem, fn, data) {
|
||||
// Returns a template item data structure for a new rendered instance of a template (a 'template item').
|
||||
// The content field is a hierarchical array of strings and nested items (to be
|
||||
// removed and replaced by nodes field of dom elements, once inserted in DOM).
|
||||
var newItem = {
|
||||
data: data || (parentItem ? parentItem.data : {}),
|
||||
_wrap: parentItem ? parentItem._wrap : null,
|
||||
tmpl: null,
|
||||
parent: parentItem || null,
|
||||
nodes: [],
|
||||
calls: tiCalls,
|
||||
nest: tiNest,
|
||||
wrap: tiWrap,
|
||||
html: tiHtml,
|
||||
update: tiUpdate
|
||||
};
|
||||
if (options) {
|
||||
jQuery.extend(newItem, options, { nodes: [], parent: parentItem });
|
||||
}
|
||||
if (fn) {
|
||||
// Build the hierarchical content to be used during insertion into DOM
|
||||
newItem.tmpl = fn;
|
||||
newItem._ctnt = newItem._ctnt || newItem.tmpl(jQuery, newItem);
|
||||
newItem.key = ++itemKey;
|
||||
// Keep track of new template item, until it is stored as jQuery Data on DOM element
|
||||
(stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem;
|
||||
}
|
||||
return newItem;
|
||||
}
|
||||
|
||||
// Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core).
|
||||
jQuery.each({
|
||||
appendTo: "append",
|
||||
prependTo: "prepend",
|
||||
insertBefore: "before",
|
||||
insertAfter: "after",
|
||||
replaceAll: "replaceWith"
|
||||
}, function (name, original) {
|
||||
jQuery.fn[name] = function (selector) {
|
||||
var ret = [], insert = jQuery(selector), elems, i, l, tmplItems,
|
||||
parent = this.length === 1 && this[0].parentNode;
|
||||
|
||||
appendToTmplItems = newTmplItems || {};
|
||||
if (parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1) {
|
||||
insert[original](this[0]);
|
||||
ret = this;
|
||||
} else {
|
||||
for (i = 0, l = insert.length; i < l; i++) {
|
||||
cloneIndex = i;
|
||||
elems = (i > 0 ? this.clone(true) : this).get();
|
||||
jQuery.fn[original].apply(jQuery(insert[i]), elems);
|
||||
ret = ret.concat(elems);
|
||||
}
|
||||
cloneIndex = 0;
|
||||
ret = this.pushStack(ret, name, insert.selector);
|
||||
}
|
||||
tmplItems = appendToTmplItems;
|
||||
appendToTmplItems = null;
|
||||
jQuery.tmpl.complete(tmplItems);
|
||||
return ret;
|
||||
};
|
||||
});
|
||||
|
||||
jQuery.fn.extend({
|
||||
// Use first wrapped element as template markup.
|
||||
// Return wrapped set of template items, obtained by rendering template against data.
|
||||
tmpl: function (data, options, parentItem) {
|
||||
return jQuery.tmpl(this[0], data, options, parentItem);
|
||||
},
|
||||
|
||||
// Find which rendered template item the first wrapped DOM element belongs to
|
||||
tmplItem: function () {
|
||||
return jQuery.tmplItem(this[0]);
|
||||
},
|
||||
|
||||
// Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template.
|
||||
template: function (name) {
|
||||
return jQuery.template(name, this[0]);
|
||||
},
|
||||
|
||||
domManip: function (args, table, callback, options) {
|
||||
// This appears to be a bug in the appendTo, etc. implementation
|
||||
// it should be doing .call() instead of .apply(). See #6227
|
||||
if (args[0] && args[0].nodeType) {
|
||||
var dmArgs = jQuery.makeArray(arguments), argsLength = args.length, i = 0, tmplItem;
|
||||
while (i < argsLength && !(tmplItem = jQuery.data(args[i++], "tmplItem"))) { }
|
||||
if (argsLength > 1) {
|
||||
dmArgs[0] = [jQuery.makeArray(args)];
|
||||
}
|
||||
if (tmplItem && cloneIndex) {
|
||||
dmArgs[2] = function (fragClone) {
|
||||
// Handler called by oldManip when rendered template has been inserted into DOM.
|
||||
jQuery.tmpl.afterManip(this, fragClone, callback);
|
||||
};
|
||||
}
|
||||
oldManip.apply(this, dmArgs);
|
||||
} else {
|
||||
oldManip.apply(this, arguments);
|
||||
}
|
||||
cloneIndex = 0;
|
||||
if (!appendToTmplItems) {
|
||||
jQuery.tmpl.complete(newTmplItems);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
jQuery.extend({
|
||||
// Return wrapped set of template items, obtained by rendering template against data.
|
||||
tmpl: function (tmpl, data, options, parentItem) {
|
||||
var ret, topLevel = !parentItem;
|
||||
if (topLevel) {
|
||||
// This is a top-level tmpl call (not from a nested template using {{tmpl}})
|
||||
parentItem = topTmplItem;
|
||||
tmpl = jQuery.template[tmpl] || jQuery.template(null, tmpl);
|
||||
wrappedItems = {}; // Any wrapped items will be rebuilt, since this is top level
|
||||
} else if (!tmpl) {
|
||||
// The template item is already associated with DOM - this is a refresh.
|
||||
// Re-evaluate rendered template for the parentItem
|
||||
tmpl = parentItem.tmpl;
|
||||
newTmplItems[parentItem.key] = parentItem;
|
||||
parentItem.nodes = [];
|
||||
if (parentItem.wrapped) {
|
||||
updateWrapped(parentItem, parentItem.wrapped);
|
||||
}
|
||||
// Rebuild, without creating a new template item
|
||||
return jQuery(build(parentItem, null, parentItem.tmpl(jQuery, parentItem)));
|
||||
}
|
||||
if (!tmpl) {
|
||||
return []; // Could throw...
|
||||
}
|
||||
if (typeof data === "function") {
|
||||
data = data.call(parentItem || {});
|
||||
}
|
||||
if (options && options.wrapped) {
|
||||
updateWrapped(options, options.wrapped);
|
||||
}
|
||||
ret = jQuery.isArray(data) ?
|
||||
jQuery.map(data, function (dataItem) {
|
||||
return dataItem ? newTmplItem(options, parentItem, tmpl, dataItem) : null;
|
||||
}) :
|
||||
[newTmplItem(options, parentItem, tmpl, data)];
|
||||
return topLevel ? jQuery(build(parentItem, null, ret)) : ret;
|
||||
},
|
||||
|
||||
// Return rendered template item for an element.
|
||||
tmplItem: function (elem) {
|
||||
var tmplItem;
|
||||
if (elem instanceof jQuery) {
|
||||
elem = elem[0];
|
||||
}
|
||||
while (elem && elem.nodeType === 1 && !(tmplItem = jQuery.data(elem, "tmplItem")) && (elem = elem.parentNode)) { }
|
||||
return tmplItem || topTmplItem;
|
||||
},
|
||||
|
||||
// Set:
|
||||
// Use $.template( name, tmpl ) to cache a named template,
|
||||
// where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc.
|
||||
// Use $( "selector" ).template( name ) to provide access by name to a script block template declaration.
|
||||
|
||||
// Get:
|
||||
// Use $.template( name ) to access a cached template.
|
||||
// Also $( selectorToScriptBlock ).template(), or $.template( null, templateString )
|
||||
// will return the compiled template, without adding a name reference.
|
||||
// If templateString includes at least one HTML tag, $.template( templateString ) is equivalent
|
||||
// to $.template( null, templateString )
|
||||
template: function (name, tmpl) {
|
||||
if (tmpl) {
|
||||
// Compile template and associate with name
|
||||
if (typeof tmpl === "string") {
|
||||
// This is an HTML string being passed directly in.
|
||||
tmpl = buildTmplFn(tmpl)
|
||||
} else if (tmpl instanceof jQuery) {
|
||||
tmpl = tmpl[0] || {};
|
||||
}
|
||||
if (tmpl.nodeType) {
|
||||
// If this is a template block, use cached copy, or generate tmpl function and cache.
|
||||
tmpl = jQuery.data(tmpl, "tmpl") || jQuery.data(tmpl, "tmpl", buildTmplFn(tmpl.innerHTML));
|
||||
}
|
||||
return typeof name === "string" ? (jQuery.template[name] = tmpl) : tmpl;
|
||||
}
|
||||
// Return named compiled template
|
||||
return name ? (typeof name !== "string" ? jQuery.template(null, name) :
|
||||
(jQuery.template[name] ||
|
||||
// If not in map, treat as a selector. (If integrated with core, use quickExpr.exec)
|
||||
jQuery.template(null, htmlExpr.test(name) ? name : jQuery(name)))) : null;
|
||||
},
|
||||
|
||||
encode: function (text) {
|
||||
// Do HTML encoding replacing < > & and ' and " by corresponding entities.
|
||||
return ("" + text).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'");
|
||||
}
|
||||
});
|
||||
|
||||
jQuery.extend(jQuery.tmpl, {
|
||||
tag: {
|
||||
"tmpl": {
|
||||
_default: { $2: "null" },
|
||||
open: "if($notnull_1){_=_.concat($item.nest($1,$2));}"
|
||||
// tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions)
|
||||
// This means that {{tmpl foo}} treats foo as a template (which IS a function).
|
||||
// Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}.
|
||||
},
|
||||
"wrap": {
|
||||
_default: { $2: "null" },
|
||||
open: "$item.calls(_,$1,$2);_=[];",
|
||||
close: "call=$item.calls();_=call._.concat($item.wrap(call,_));"
|
||||
},
|
||||
"each": {
|
||||
_default: { $2: "$index, $value" },
|
||||
open: "if($notnull_1){$.each($1a,function($2){with(this){",
|
||||
close: "}});}"
|
||||
},
|
||||
"if": {
|
||||
open: "if(($notnull_1) && $1a){",
|
||||
close: "}"
|
||||
},
|
||||
"else": {
|
||||
_default: { $1: "true" },
|
||||
open: "}else if(($notnull_1) && $1a){"
|
||||
},
|
||||
"html": {
|
||||
// Unecoded expression evaluation.
|
||||
open: "if($notnull_1){_.push($1a);}"
|
||||
},
|
||||
"=": {
|
||||
// Encoded expression evaluation. Abbreviated form is ${}.
|
||||
_default: { $1: "$data" },
|
||||
open: "if($notnull_1){_.push($.encode($1a));}"
|
||||
},
|
||||
"!": {
|
||||
// Comment tag. Skipped by parser
|
||||
open: ""
|
||||
}
|
||||
},
|
||||
|
||||
// This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events
|
||||
complete: function (items) {
|
||||
newTmplItems = {};
|
||||
},
|
||||
|
||||
// Call this from code which overrides domManip, or equivalent
|
||||
// Manage cloning/storing template items etc.
|
||||
afterManip: function afterManip(elem, fragClone, callback) {
|
||||
// Provides cloned fragment ready for fixup prior to and after insertion into DOM
|
||||
var content = fragClone.nodeType === 11 ?
|
||||
jQuery.makeArray(fragClone.childNodes) :
|
||||
fragClone.nodeType === 1 ? [fragClone] : [];
|
||||
|
||||
// Return fragment to original caller (e.g. append) for DOM insertion
|
||||
callback.call(elem, fragClone);
|
||||
|
||||
// Fragment has been inserted:- Add inserted nodes to tmplItem data structure. Replace inserted element annotations by jQuery.data.
|
||||
storeTmplItems(content);
|
||||
cloneIndex++;
|
||||
}
|
||||
});
|
||||
|
||||
//========================== Private helper functions, used by code above ==========================
|
||||
|
||||
function build(tmplItem, nested, content) {
|
||||
// Convert hierarchical content into flat string array
|
||||
// and finally return array of fragments ready for DOM insertion
|
||||
var frag, ret = content ? jQuery.map(content, function (item) {
|
||||
return (typeof item === "string") ?
|
||||
// Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM.
|
||||
(tmplItem.key ? item.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g, "$1 " + tmplItmAtt + "=\"" + tmplItem.key + "\" $2") : item) :
|
||||
// This is a child template item. Build nested template.
|
||||
build(item, tmplItem, item._ctnt);
|
||||
}) :
|
||||
// If content is not defined, insert tmplItem directly. Not a template item. May be a string, or a string array, e.g. from {{html $item.html()}}.
|
||||
tmplItem;
|
||||
if (nested) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// top-level template
|
||||
ret = ret.join("");
|
||||
|
||||
// Support templates which have initial or final text nodes, or consist only of text
|
||||
// Also support HTML entities within the HTML markup.
|
||||
ret.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/, function (all, before, middle, after) {
|
||||
frag = jQuery(middle).get();
|
||||
|
||||
storeTmplItems(frag);
|
||||
if (before) {
|
||||
frag = unencode(before).concat(frag);
|
||||
}
|
||||
if (after) {
|
||||
frag = frag.concat(unencode(after));
|
||||
}
|
||||
});
|
||||
return frag ? frag : unencode(ret);
|
||||
}
|
||||
|
||||
function unencode(text) {
|
||||
// Use createElement, since createTextNode will not render HTML entities correctly
|
||||
var el = document.createElement("div");
|
||||
el.innerHTML = text;
|
||||
return jQuery.makeArray(el.childNodes);
|
||||
}
|
||||
|
||||
// Generate a reusable function that will serve to render a template against data
|
||||
function buildTmplFn(markup) {
|
||||
return new Function("jQuery", "$item",
|
||||
"var $=jQuery,call,_=[],$data=$item.data;" +
|
||||
|
||||
// Introduce the data as local variables using with(){}
|
||||
"with($data){_.push('" +
|
||||
|
||||
// Convert the template into pure JavaScript
|
||||
jQuery.trim(markup)
|
||||
.replace(/([\\'])/g, "\\$1")
|
||||
.replace(/[\r\t\n]/g, " ")
|
||||
.replace(/\$\{([^\}]*)\}/g, "{{= $1}}")
|
||||
.replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,
|
||||
function (all, slash, type, fnargs, target, parens, args) {
|
||||
var tag = jQuery.tmpl.tag[type], def, expr, exprAutoFnDetect;
|
||||
if (!tag) {
|
||||
throw "Template command not found: " + type;
|
||||
}
|
||||
def = tag._default || [];
|
||||
if (parens && !/\w$/.test(target)) {
|
||||
target += parens;
|
||||
parens = "";
|
||||
}
|
||||
if (target) {
|
||||
target = unescape(target);
|
||||
args = args ? ("," + unescape(args) + ")") : (parens ? ")" : "");
|
||||
// Support for target being things like a.toLowerCase();
|
||||
// In that case don't call with template item as 'this' pointer. Just evaluate...
|
||||
expr = parens ? (target.indexOf(".") > -1 ? target + parens : ("(" + target + ").call($item" + args)) : target;
|
||||
exprAutoFnDetect = parens ? expr : "(typeof(" + target + ")==='function'?(" + target + ").call($item):(" + target + "))";
|
||||
} else {
|
||||
exprAutoFnDetect = expr = def.$1 || "null";
|
||||
}
|
||||
fnargs = unescape(fnargs);
|
||||
return "');" +
|
||||
tag[slash ? "close" : "open"]
|
||||
.split("$notnull_1").join(target ? "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" : "true")
|
||||
.split("$1a").join(exprAutoFnDetect)
|
||||
.split("$1").join(expr)
|
||||
.split("$2").join(fnargs ?
|
||||
fnargs.replace(/\s*([^\(]+)\s*(\((.*?)\))?/g, function (all, name, parens, params) {
|
||||
params = params ? ("," + params + ")") : (parens ? ")" : "");
|
||||
return params ? ("(" + name + ").call($item" + params) : all;
|
||||
})
|
||||
: (def.$2 || "")
|
||||
) +
|
||||
"_.push('";
|
||||
}) +
|
||||
"');}return _;"
|
||||
);
|
||||
}
|
||||
function updateWrapped(options, wrapped) {
|
||||
// Build the wrapped content.
|
||||
options._wrap = build(options, true,
|
||||
// Suport imperative scenario in which options.wrapped can be set to a selector or an HTML string.
|
||||
jQuery.isArray(wrapped) ? wrapped : [htmlExpr.test(wrapped) ? wrapped : jQuery(wrapped).html()]
|
||||
).join("");
|
||||
}
|
||||
|
||||
function unescape(args) {
|
||||
return args ? args.replace(/\\'/g, "'").replace(/\\\\/g, "\\") : null;
|
||||
}
|
||||
function outerHtml(elem) {
|
||||
var div = document.createElement("div");
|
||||
div.appendChild(elem.cloneNode(true));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Store template items in jQuery.data(), ensuring a unique tmplItem data data structure for each rendered template instance.
|
||||
function storeTmplItems(content) {
|
||||
var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {}, i, l, m;
|
||||
for (i = 0, l = content.length; i < l; i++) {
|
||||
if ((elem = content[i]).nodeType !== 1) {
|
||||
continue;
|
||||
}
|
||||
elems = elem.getElementsByTagName("*");
|
||||
for (m = elems.length - 1; m >= 0; m--) {
|
||||
processItemKey(elems[m]);
|
||||
}
|
||||
processItemKey(elem);
|
||||
}
|
||||
function processItemKey(el) {
|
||||
var pntKey, pntNode = el, pntItem, tmplItem, key;
|
||||
// Ensure that each rendered template inserted into the DOM has its own template item,
|
||||
if ((key = el.getAttribute(tmplItmAtt))) {
|
||||
while (pntNode.parentNode && (pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute(tmplItmAtt))) { }
|
||||
if (pntKey !== key) {
|
||||
// The next ancestor with a _tmplitem expando is on a different key than this one.
|
||||
// So this is a top-level element within this template item
|
||||
// Set pntNode to the key of the parentNode, or to 0 if pntNode.parentNode is null, or pntNode is a fragment.
|
||||
pntNode = pntNode.parentNode ? (pntNode.nodeType === 11 ? 0 : (pntNode.getAttribute(tmplItmAtt) || 0)) : 0;
|
||||
if (!(tmplItem = newTmplItems[key])) {
|
||||
// The item is for wrapped content, and was copied from the temporary parent wrappedItem.
|
||||
tmplItem = wrappedItems[key];
|
||||
tmplItem = newTmplItem(tmplItem, newTmplItems[pntNode] || wrappedItems[pntNode], null, true);
|
||||
tmplItem.key = ++itemKey;
|
||||
newTmplItems[itemKey] = tmplItem;
|
||||
}
|
||||
if (cloneIndex) {
|
||||
cloneTmplItem(key);
|
||||
}
|
||||
}
|
||||
el.removeAttribute(tmplItmAtt);
|
||||
} else if (cloneIndex && (tmplItem = jQuery.data(el, "tmplItem"))) {
|
||||
// This was a rendered element, cloned during append or appendTo etc.
|
||||
// TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem.
|
||||
cloneTmplItem(tmplItem.key);
|
||||
newTmplItems[tmplItem.key] = tmplItem;
|
||||
pntNode = jQuery.data(el.parentNode, "tmplItem");
|
||||
pntNode = pntNode ? pntNode.key : 0;
|
||||
}
|
||||
if (tmplItem) {
|
||||
pntItem = tmplItem;
|
||||
// Find the template item of the parent element.
|
||||
// (Using !=, not !==, since pntItem.key is number, and pntNode may be a string)
|
||||
while (pntItem && pntItem.key != pntNode) {
|
||||
// Add this element as a top-level node for this rendered template item, as well as for any
|
||||
// ancestor items between this item and the item of its parent element
|
||||
pntItem.nodes.push(el);
|
||||
pntItem = pntItem.parent;
|
||||
}
|
||||
// Delete content built during rendering - reduce API surface area and memory use, and avoid exposing of stale data after rendering...
|
||||
delete tmplItem._ctnt;
|
||||
delete tmplItem._wrap;
|
||||
// Store template item as jQuery data on the element
|
||||
jQuery.data(el, "tmplItem", tmplItem);
|
||||
}
|
||||
function cloneTmplItem(key) {
|
||||
key = key + keySuffix;
|
||||
tmplItem = newClonedItems[key] =
|
||||
(newClonedItems[key] || newTmplItem(tmplItem, newTmplItems[tmplItem.parent.key + keySuffix] || tmplItem.parent, null, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//---- Helper functions for template item ----
|
||||
|
||||
function tiCalls(content, tmpl, data, options) {
|
||||
if (!content) {
|
||||
return stack.pop();
|
||||
}
|
||||
stack.push({ _: content, tmpl: tmpl, item: this, data: data, options: options });
|
||||
}
|
||||
|
||||
function tiNest(tmpl, data, options) {
|
||||
// nested template, using {{tmpl}} tag
|
||||
return jQuery.tmpl(jQuery.template(tmpl), data, options, this);
|
||||
}
|
||||
|
||||
function tiWrap(call, wrapped) {
|
||||
// nested template, using {{wrap}} tag
|
||||
var options = call.options || {};
|
||||
options.wrapped = wrapped;
|
||||
// Apply the template, which may incorporate wrapped content,
|
||||
return jQuery.tmpl(jQuery.template(call.tmpl), call.data, options, call.item);
|
||||
}
|
||||
|
||||
function tiHtml(filter, textOnly) {
|
||||
var wrapped = this._wrap;
|
||||
return jQuery.map(
|
||||
jQuery(jQuery.isArray(wrapped) ? wrapped.join("") : wrapped).filter(filter || "*"),
|
||||
function (e) {
|
||||
return textOnly ?
|
||||
e.innerText || e.textContent :
|
||||
e.outerHTML || outerHtml(e);
|
||||
});
|
||||
}
|
||||
|
||||
function tiUpdate() {
|
||||
var coll = this.nodes;
|
||||
jQuery.tmpl(null, null, null, this).insertBefore(coll[0]);
|
||||
jQuery(coll).remove();
|
||||
}
|
||||
})(MiniProfiler.jQuery);
|
|
@ -0,0 +1,9 @@
|
|||
tbody tr:nth-child(odd) { background-color:#eee; }
|
||||
tbody tr:nth-child(even) { background-color:#fff; }
|
||||
table { border: 0; border-spacing:0;}
|
||||
tr {border: 0;}
|
||||
.date {font-size: 11px; color: #666;}
|
||||
td {padding: 8px;}
|
||||
.time {text-align:center;}
|
||||
thead tr {background-color: #bbb; color: #444; font-size: 12px;}
|
||||
thead tr th { padding: 5px 15px;}
|
|
@ -0,0 +1,38 @@
|
|||
var MiniProfiler = MiniProfiler || {};
|
||||
MiniProfiler.list = {
|
||||
init:
|
||||
function (options) {
|
||||
var $ = MiniProfiler.jQuery;
|
||||
var opt = options || {};
|
||||
|
||||
var updateGrid = function (id) {
|
||||
$.ajax({
|
||||
url: options.path + 'results-list',
|
||||
data: { "last-id": id },
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
success: function (data) {
|
||||
$('table tbody').append($("#rowTemplate").tmpl(data));
|
||||
var oldId = id;
|
||||
var oldData = data;
|
||||
setTimeout(function () {
|
||||
var newId = oldId;
|
||||
if (oldData.length > 0) {
|
||||
newId = oldData[oldData.length - 1].Id;
|
||||
}
|
||||
updateGrid(newId);
|
||||
}, 4000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
MiniProfiler.path = options.path;
|
||||
$.get(options.path + 'list.tmpl?v=' + options.version, function (data) {
|
||||
if (data) {
|
||||
$('body').append(data);
|
||||
$('body').append($('#tableTemplate').tmpl());
|
||||
updateGrid();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
<script id="tableTemplate" type="text/x-jquery-tmpl">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Started</th>
|
||||
<th>Sql Duration</th>
|
||||
<th>Total Duration</th>
|
||||
<th>Request Start</th>
|
||||
<th>Response Start</th>
|
||||
<th>Dom Complete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</script>
|
||||
<script id="rowTemplate" type="text/x-jquery-tmpl">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="${MiniProfiler.path}results?id=${Id}">${Name}</a></td>
|
||||
<td class="date">${MiniProfiler.renderDate(Started)}</td>
|
||||
<td class="time">${DurationMillisecondsInSql}</td>
|
||||
<td class="time">${DurationMilliseconds}</td>
|
||||
{{if ClientTimings}}
|
||||
<td class="time">${MiniProfiler.getClientTimingByName(ClientTimings,"Request").Start}</td>
|
||||
<td class="time">${MiniProfiler.getClientTimingByName(ClientTimings,"Response").Start}</td>
|
||||
<td class="time">${MiniProfiler.getClientTimingByName(ClientTimings,"Dom Complete").Start}</td>
|
||||
{{else}}
|
||||
<td colspan="3"></td>
|
||||
{{/if}}
|
||||
</tr>
|
||||
</script>
|
|
@ -0,0 +1 @@
|
|||
<script async type="text/javascript" id="mini-profiler" src="{path}includes.js?v={version}" data-version="{version}" data-path="{path}" data-current-id="{currentId}" data-ids="{ids}" data-position="{position}" data-trivial="{showTrivial}" data-children="{showChildren}" data-max-traces="{maxTracesToShow}" data-controls="{showControls}" data-authorized="{authorized}" data-toggle-shortcut="{toggleShortcut}" data-start-hidden="{startHidden}"></script>
|
|
@ -0,0 +1,11 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>{name} ({duration} ms) - Profiling Results</title>
|
||||
<script type='text/javascript' src='{path}jquery.1.7.1.js?v={version}'></script>
|
||||
<script type='text/javascript'> var profiler = {json}; </script>
|
||||
{includes}
|
||||
</head>
|
||||
<body>
|
||||
<div class='profiler-result-full'></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,65 @@
|
|||
module Rack
|
||||
class MiniProfiler
|
||||
class ClientSettings
|
||||
|
||||
COOKIE_NAME = "__profilin"
|
||||
|
||||
BACKTRACE_DEFAULT = nil
|
||||
BACKTRACE_FULL = 1
|
||||
BACKTRACE_NONE = 2
|
||||
|
||||
attr_accessor :disable_profiling
|
||||
attr_accessor :backtrace_level
|
||||
|
||||
|
||||
def initialize(env)
|
||||
request = ::Rack::Request.new(env)
|
||||
@cookie = request.cookies[COOKIE_NAME]
|
||||
if @cookie
|
||||
@cookie.split(",").map{|pair| pair.split("=")}.each do |k,v|
|
||||
@orig_disable_profiling = @disable_profiling = (v=='t') if k == "dp"
|
||||
@backtrace_level = v.to_i if k == "bt"
|
||||
end
|
||||
end
|
||||
|
||||
@backtrace_level = nil if !@backtrace_level.nil? && (@backtrace_level == 0 || @backtrace_level > BACKTRACE_NONE)
|
||||
@orig_backtrace_level = @backtrace_level
|
||||
|
||||
end
|
||||
|
||||
def write!(headers)
|
||||
if @orig_disable_profiling != @disable_profiling || @orig_backtrace_level != @backtrace_level || @cookie.nil?
|
||||
settings = {"p" => "t" }
|
||||
settings["dp"] = "t" if @disable_profiling
|
||||
settings["bt"] = @backtrace_level if @backtrace_level
|
||||
settings_string = settings.map{|k,v| "#{k}=#{v}"}.join(",")
|
||||
Rack::Utils.set_cookie_header!(headers, COOKIE_NAME, :value => settings_string, :path => '/')
|
||||
end
|
||||
end
|
||||
|
||||
def discard_cookie!(headers)
|
||||
Rack::Utils.delete_cookie_header!(headers, COOKIE_NAME, :path => '/')
|
||||
end
|
||||
|
||||
def has_cookie?
|
||||
!@cookie.nil?
|
||||
end
|
||||
|
||||
def disable_profiling?
|
||||
@disable_profiling
|
||||
end
|
||||
|
||||
def backtrace_full?
|
||||
@backtrace_level == BACKTRACE_FULL
|
||||
end
|
||||
|
||||
def backtrace_default?
|
||||
@backtrace_level == BACKTRACE_DEFAULT
|
||||
end
|
||||
|
||||
def backtrace_none?
|
||||
@backtrace_level == BACKTRACE_NONE
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,78 @@
|
|||
require 'mini_profiler/timer_struct'
|
||||
|
||||
module Rack
|
||||
class MiniProfiler
|
||||
|
||||
# This class holds the client timings
|
||||
class ClientTimerStruct < TimerStruct
|
||||
|
||||
def self.init_instrumentation
|
||||
"<script type=\"text/javascript\">mPt=function(){var t=[];return{t:t,probe:function(n){t.push({d:new Date(),n:n})}}}()</script>"
|
||||
end
|
||||
|
||||
def self.instrument(name,orig)
|
||||
probe = "<script>mPt.probe('#{name}')</script>"
|
||||
wrapped = probe
|
||||
wrapped << orig
|
||||
wrapped << probe
|
||||
wrapped
|
||||
end
|
||||
|
||||
|
||||
def initialize(env={})
|
||||
super
|
||||
end
|
||||
|
||||
def self.init_from_form_data(env, page_struct)
|
||||
timings = []
|
||||
clientTimes, clientPerf, baseTime = nil
|
||||
form = env['rack.request.form_hash']
|
||||
|
||||
clientPerf = form['clientPerformance'] if form
|
||||
clientTimes = clientPerf['timing'] if clientPerf
|
||||
|
||||
baseTime = clientTimes['navigationStart'].to_i if clientTimes
|
||||
return unless clientTimes && baseTime
|
||||
|
||||
probes = form['clientProbes']
|
||||
translated = {}
|
||||
if probes && !["null", ""].include?(probes)
|
||||
probes.each do |id, val|
|
||||
name = val["n"]
|
||||
translated[name] ||= {}
|
||||
if translated[name][:start]
|
||||
translated[name][:finish] = val["d"]
|
||||
else
|
||||
translated[name][:start] = val["d"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
translated.each do |name, data|
|
||||
h = {"Name" => name, "Start" => data[:start].to_i - baseTime}
|
||||
h["Duration"] = data[:finish].to_i - data[:start].to_i if data[:finish]
|
||||
timings.push(h)
|
||||
end
|
||||
|
||||
clientTimes.keys.find_all{|k| k =~ /Start$/ }.each do |k|
|
||||
start = clientTimes[k].to_i - baseTime
|
||||
finish = clientTimes[k.sub(/Start$/, "End")].to_i - baseTime
|
||||
duration = 0
|
||||
duration = finish - start if finish > start
|
||||
name = k.sub(/Start$/, "").split(/(?=[A-Z])/).map{|s| s.capitalize}.join(' ')
|
||||
timings.push({"Name" => name, "Start" => start, "Duration" => duration}) if start >= 0
|
||||
end
|
||||
|
||||
clientTimes.keys.find_all{|k| !(k =~ /(End|Start)$/)}.each do |k|
|
||||
timings.push("Name" => k, "Start" => clientTimes[k].to_i - baseTime, "Duration" => -1)
|
||||
end
|
||||
|
||||
rval = self.new
|
||||
rval['RedirectCount'] = env['rack.request.form_hash']['clientPerformance']['navigation']['redirectCount']
|
||||
rval['Timings'] = timings
|
||||
rval
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,65 @@
|
|||
module Rack
|
||||
class MiniProfiler
|
||||
class Config
|
||||
|
||||
def self.attr_accessor(*vars)
|
||||
@attributes ||= []
|
||||
@attributes.concat vars
|
||||
super(*vars)
|
||||
end
|
||||
|
||||
def self.attributes
|
||||
@attributes
|
||||
end
|
||||
|
||||
attr_accessor :authorization_mode, :auto_inject, :backtrace_ignores, :backtrace_includes, :backtrace_remove,
|
||||
:backtrace_threshold_ms, :base_url_path, :enabled, :flamegraph_sample_rate, :logger, :position,
|
||||
:pre_authorize_cb, :skip_paths, :skip_schema_queries, :start_hidden, :storage, :storage_failure,
|
||||
:storage_instance, :storage_options, :toggle_shortcut, :user_provider
|
||||
|
||||
# Deprecated options
|
||||
attr_accessor :use_existing_jquery
|
||||
|
||||
def self.default
|
||||
new.instance_eval {
|
||||
@auto_inject = true # automatically inject on every html page
|
||||
@base_url_path = "/mini-profiler-resources/"
|
||||
|
||||
# called prior to rack chain, to ensure we are allowed to profile
|
||||
@pre_authorize_cb = lambda {|env| true}
|
||||
|
||||
# called after rack chain, to ensure we are REALLY allowed to profile
|
||||
@position = 'left' # Where it is displayed
|
||||
@skip_schema_queries = false
|
||||
@storage = MiniProfiler::MemoryStore
|
||||
@user_provider = Proc.new{|env| Rack::Request.new(env).ip}
|
||||
@authorization_mode = :allow_all
|
||||
@toggle_shortcut = 'Alt+P'
|
||||
@start_hidden = false
|
||||
@backtrace_threshold_ms = 0
|
||||
@flamegraph_sample_rate = 0.5
|
||||
@storage_failure = Proc.new do |exception|
|
||||
if @logger
|
||||
@logger.warn("MiniProfiler storage failure: #{exception.message}")
|
||||
end
|
||||
end
|
||||
@enabled = true
|
||||
self
|
||||
}
|
||||
end
|
||||
|
||||
def merge!(config)
|
||||
return unless config
|
||||
if Hash === config
|
||||
config.each{|k,v| instance_variable_set "@#{k}",v}
|
||||
else
|
||||
self.class.attributes.each{ |k|
|
||||
v = config.send k
|
||||
instance_variable_set "@#{k}", v if v
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
class Rack::MiniProfiler::Context
|
||||
attr_accessor :inject_js,:current_timer,:page_struct,:skip_backtrace,:full_backtrace,:discard, :mpt_init, :measure
|
||||
|
||||
def initialize(opts = {})
|
||||
opts["measure"] = true unless opts.key? "measure"
|
||||
opts.each do |k,v|
|
||||
self.instance_variable_set('@' + k, v)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
require 'mini_profiler/timer_struct'
|
||||
|
||||
module Rack
|
||||
class MiniProfiler
|
||||
|
||||
# Timing system for a custom timers such as cache, redis, RPC, external API
|
||||
# calls, etc.
|
||||
class CustomTimerStruct < TimerStruct
|
||||
def initialize(type, duration_ms, page, parent)
|
||||
@parent = parent
|
||||
@page = page
|
||||
@type = type
|
||||
|
||||
super("Type" => type,
|
||||
"StartMilliseconds" => ((Time.now.to_f * 1000).to_i - page['Started']) - duration_ms,
|
||||
"DurationMilliseconds" => duration_ms,
|
||||
"ParentTimingId" => nil)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,181 @@
|
|||
class Rack::MiniProfiler::GCProfiler
|
||||
|
||||
def initialize
|
||||
@ignore = []
|
||||
@ignore << @ignore.__id__
|
||||
end
|
||||
|
||||
def object_space_stats
|
||||
stats = {}
|
||||
ids = {}
|
||||
|
||||
@ignore << stats.__id__
|
||||
@ignore << ids.__id__
|
||||
|
||||
i=0
|
||||
ObjectSpace.each_object { |o|
|
||||
begin
|
||||
i = stats[o.class] || 0
|
||||
i += 1
|
||||
stats[o.class] = i
|
||||
ids[o.__id__] = o if Integer === o.__id__
|
||||
rescue NoMethodError
|
||||
# protect against BasicObject
|
||||
end
|
||||
}
|
||||
|
||||
@ignore.each do |id|
|
||||
if ids.delete(id)
|
||||
klass = ObjectSpace._id2ref(id).class
|
||||
stats[klass] -= 1
|
||||
end
|
||||
end
|
||||
|
||||
result = {:stats => stats, :ids => ids}
|
||||
@ignore << result.__id__
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def diff_object_stats(before,after)
|
||||
diff = {}
|
||||
after.each do |k,v|
|
||||
diff[k] = v - (before[k] || 0)
|
||||
end
|
||||
before.each do |k,v|
|
||||
diff[k] = 0 - v unless after[k]
|
||||
end
|
||||
|
||||
diff
|
||||
end
|
||||
|
||||
def analyze_strings(ids_before,ids_after)
|
||||
result = {}
|
||||
ids_after.each do |id,_|
|
||||
obj = ObjectSpace._id2ref(id)
|
||||
if String === obj && !ids_before.include?(obj.object_id)
|
||||
result[obj] ||= 0
|
||||
result[obj] += 1
|
||||
end
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def analyze_growth(ids_before, ids_after)
|
||||
new_objects = 0
|
||||
memory_allocated = 0
|
||||
|
||||
ids_after.each do |id,_|
|
||||
if !ids_before.include?(id) && obj=ObjectSpace._id2ref(id)
|
||||
# this is going to be version specific (may change in 2.1)
|
||||
size = ObjectSpace.memsize_of(obj)
|
||||
memory_allocated += size
|
||||
new_objects += 1
|
||||
end
|
||||
end
|
||||
|
||||
[new_objects, memory_allocated]
|
||||
end
|
||||
|
||||
def analyze_initial_state(ids_before)
|
||||
memory_allocated = 0
|
||||
objects = 0
|
||||
|
||||
ids_before.each do |id,_|
|
||||
if obj=ObjectSpace._id2ref(id)
|
||||
# this is going to be version specific (may change in 2.1)
|
||||
memory_allocated += ObjectSpace.memsize_of(obj)
|
||||
objects += 1
|
||||
end
|
||||
end
|
||||
|
||||
[objects,memory_allocated]
|
||||
end
|
||||
|
||||
def profile_gc_time(app,env)
|
||||
body = []
|
||||
|
||||
begin
|
||||
GC::Profiler.clear
|
||||
prev_profiler_state = GC::Profiler.enabled?
|
||||
prev_gc_state = GC.enable
|
||||
GC::Profiler.enable
|
||||
b = app.call(env)[2]
|
||||
b.close if b.respond_to? :close
|
||||
body << "GC Profiler ran during this request, if it fired you will see the cost below:\n\n"
|
||||
body << GC::Profiler.result
|
||||
ensure
|
||||
prev_gc_state ? GC.disable : GC.enable
|
||||
GC::Profiler.disable unless prev_profiler_state
|
||||
end
|
||||
|
||||
return [200, {'Content-Type' => 'text/plain'}, body]
|
||||
end
|
||||
|
||||
def profile_gc(app,env)
|
||||
|
||||
# for memsize_of
|
||||
require 'objspace'
|
||||
|
||||
body = [];
|
||||
|
||||
stat_before,stat_after,diff,string_analysis,
|
||||
new_objects, memory_allocated, stat, memory_before, objects_before = nil
|
||||
|
||||
# clean up before
|
||||
GC.start
|
||||
stat = GC.stat
|
||||
prev_gc_state = GC.disable
|
||||
stat_before = object_space_stats
|
||||
b = app.call(env)[2]
|
||||
b.close if b.respond_to? :close
|
||||
stat_after = object_space_stats
|
||||
# so we don't blow out on memory
|
||||
prev_gc_state ? GC.disable : GC.enable
|
||||
|
||||
diff = diff_object_stats(stat_before[:stats],stat_after[:stats])
|
||||
string_analysis = analyze_strings(stat_before[:ids], stat_after[:ids])
|
||||
new_objects, memory_allocated = analyze_growth(stat_before[:ids], stat_after[:ids])
|
||||
objects_before, memory_before = analyze_initial_state(stat_before[:ids])
|
||||
|
||||
|
||||
body << "
|
||||
Overview
|
||||
------------------------------------
|
||||
Initial state: object count - #{objects_before} , memory allocated outside heap (bytes) #{memory_before}
|
||||
|
||||
GC Stats: #{stat.map{|k,v| "#{k} : #{v}" }.join(", ")}
|
||||
|
||||
New bytes allocated outside of Ruby heaps: #{memory_allocated}
|
||||
New objects: #{new_objects}
|
||||
"
|
||||
|
||||
body << "
|
||||
ObjectSpace delta caused by request:
|
||||
--------------------------------------------\n"
|
||||
diff.to_a.reject{|k,v| v == 0}.sort{|x,y| y[1] <=> x[1]}.each do |k,v|
|
||||
body << "#{k} : #{v}\n" if v != 0
|
||||
end
|
||||
|
||||
body << "\n
|
||||
ObjectSpace stats:
|
||||
-----------------\n"
|
||||
|
||||
stat_after[:stats].to_a.sort{|x,y| y[1] <=> x[1]}.each do |k,v|
|
||||
body << "#{k} : #{v}\n"
|
||||
end
|
||||
|
||||
|
||||
body << "\n
|
||||
String stats:
|
||||
------------\n"
|
||||
|
||||
string_analysis.to_a.sort{|x,y| y[1] <=> x[1] }.take(1000).each do |string,count|
|
||||
body << "#{count} : #{string}\n"
|
||||
end
|
||||
|
||||
return [200, {'Content-Type' => 'text/plain'}, body]
|
||||
ensure
|
||||
prev_gc_state ? GC.disable : GC.enable
|
||||
end
|
||||
end
|
|
@ -0,0 +1,58 @@
|
|||
require 'mini_profiler/timer_struct'
|
||||
|
||||
module Rack
|
||||
class MiniProfiler
|
||||
|
||||
# PageTimerStruct
|
||||
# Root: RequestTimer
|
||||
# :has_many RequestTimer children
|
||||
# :has_many SqlTimer children
|
||||
# :has_many CustomTimer children
|
||||
class PageTimerStruct < TimerStruct
|
||||
def initialize(env)
|
||||
super("Id" => MiniProfiler.generate_id,
|
||||
"Name" => env['PATH_INFO'],
|
||||
"Started" => (Time.now.to_f * 1000).to_i,
|
||||
"MachineName" => env['SERVER_NAME'],
|
||||
"Level" => 0,
|
||||
"User" => "unknown user",
|
||||
"HasUserViewed" => false,
|
||||
"ClientTimings" => nil,
|
||||
"DurationMilliseconds" => 0,
|
||||
"HasTrivialTimings" => true,
|
||||
"HasAllTrivialTimigs" => false,
|
||||
"TrivialDurationThresholdMilliseconds" => 2,
|
||||
"Head" => nil,
|
||||
"DurationMillisecondsInSql" => 0,
|
||||
"HasSqlTimings" => true,
|
||||
"HasDuplicateSqlTimings" => false,
|
||||
"ExecutedReaders" => 0,
|
||||
"ExecutedScalars" => 0,
|
||||
"ExecutedNonQueries" => 0,
|
||||
"CustomTimingNames" => [],
|
||||
"CustomTimingStats" => {}
|
||||
)
|
||||
name = "#{env['REQUEST_METHOD']} http://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
|
||||
self['Root'] = RequestTimerStruct.createRoot(name, self)
|
||||
end
|
||||
|
||||
def duration_ms
|
||||
@attributes['Root']['DurationMilliseconds']
|
||||
end
|
||||
|
||||
def root
|
||||
@attributes['Root']
|
||||
end
|
||||
|
||||
def to_json(*a)
|
||||
attribs = @attributes.merge(
|
||||
"Started" => '/Date(%d)/' % @attributes['Started'],
|
||||
"DurationMilliseconds" => @attributes['Root']['DurationMilliseconds'],
|
||||
"CustomTimingNames" => @attributes['CustomTimingStats'].keys.sort
|
||||
)
|
||||
::JSON.generate(attribs, :max_nesting => 100)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,567 @@
|
|||
require 'json'
|
||||
require 'timeout'
|
||||
require 'thread'
|
||||
|
||||
require 'mini_profiler/version'
|
||||
require 'mini_profiler/page_timer_struct'
|
||||
require 'mini_profiler/sql_timer_struct'
|
||||
require 'mini_profiler/custom_timer_struct'
|
||||
require 'mini_profiler/client_timer_struct'
|
||||
require 'mini_profiler/request_timer_struct'
|
||||
require 'mini_profiler/storage/abstract_store'
|
||||
require 'mini_profiler/storage/memcache_store'
|
||||
require 'mini_profiler/storage/memory_store'
|
||||
require 'mini_profiler/storage/redis_store'
|
||||
require 'mini_profiler/storage/file_store'
|
||||
require 'mini_profiler/config'
|
||||
require 'mini_profiler/profiling_methods'
|
||||
require 'mini_profiler/context'
|
||||
require 'mini_profiler/client_settings'
|
||||
require 'mini_profiler/gc_profiler'
|
||||
# TODO
|
||||
# require 'mini_profiler/gc_profiler_ruby_head' if Gem::Version.new('2.1.0') <= Gem::Version.new(RUBY_VERSION)
|
||||
|
||||
module Rack
|
||||
|
||||
class MiniProfiler
|
||||
|
||||
class << self
|
||||
|
||||
include Rack::MiniProfiler::ProfilingMethods
|
||||
|
||||
def generate_id
|
||||
rand(36**20).to_s(36)
|
||||
end
|
||||
|
||||
def reset_config
|
||||
@config = Config.default
|
||||
end
|
||||
|
||||
# So we can change the configuration if we want
|
||||
def config
|
||||
@config ||= Config.default
|
||||
end
|
||||
|
||||
def share_template
|
||||
return @share_template unless @share_template.nil?
|
||||
@share_template = ::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__)))
|
||||
end
|
||||
|
||||
def current
|
||||
Thread.current[:mini_profiler_private]
|
||||
end
|
||||
|
||||
def current=(c)
|
||||
# we use TLS cause we need access to this from sql blocks and code blocks that have no access to env
|
||||
Thread.current[:mini_profiler_private]= c
|
||||
end
|
||||
|
||||
# discard existing results, don't track this request
|
||||
def discard_results
|
||||
self.current.discard = true if current
|
||||
end
|
||||
|
||||
def create_current(env={}, options={})
|
||||
# profiling the request
|
||||
self.current = Context.new
|
||||
self.current.inject_js = config.auto_inject && (!env['HTTP_X_REQUESTED_WITH'].eql? 'XMLHttpRequest')
|
||||
self.current.page_struct = PageTimerStruct.new(env)
|
||||
self.current.current_timer = current.page_struct['Root']
|
||||
end
|
||||
|
||||
def authorize_request
|
||||
Thread.current[:mp_authorized] = true
|
||||
end
|
||||
|
||||
def deauthorize_request
|
||||
Thread.current[:mp_authorized] = nil
|
||||
end
|
||||
|
||||
def request_authorized?
|
||||
Thread.current[:mp_authorized]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
#
|
||||
# options:
|
||||
# :auto_inject - should script be automatically injected on every html page (not xhr)
|
||||
def initialize(app, config = nil)
|
||||
MiniProfiler.config.merge!(config)
|
||||
@config = MiniProfiler.config
|
||||
@app = app
|
||||
@config.base_url_path << "/" unless @config.base_url_path.end_with? "/"
|
||||
unless @config.storage_instance
|
||||
@config.storage_instance = @config.storage.new(@config.storage_options)
|
||||
end
|
||||
@storage = @config.storage_instance
|
||||
end
|
||||
|
||||
def user(env)
|
||||
@config.user_provider.call(env)
|
||||
end
|
||||
|
||||
def serve_results(env)
|
||||
request = Rack::Request.new(env)
|
||||
id = request['id']
|
||||
page_struct = @storage.load(id)
|
||||
unless page_struct
|
||||
@storage.set_viewed(user(env), id)
|
||||
id = ERB::Util.html_escape(request['id'])
|
||||
user_info = ERB::Util.html_escape(user(env))
|
||||
return [404, {}, ["Request not found: #{id} - user #{user_info}"]]
|
||||
end
|
||||
unless page_struct['HasUserViewed']
|
||||
page_struct['ClientTimings'] = ClientTimerStruct.init_from_form_data(env, page_struct)
|
||||
page_struct['HasUserViewed'] = true
|
||||
@storage.save(page_struct)
|
||||
@storage.set_viewed(user(env), id)
|
||||
end
|
||||
|
||||
result_json = page_struct.to_json
|
||||
# If we're an XMLHttpRequest, serve up the contents as JSON
|
||||
if request.xhr?
|
||||
[200, { 'Content-Type' => 'application/json'}, [result_json]]
|
||||
else
|
||||
|
||||
# Otherwise give the HTML back
|
||||
html = MiniProfiler.share_template.dup
|
||||
html.gsub!(/\{path\}/, "#{env['SCRIPT_NAME']}#{@config.base_url_path}")
|
||||
html.gsub!(/\{version\}/, MiniProfiler::VERSION)
|
||||
html.gsub!(/\{json\}/, result_json)
|
||||
html.gsub!(/\{includes\}/, get_profile_script(env))
|
||||
html.gsub!(/\{name\}/, page_struct['Name'])
|
||||
html.gsub!(/\{duration\}/, "%.1f" % page_struct.duration_ms)
|
||||
|
||||
[200, {'Content-Type' => 'text/html'}, [html]]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def serve_html(env)
|
||||
file_name = env['PATH_INFO'][(@config.base_url_path.length)..1000]
|
||||
|
||||
return serve_results(env) if file_name.eql?('results')
|
||||
|
||||
full_path = ::File.expand_path("../html/#{file_name}", ::File.dirname(__FILE__))
|
||||
return [404, {}, ["Not found"]] unless ::File.exists? full_path
|
||||
f = Rack::File.new nil
|
||||
f.path = full_path
|
||||
|
||||
begin
|
||||
f.cache_control = "max-age:86400"
|
||||
f.serving env
|
||||
rescue
|
||||
# old versions of rack have a different api
|
||||
status, headers, body = f.serving
|
||||
headers.merge! 'Cache-Control' => "max-age:86400"
|
||||
[status, headers, body]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
def current
|
||||
MiniProfiler.current
|
||||
end
|
||||
|
||||
def current=(c)
|
||||
MiniProfiler.current=c
|
||||
end
|
||||
|
||||
|
||||
def config
|
||||
@config
|
||||
end
|
||||
|
||||
|
||||
def call(env)
|
||||
|
||||
client_settings = ClientSettings.new(env)
|
||||
|
||||
status = headers = body = nil
|
||||
query_string = env['QUERY_STRING']
|
||||
path = env['PATH_INFO']
|
||||
|
||||
skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env)) ||
|
||||
(@config.skip_paths && @config.skip_paths.any?{ |p| path[0,p.length] == p}) ||
|
||||
query_string =~ /pp=skip/
|
||||
|
||||
has_profiling_cookie = client_settings.has_cookie?
|
||||
|
||||
if skip_it || (@config.authorization_mode == :whitelist && !has_profiling_cookie)
|
||||
status,headers,body = @app.call(env)
|
||||
if !skip_it && @config.authorization_mode == :whitelist && !has_profiling_cookie && MiniProfiler.request_authorized?
|
||||
client_settings.write!(headers)
|
||||
end
|
||||
return [status,headers,body]
|
||||
end
|
||||
|
||||
# handle all /mini-profiler requests here
|
||||
return serve_html(env) if path.start_with? @config.base_url_path
|
||||
|
||||
has_disable_cookie = client_settings.disable_profiling?
|
||||
# manual session disable / enable
|
||||
if query_string =~ /pp=disable/ || has_disable_cookie
|
||||
skip_it = true
|
||||
end
|
||||
|
||||
if query_string =~ /pp=enable/ && (@config.authorization_mode != :whitelist || MiniProfiler.request_authorized?)
|
||||
skip_it = false
|
||||
config.enabled = true
|
||||
end
|
||||
|
||||
if skip_it || !config.enabled
|
||||
status,headers,body = @app.call(env)
|
||||
client_settings.disable_profiling = true
|
||||
client_settings.write!(headers)
|
||||
return [status,headers,body]
|
||||
else
|
||||
client_settings.disable_profiling = false
|
||||
end
|
||||
|
||||
if query_string =~ /pp=profile-gc/
|
||||
current.measure = false if current
|
||||
|
||||
if query_string =~ /pp=profile-gc-time/
|
||||
return Rack::MiniProfiler::GCProfiler.new.profile_gc_time(@app, env)
|
||||
elsif query_string =~ /pp=profile-gc-ruby-head/
|
||||
result = StringIO.new
|
||||
report = MemoryProfiler.report do
|
||||
_,_,body = @app.call(env)
|
||||
body.close if body.respond_to? :close
|
||||
end
|
||||
report.pretty_print(result)
|
||||
return text_result(result.string)
|
||||
else
|
||||
return Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env)
|
||||
end
|
||||
end
|
||||
|
||||
MiniProfiler.create_current(env, @config)
|
||||
MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
|
||||
|
||||
if query_string =~ /pp=normal-backtrace/
|
||||
client_settings.backtrace_level = ClientSettings::BACKTRACE_DEFAULT
|
||||
elsif query_string =~ /pp=no-backtrace/
|
||||
current.skip_backtrace = true
|
||||
client_settings.backtrace_level = ClientSettings::BACKTRACE_NONE
|
||||
elsif query_string =~ /pp=full-backtrace/ || client_settings.backtrace_full?
|
||||
current.full_backtrace = true
|
||||
client_settings.backtrace_level = ClientSettings::BACKTRACE_FULL
|
||||
elsif client_settings.backtrace_none?
|
||||
current.skip_backtrace = true
|
||||
end
|
||||
|
||||
flamegraph = nil
|
||||
|
||||
trace_exceptions = query_string =~ /pp=trace-exceptions/ && defined? TracePoint
|
||||
status, headers, body, exceptions,trace = nil
|
||||
|
||||
start = Time.now
|
||||
|
||||
if trace_exceptions
|
||||
exceptions = []
|
||||
trace = TracePoint.new(:raise) do |tp|
|
||||
exceptions << tp.raised_exception
|
||||
end
|
||||
trace.enable
|
||||
end
|
||||
|
||||
begin
|
||||
|
||||
# Strip all the caching headers so we don't get 304s back
|
||||
# This solves a very annoying bug where rack mini profiler never shows up
|
||||
env['HTTP_IF_MODIFIED_SINCE'] = ''
|
||||
env['HTTP_IF_NONE_MATCH'] = ''
|
||||
|
||||
if query_string =~ /pp=flamegraph/
|
||||
unless defined?(Flamegraph) && Flamegraph.respond_to?(:generate)
|
||||
|
||||
flamegraph = "Please install the flamegraph gem and require it: add gem 'flamegraph' to your Gemfile"
|
||||
status,headers,body = @app.call(env)
|
||||
else
|
||||
# do not sully our profile with mini profiler timings
|
||||
current.measure = false
|
||||
match_data = query_string.match(/flamegraph_sample_rate=([\d\.]+)/)
|
||||
|
||||
mode = query_string =~ /mode=c/ ? :c : :ruby
|
||||
|
||||
if match_data && !match_data[1].to_f.zero?
|
||||
sample_rate = match_data[1].to_f
|
||||
else
|
||||
sample_rate = config.flamegraph_sample_rate
|
||||
end
|
||||
flamegraph = Flamegraph.generate(nil, :fidelity => sample_rate, :embed_resources => query_string =~ /embed/, :mode => mode) do
|
||||
status,headers,body = @app.call(env)
|
||||
end
|
||||
end
|
||||
else
|
||||
status,headers,body = @app.call(env)
|
||||
end
|
||||
client_settings.write!(headers)
|
||||
ensure
|
||||
trace.disable if trace
|
||||
end
|
||||
|
||||
skip_it = current.discard
|
||||
|
||||
if (config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
|
||||
# this is non-obvious, don't kill the profiling cookie on errors or short requests
|
||||
# this ensures that stuff that never reaches the rails stack does not kill profiling
|
||||
if status == 200 && ((Time.now - start) > 0.1)
|
||||
client_settings.discard_cookie!(headers)
|
||||
end
|
||||
skip_it = true
|
||||
end
|
||||
|
||||
return [status,headers,body] if skip_it
|
||||
|
||||
# we must do this here, otherwise current[:discard] is not being properly treated
|
||||
if trace_exceptions
|
||||
body.close if body.respond_to? :close
|
||||
return dump_exceptions exceptions
|
||||
end
|
||||
|
||||
if query_string =~ /pp=env/
|
||||
body.close if body.respond_to? :close
|
||||
return dump_env env
|
||||
end
|
||||
|
||||
if query_string =~ /pp=help/
|
||||
body.close if body.respond_to? :close
|
||||
return help(client_settings)
|
||||
end
|
||||
|
||||
page_struct = current.page_struct
|
||||
page_struct['User'] = user(env)
|
||||
page_struct['Root'].record_time((Time.now - start) * 1000)
|
||||
|
||||
if flamegraph
|
||||
body.close if body.respond_to? :close
|
||||
return self.flamegraph(flamegraph)
|
||||
end
|
||||
|
||||
|
||||
begin
|
||||
# no matter what it is, it should be unviewed, otherwise we will miss POST
|
||||
@storage.set_unviewed(page_struct['User'], page_struct['Id'])
|
||||
@storage.save(page_struct)
|
||||
|
||||
# inject headers, script
|
||||
if headers['Content-Type'] && status == 200
|
||||
client_settings.write!(headers)
|
||||
result = inject_profiler(env,status,headers,body)
|
||||
return result if result
|
||||
end
|
||||
rescue Exception => e
|
||||
if @config.storage_failure != nil
|
||||
@config.storage_failure.call(e)
|
||||
end
|
||||
end
|
||||
|
||||
client_settings.write!(headers)
|
||||
[status, headers, body]
|
||||
|
||||
ensure
|
||||
# Make sure this always happens
|
||||
self.current = nil
|
||||
end
|
||||
|
||||
def inject_profiler(env,status,headers,body)
|
||||
# mini profiler is meddling with stuff, we can not cache cause we will get incorrect data
|
||||
# Rack::ETag has already inserted some nonesense in the chain
|
||||
content_type = headers['Content-Type']
|
||||
|
||||
headers.delete('ETag')
|
||||
headers.delete('Date')
|
||||
headers['Cache-Control'] = 'no-store, must-revalidate, private, max-age=0'
|
||||
|
||||
# inject header
|
||||
if headers.is_a? Hash
|
||||
headers['X-MiniProfiler-Ids'] = ids_json(env)
|
||||
end
|
||||
|
||||
if current.inject_js && content_type =~ /text\/html/
|
||||
response = Rack::Response.new([], status, headers)
|
||||
script = self.get_profile_script(env)
|
||||
|
||||
if String === body
|
||||
response.write inject(body,script)
|
||||
else
|
||||
body.each { |fragment| response.write inject(fragment, script) }
|
||||
end
|
||||
body.close if body.respond_to? :close
|
||||
response.finish
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def inject(fragment, script)
|
||||
if fragment.match(/<\/body>/i)
|
||||
# explicit </body>
|
||||
|
||||
regex = /<\/body>/i
|
||||
close_tag = '</body>'
|
||||
elsif fragment.match(/<\/html>/i)
|
||||
# implicit </body>
|
||||
|
||||
regex = /<\/html>/i
|
||||
close_tag = '</html>'
|
||||
else
|
||||
# implicit </body> and </html>. Don't do anything.
|
||||
|
||||
return fragment
|
||||
end
|
||||
|
||||
matches = fragment.scan(regex).length
|
||||
index = 1
|
||||
fragment.gsub(regex) do
|
||||
# though malformed there is an edge case where /body exists earlier in the html, work around
|
||||
if index < matches
|
||||
index += 1
|
||||
close_tag
|
||||
else
|
||||
|
||||
# if for whatever crazy reason we dont get a utf string,
|
||||
# just force the encoding, no utf in the mp scripts anyway
|
||||
if script.respond_to?(:encoding) && script.respond_to?(:force_encoding)
|
||||
(script + close_tag).force_encoding(fragment.encoding)
|
||||
else
|
||||
script + close_tag
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def dump_exceptions(exceptions)
|
||||
headers = {'Content-Type' => 'text/plain'}
|
||||
body = "Exceptions (#{exceptions.length} raised during request)\n\n"
|
||||
exceptions.each do |e|
|
||||
body << "#{e.class} #{e.message}\n#{e.backtrace.join("\n")}\n\n\n\n"
|
||||
end
|
||||
|
||||
[200, headers, [body]]
|
||||
end
|
||||
|
||||
def dump_env(env)
|
||||
body = "Rack Environment\n---------------\n"
|
||||
env.each do |k,v|
|
||||
body << "#{k}: #{v}\n"
|
||||
end
|
||||
|
||||
body << "\n\nEnvironment\n---------------\n"
|
||||
ENV.each do |k,v|
|
||||
body << "#{k}: #{v}\n"
|
||||
end
|
||||
|
||||
body << "\n\nRuby Version\n---------------\n"
|
||||
body << "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL}\n"
|
||||
|
||||
body << "\n\nInternals\n---------------\n"
|
||||
body << "Storage Provider #{config.storage_instance}\n"
|
||||
body << "User #{user(env)}\n"
|
||||
body << config.storage_instance.diagnostics(user(env)) rescue "no diagnostics implemented for storage"
|
||||
|
||||
text_result(body)
|
||||
end
|
||||
|
||||
def text_result(body)
|
||||
headers = {'Content-Type' => 'text/plain'}
|
||||
[200, headers, [body]]
|
||||
end
|
||||
|
||||
def help(client_settings)
|
||||
headers = {'Content-Type' => 'text/plain'}
|
||||
body = "Append the following to your query string:
|
||||
|
||||
pp=help : display this screen
|
||||
pp=env : display the rack environment
|
||||
pp=skip : skip mini profiler for this request
|
||||
pp=no-backtrace #{"(*) " if client_settings.backtrace_none?}: don't collect stack traces from all the SQL executed (sticky, use pp=normal-backtrace to enable)
|
||||
pp=normal-backtrace #{"(*) " if client_settings.backtrace_default?}: collect stack traces from all the SQL executed and filter normally
|
||||
pp=full-backtrace #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use pp=normal-backtrace to disable)
|
||||
pp=disable : disable profiling for this session
|
||||
pp=enable : enable profiling for this session (if previously disabled)
|
||||
pp=profile-gc: perform gc profiling on this request, analyzes ObjectSpace generated by request (ruby 1.9.3 only)
|
||||
pp=profile-gc-time: perform built-in gc profiling on this request (ruby 1.9.3 only)
|
||||
pp=profile-gc-ruby-head: requires the memory_profiler gem, new location based report
|
||||
pp=flamegraph: works best on Ruby 2.0, a graph representing sampled activity (requires the flamegraph gem).
|
||||
pp=flamegraph&flamegraph_sample_rate=1: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
|
||||
pp=flamegraph_embed: works best on Ruby 2.0, a graph representing sampled activity (requires the flamegraph gem), embedded resources for use on an intranet.
|
||||
pp=trace-exceptions: requires Ruby 2.0, will return all the spots where your application raises execptions
|
||||
"
|
||||
|
||||
client_settings.write!(headers)
|
||||
[200, headers, [body]]
|
||||
end
|
||||
|
||||
def flamegraph(graph)
|
||||
headers = {'Content-Type' => 'text/html'}
|
||||
[200, headers, [graph]]
|
||||
end
|
||||
|
||||
def ids(env)
|
||||
# cap at 10 ids, otherwise there is a chance you can blow the header
|
||||
([current.page_struct["Id"]] + (@storage.get_unviewed_ids(user(env)) || [])[0..8]).uniq
|
||||
end
|
||||
|
||||
def ids_json(env)
|
||||
::JSON.generate(ids(env))
|
||||
end
|
||||
|
||||
def ids_comma_separated(env)
|
||||
ids(env).join(",")
|
||||
end
|
||||
|
||||
# get_profile_script returns script to be injected inside current html page
|
||||
# By default, profile_script is appended to the end of all html requests automatically.
|
||||
# Calling get_profile_script cancels automatic append for the current page
|
||||
# Use it when:
|
||||
# * you have disabled auto append behaviour throught :auto_inject => false flag
|
||||
# * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
|
||||
def get_profile_script(env)
|
||||
|
||||
settings = {
|
||||
:path => "#{env['SCRIPT_NAME']}#{@config.base_url_path}",
|
||||
:version => MiniProfiler::VERSION,
|
||||
:position => @config.position,
|
||||
:showTrivial => false,
|
||||
:showChildren => false,
|
||||
:maxTracesToShow => 10,
|
||||
:showControls => false,
|
||||
:authorized => true,
|
||||
:toggleShortcut => @config.toggle_shortcut,
|
||||
:startHidden => @config.start_hidden
|
||||
}
|
||||
|
||||
if current && current.page_struct
|
||||
settings[:ids] = ids_comma_separated(env)
|
||||
settings[:currentId] = current.page_struct["Id"]
|
||||
else
|
||||
settings[:ids] = []
|
||||
settings[:currentId] = ""
|
||||
end
|
||||
|
||||
# TODO : cache this snippet
|
||||
script = IO.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
|
||||
# replace the variables
|
||||
settings.each do |k,v|
|
||||
regex = Regexp.new("\\{#{k.to_s}\\}")
|
||||
script.gsub!(regex, v.to_s)
|
||||
end
|
||||
|
||||
current.inject_js = false if current
|
||||
script
|
||||
end
|
||||
|
||||
# cancels automatic injection of profile script for the current page
|
||||
def cancel_auto_inject(env)
|
||||
current.inject_js = false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
module Rack
|
||||
class MiniProfiler
|
||||
module ProfilingMethods
|
||||
|
||||
def record_sql(query, elapsed_ms)
|
||||
c = current
|
||||
return unless c
|
||||
c.current_timer.add_sql(query, elapsed_ms, c.page_struct, c.skip_backtrace, c.full_backtrace) if (c && c.current_timer)
|
||||
end
|
||||
|
||||
def start_step(name)
|
||||
if current
|
||||
parent_timer = current.current_timer
|
||||
current.current_timer = current_timer = current.current_timer.add_child(name)
|
||||
[current_timer,parent_timer]
|
||||
end
|
||||
end
|
||||
|
||||
def finish_step(obj)
|
||||
if obj && current
|
||||
current_timer, parent_timer = obj
|
||||
current_timer.record_time
|
||||
current.current_timer = parent_timer
|
||||
end
|
||||
end
|
||||
|
||||
# perform a profiling step on given block
|
||||
def step(name, opts = nil)
|
||||
if current
|
||||
parent_timer = current.current_timer
|
||||
result = nil
|
||||
current.current_timer = current_timer = current.current_timer.add_child(name)
|
||||
begin
|
||||
result = yield if block_given?
|
||||
ensure
|
||||
current_timer.record_time
|
||||
current.current_timer = parent_timer
|
||||
end
|
||||
else
|
||||
yield if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
def unprofile_method(klass, method)
|
||||
|
||||
clean = clean_method_name(method)
|
||||
|
||||
with_profiling = ("#{clean}_with_mini_profiler").intern
|
||||
without_profiling = ("#{clean}_without_mini_profiler").intern
|
||||
|
||||
if klass.send :method_defined?, with_profiling
|
||||
klass.send :alias_method, method, without_profiling
|
||||
klass.send :remove_method, with_profiling
|
||||
klass.send :remove_method, without_profiling
|
||||
end
|
||||
end
|
||||
|
||||
def counter_method(klass, method, &blk)
|
||||
self.profile_method(klass, method, :counter, &blk)
|
||||
end
|
||||
|
||||
def uncounter_method(klass, method)
|
||||
self.unprofile_method(klass, method)
|
||||
end
|
||||
|
||||
def profile_method(klass, method, type = :profile, &blk)
|
||||
default_name = type==:counter ? method.to_s : klass.to_s + " " + method.to_s
|
||||
clean = clean_method_name(method)
|
||||
|
||||
with_profiling = ("#{clean}_with_mini_profiler").intern
|
||||
without_profiling = ("#{clean}_without_mini_profiler").intern
|
||||
|
||||
if klass.send :method_defined?, with_profiling
|
||||
return # dont double profile
|
||||
end
|
||||
|
||||
klass.send :alias_method, without_profiling, method
|
||||
klass.send :define_method, with_profiling do |*args, &orig|
|
||||
return self.send without_profiling, *args, &orig unless Rack::MiniProfiler.current
|
||||
|
||||
name = default_name
|
||||
if blk
|
||||
name =
|
||||
if respond_to?(:instance_exec)
|
||||
instance_exec(*args, &blk)
|
||||
else
|
||||
# deprecated in Rails 4.x
|
||||
blk.bind(self).call(*args)
|
||||
end
|
||||
end
|
||||
|
||||
result = nil
|
||||
parent_timer = Rack::MiniProfiler.current.current_timer
|
||||
|
||||
if type == :counter
|
||||
start = Time.now
|
||||
begin
|
||||
result = self.send without_profiling, *args, &orig
|
||||
ensure
|
||||
duration_ms = (Time.now - start).to_f * 1000
|
||||
parent_timer.add_custom(name, duration_ms, Rack::MiniProfiler.current.page_struct )
|
||||
end
|
||||
else
|
||||
page_struct = Rack::MiniProfiler.current.page_struct
|
||||
|
||||
Rack::MiniProfiler.current.current_timer = current_timer = parent_timer.add_child(name)
|
||||
begin
|
||||
result = self.send without_profiling, *args, &orig
|
||||
ensure
|
||||
current_timer.record_time
|
||||
Rack::MiniProfiler.current.current_timer = parent_timer
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
klass.send :alias_method, method, with_profiling
|
||||
end
|
||||
|
||||
# Add a custom timing. These are displayed similar to SQL/query time in
|
||||
# columns expanding to the right.
|
||||
#
|
||||
# type - String counter type. Each distinct type gets its own column.
|
||||
# duration_ms - Duration of the call in ms. Either this or a block must be
|
||||
# given but not both.
|
||||
#
|
||||
# When a block is given, calculate the duration by yielding to the block
|
||||
# and keeping a record of its run time.
|
||||
#
|
||||
# Returns the result of the block, or nil when no block is given.
|
||||
def counter(type, duration_ms=nil)
|
||||
result = nil
|
||||
if block_given?
|
||||
start = Time.now
|
||||
result = yield
|
||||
duration_ms = (Time.now - start).to_f * 1000
|
||||
end
|
||||
return result if current.nil? || !request_authorized?
|
||||
current.current_timer.add_custom(type, duration_ms, current.page_struct)
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clean_method_name(method)
|
||||
method.to_s.gsub(/[\?\!]/, "")
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,115 @@
|
|||
require 'mini_profiler/timer_struct'
|
||||
|
||||
module Rack
|
||||
class MiniProfiler
|
||||
|
||||
class RequestTimerStruct < TimerStruct
|
||||
|
||||
def self.createRoot(name, page)
|
||||
rt = RequestTimerStruct.new(name, page, nil)
|
||||
rt["IsRoot"]= true
|
||||
rt
|
||||
end
|
||||
|
||||
attr_accessor :children_duration
|
||||
|
||||
def initialize(name, page, parent)
|
||||
super("Id" => MiniProfiler.generate_id,
|
||||
"Name" => name,
|
||||
"DurationMilliseconds" => 0,
|
||||
"DurationWithoutChildrenMilliseconds"=> 0,
|
||||
"StartMilliseconds" => (Time.now.to_f * 1000).to_i - page['Started'],
|
||||
"ParentTimingId" => nil,
|
||||
"Children" => [],
|
||||
"HasChildren"=> false,
|
||||
"KeyValues" => nil,
|
||||
"HasSqlTimings"=> false,
|
||||
"HasDuplicateSqlTimings"=> false,
|
||||
"TrivialDurationThresholdMilliseconds" => 2,
|
||||
"SqlTimings" => [],
|
||||
"SqlTimingsDurationMilliseconds"=> 0,
|
||||
"IsTrivial"=> false,
|
||||
"IsRoot"=> false,
|
||||
"Depth"=> parent ? parent.depth + 1 : 0,
|
||||
"ExecutedReaders"=> 0,
|
||||
"ExecutedScalars"=> 0,
|
||||
"ExecutedNonQueries"=> 0,
|
||||
"CustomTimingStats" => {},
|
||||
"CustomTimings" => {})
|
||||
@children_duration = 0
|
||||
@start = Time.now
|
||||
@parent = parent
|
||||
@page = page
|
||||
end
|
||||
|
||||
def duration_ms
|
||||
self['DurationMilliseconds']
|
||||
end
|
||||
|
||||
def start_ms
|
||||
self['StartMilliseconds']
|
||||
end
|
||||
|
||||
def start
|
||||
@start
|
||||
end
|
||||
|
||||
def depth
|
||||
self['Depth']
|
||||
end
|
||||
|
||||
def children
|
||||
self['Children']
|
||||
end
|
||||
|
||||
def add_child(name)
|
||||
request_timer = RequestTimerStruct.new(name, @page, self)
|
||||
self['Children'].push(request_timer)
|
||||
self['HasChildren'] = true
|
||||
request_timer['ParentTimingId'] = self['Id']
|
||||
request_timer['Depth'] = self['Depth'] + 1
|
||||
request_timer
|
||||
end
|
||||
|
||||
def add_sql(query, elapsed_ms, page, skip_backtrace = false, full_backtrace = false)
|
||||
timer = SqlTimerStruct.new(query, elapsed_ms, page, self , skip_backtrace, full_backtrace)
|
||||
timer['ParentTimingId'] = self['Id']
|
||||
self['SqlTimings'].push(timer)
|
||||
self['HasSqlTimings'] = true
|
||||
self['SqlTimingsDurationMilliseconds'] += elapsed_ms
|
||||
page['DurationMillisecondsInSql'] += elapsed_ms
|
||||
timer
|
||||
end
|
||||
|
||||
def add_custom(type, elapsed_ms, page)
|
||||
timer = CustomTimerStruct.new(type, elapsed_ms, page, self)
|
||||
timer['ParentTimingId'] = self['Id']
|
||||
self['CustomTimings'][type] ||= []
|
||||
self['CustomTimings'][type].push(timer)
|
||||
|
||||
self['CustomTimingStats'][type] ||= {"Count" => 0, "Duration" => 0.0}
|
||||
self['CustomTimingStats'][type]['Count'] += 1
|
||||
self['CustomTimingStats'][type]['Duration'] += elapsed_ms
|
||||
|
||||
page['CustomTimingStats'][type] ||= {"Count" => 0, "Duration" => 0.0}
|
||||
page['CustomTimingStats'][type]['Count'] += 1
|
||||
page['CustomTimingStats'][type]['Duration'] += elapsed_ms
|
||||
|
||||
timer
|
||||
end
|
||||
|
||||
def record_time(milliseconds = nil)
|
||||
milliseconds ||= (Time.now - @start) * 1000
|
||||
self['DurationMilliseconds'] = milliseconds
|
||||
self['IsTrivial'] = true if milliseconds < self["TrivialDurationThresholdMilliseconds"]
|
||||
self['DurationWithoutChildrenMilliseconds'] = milliseconds - @children_duration
|
||||
|
||||
if @parent
|
||||
@parent.children_duration += milliseconds
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,58 @@
|
|||
require 'mini_profiler/timer_struct'
|
||||
|
||||
module Rack
|
||||
class MiniProfiler
|
||||
|
||||
# Timing system for a SQL query
|
||||
class SqlTimerStruct < TimerStruct
|
||||
def initialize(query, duration_ms, page, parent, skip_backtrace = false, full_backtrace = false)
|
||||
|
||||
stack_trace = nil
|
||||
unless skip_backtrace || duration_ms < Rack::MiniProfiler.config.backtrace_threshold_ms
|
||||
# Allow us to filter the stack trace
|
||||
stack_trace = ""
|
||||
# Clean up the stack trace if there are options to do so
|
||||
Kernel.caller.each do |ln|
|
||||
ln.gsub!(Rack::MiniProfiler.config.backtrace_remove, '') if Rack::MiniProfiler.config.backtrace_remove and !full_backtrace
|
||||
if full_backtrace or
|
||||
(
|
||||
(
|
||||
Rack::MiniProfiler.config.backtrace_includes.nil? or
|
||||
Rack::MiniProfiler.config.backtrace_includes.all?{|regex| ln =~ regex}
|
||||
) and
|
||||
(
|
||||
Rack::MiniProfiler.config.backtrace_ignores.nil? or
|
||||
Rack::MiniProfiler.config.backtrace_ignores.all?{|regex| !(ln =~ regex)}
|
||||
)
|
||||
)
|
||||
stack_trace << ln << "\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@parent = parent
|
||||
@page = page
|
||||
|
||||
super("ExecuteType" => 3, # TODO
|
||||
"FormattedCommandString" => query,
|
||||
"StackTraceSnippet" => stack_trace,
|
||||
"StartMilliseconds" => ((Time.now.to_f * 1000).to_i - page['Started']) - duration_ms,
|
||||
"DurationMilliseconds" => duration_ms,
|
||||
"FirstFetchDurationMilliseconds" => duration_ms,
|
||||
"Parameters" => nil,
|
||||
"ParentTimingId" => nil,
|
||||
"IsDuplicate" => false)
|
||||
end
|
||||
|
||||
def report_reader_duration(elapsed_ms)
|
||||
return if @reported
|
||||
@reported = true
|
||||
self["DurationMilliseconds"] += elapsed_ms
|
||||
@parent["SqlTimingsDurationMilliseconds"] += elapsed_ms
|
||||
@page["DurationMillisecondsInSql"] += elapsed_ms
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
module Rack
|
||||
class MiniProfiler
|
||||
class AbstractStore
|
||||
|
||||
def save(page_struct)
|
||||
raise NotImplementedError.new("save is not implemented")
|
||||
end
|
||||
|
||||
def load(id)
|
||||
raise NotImplementedError.new("load is not implemented")
|
||||
end
|
||||
|
||||
def set_unviewed(user, id)
|
||||
raise NotImplementedError.new("set_unviewed is not implemented")
|
||||
end
|
||||
|
||||
def set_viewed(user, id)
|
||||
raise NotImplementedError.new("set_viewed is not implemented")
|
||||
end
|
||||
|
||||
def get_unviewed_ids(user)
|
||||
raise NotImplementedError.new("get_unviewed_ids is not implemented")
|
||||
end
|
||||
|
||||
def diagnostics(user)
|
||||
# this is opt in, no need to explode if not implemented
|
||||
""
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,133 @@
|
|||
module Rack
|
||||
class MiniProfiler
|
||||
class FileStore < AbstractStore
|
||||
|
||||
# Sub-class thread so we have a named thread (useful for debugging in Thread.list).
|
||||
class CacheCleanupThread < Thread
|
||||
end
|
||||
|
||||
class FileCache
|
||||
def initialize(path, prefix)
|
||||
@path = path
|
||||
@prefix = prefix
|
||||
end
|
||||
|
||||
def [](key)
|
||||
begin
|
||||
data = ::File.open(path(key),"rb") {|f| f.read}
|
||||
return Marshal.load data
|
||||
rescue => e
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def []=(key,val)
|
||||
::File.open(path(key), "wb+") {|f| f.write Marshal.dump(val)}
|
||||
end
|
||||
|
||||
private
|
||||
def path(key)
|
||||
@path + "/" + @prefix + "_" + key
|
||||
end
|
||||
end
|
||||
|
||||
EXPIRES_IN_SECONDS = 60 * 60 * 24
|
||||
|
||||
def initialize(args = nil)
|
||||
args ||= {}
|
||||
@path = args[:path]
|
||||
@expires_in_seconds = args[:expires_in] || EXPIRES_IN_SECONDS
|
||||
raise ArgumentError.new :path unless @path
|
||||
@timer_struct_cache = FileCache.new(@path, "mp_timers")
|
||||
@timer_struct_lock = Mutex.new
|
||||
@user_view_cache = FileCache.new(@path, "mp_views")
|
||||
@user_view_lock = Mutex.new
|
||||
|
||||
me = self
|
||||
t = CacheCleanupThread.new do
|
||||
interval = 10
|
||||
cleanup_cache_cycle = 3600
|
||||
cycle_count = 1
|
||||
|
||||
begin
|
||||
until Thread.current[:should_exit] do
|
||||
# TODO: a sane retry count before bailing
|
||||
|
||||
# We don't want to hit the filesystem every 10s to clean up the cache so we need to do a bit of
|
||||
# accounting to avoid sleeping that entire time. We don't want to sleep for the entire period because
|
||||
# it means the thread will stay live in hot deployment scenarios, keeping a potentially large memory
|
||||
# graph from being garbage collected upon undeploy.
|
||||
if cycle_count * interval >= cleanup_cache_cycle
|
||||
cycle_count = 1
|
||||
me.cleanup_cache
|
||||
end
|
||||
|
||||
sleep(interval)
|
||||
cycle_count += 1
|
||||
end
|
||||
rescue
|
||||
# don't crash the thread, we can clean up next time
|
||||
end
|
||||
end
|
||||
|
||||
at_exit { t[:should_exit] = true }
|
||||
|
||||
t
|
||||
end
|
||||
|
||||
def save(page_struct)
|
||||
@timer_struct_lock.synchronize {
|
||||
@timer_struct_cache[page_struct['Id']] = page_struct
|
||||
}
|
||||
end
|
||||
|
||||
def load(id)
|
||||
@timer_struct_lock.synchronize {
|
||||
@timer_struct_cache[id]
|
||||
}
|
||||
end
|
||||
|
||||
def set_unviewed(user, id)
|
||||
@user_view_lock.synchronize {
|
||||
current = @user_view_cache[user]
|
||||
current = [] unless Array === current
|
||||
current << id
|
||||
@user_view_cache[user] = current.uniq
|
||||
}
|
||||
end
|
||||
|
||||
def set_viewed(user, id)
|
||||
@user_view_lock.synchronize {
|
||||
@user_view_cache[user] ||= []
|
||||
current = @user_view_cache[user]
|
||||
current = [] unless Array === current
|
||||
current.delete(id)
|
||||
@user_view_cache[user] = current.uniq
|
||||
}
|
||||
end
|
||||
|
||||
def get_unviewed_ids(user)
|
||||
@user_view_lock.synchronize {
|
||||
@user_view_cache[user]
|
||||
}
|
||||
end
|
||||
|
||||
def cleanup_cache
|
||||
files = Dir.entries(@path)
|
||||
@timer_struct_lock.synchronize {
|
||||
files.each do |f|
|
||||
f = @path + '/' + f
|
||||
::File.delete f if ::File.basename(f) =~ /^mp_timers/ and (Time.now - ::File.mtime(f)) > @expires_in_seconds
|
||||
end
|
||||
}
|
||||
@user_view_lock.synchronize {
|
||||
files.each do |f|
|
||||
f = @path + '/' + f
|
||||
::File.delete f if ::File.basename(f) =~ /^mp_views/ and (Time.now - ::File.mtime(f)) > @expires_in_seconds
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,53 @@
|
|||
module Rack
|
||||
class MiniProfiler
|
||||
class MemcacheStore < AbstractStore
|
||||
|
||||
EXPIRES_IN_SECONDS = 60 * 60 * 24
|
||||
MAX_RETRIES = 10
|
||||
|
||||
def initialize(args = nil)
|
||||
require 'dalli' unless defined? Dalli
|
||||
args ||= {}
|
||||
@prefix = args[:prefix] || "MPMemcacheStore"
|
||||
@client = args[:client] || Dalli::Client.new
|
||||
@expires_in_seconds = args[:expires_in] || EXPIRES_IN_SECONDS
|
||||
end
|
||||
|
||||
def save(page_struct)
|
||||
@client.set("#{@prefix}#{page_struct['Id']}", Marshal::dump(page_struct), @expires_in_seconds)
|
||||
end
|
||||
|
||||
def load(id)
|
||||
raw = @client.get("#{@prefix}#{id}")
|
||||
if raw
|
||||
Marshal::load raw
|
||||
end
|
||||
end
|
||||
|
||||
def set_unviewed(user, id)
|
||||
@client.add("#{@prefix}-#{user}-v", [], @expires_in_seconds)
|
||||
MAX_RETRIES.times do
|
||||
break if @client.cas("#{@prefix}-#{user}-v", @expires_in_seconds) do |ids|
|
||||
ids << id unless ids.include?(id)
|
||||
ids
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_viewed(user, id)
|
||||
@client.add("#{@prefix}-#{user}-v", [], @expires_in_seconds)
|
||||
MAX_RETRIES.times do
|
||||
break if @client.cas("#{@prefix}-#{user}-v", @expires_in_seconds) do |ids|
|
||||
ids.delete id
|
||||
ids
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_unviewed_ids(user)
|
||||
@client.get("#{@prefix}-#{user}-v") || []
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,86 @@
|
|||
module Rack
|
||||
class MiniProfiler
|
||||
class MemoryStore < AbstractStore
|
||||
|
||||
# Sub-class thread so we have a named thread (useful for debugging in Thread.list).
|
||||
class CacheCleanupThread < Thread
|
||||
end
|
||||
|
||||
EXPIRES_IN_SECONDS = 60 * 60 * 24
|
||||
|
||||
def initialize(args = nil)
|
||||
args ||= {}
|
||||
@expires_in_seconds = args[:expires_in] || EXPIRES_IN_SECONDS
|
||||
@timer_struct_lock = Mutex.new
|
||||
@timer_struct_cache = {}
|
||||
@user_view_lock = Mutex.new
|
||||
@user_view_cache = {}
|
||||
|
||||
# TODO: fix it to use weak ref, trouble is may be broken in 1.9 so need to use the 'ref' gem
|
||||
me = self
|
||||
t = CacheCleanupThread.new do
|
||||
interval = 10
|
||||
cleanup_cache_cycle = 3600
|
||||
cycle_count = 1
|
||||
|
||||
until Thread.current[:should_exit] do
|
||||
# We don't want to hit the filesystem every 10s to clean up the cache so we need to do a bit of
|
||||
# accounting to avoid sleeping that entire time. We don't want to sleep for the entire period because
|
||||
# it means the thread will stay live in hot deployment scenarios, keeping a potentially large memory
|
||||
# graph from being garbage collected upon undeploy.
|
||||
if cycle_count * interval >= cleanup_cache_cycle
|
||||
cycle_count = 1
|
||||
me.cleanup_cache
|
||||
end
|
||||
|
||||
sleep(interval)
|
||||
cycle_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
at_exit { t[:should_exit] = true }
|
||||
|
||||
t
|
||||
end
|
||||
|
||||
def save(page_struct)
|
||||
@timer_struct_lock.synchronize {
|
||||
@timer_struct_cache[page_struct['Id']] = page_struct
|
||||
}
|
||||
end
|
||||
|
||||
def load(id)
|
||||
@timer_struct_lock.synchronize {
|
||||
@timer_struct_cache[id]
|
||||
}
|
||||
end
|
||||
|
||||
def set_unviewed(user, id)
|
||||
@user_view_lock.synchronize {
|
||||
@user_view_cache[user] ||= []
|
||||
@user_view_cache[user] << id
|
||||
}
|
||||
end
|
||||
|
||||
def set_viewed(user, id)
|
||||
@user_view_lock.synchronize {
|
||||
@user_view_cache[user] ||= []
|
||||
@user_view_cache[user].delete(id)
|
||||
}
|
||||
end
|
||||
|
||||
def get_unviewed_ids(user)
|
||||
@user_view_lock.synchronize {
|
||||
@user_view_cache[user]
|
||||
}
|
||||
end
|
||||
|
||||
def cleanup_cache
|
||||
expire_older_than = ((Time.now.to_f - @expires_in_seconds) * 1000).to_i
|
||||
@timer_struct_lock.synchronize {
|
||||
@timer_struct_cache.delete_if { |k, v| v['Started'] < expire_older_than }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,54 @@
|
|||
module Rack
|
||||
class MiniProfiler
|
||||
class RedisStore < AbstractStore
|
||||
|
||||
EXPIRES_IN_SECONDS = 60 * 60 * 24
|
||||
|
||||
def initialize(args = nil)
|
||||
@args = args || {}
|
||||
@prefix = @args.delete(:prefix) || 'MPRedisStore'
|
||||
@redis_connection = @args.delete(:connection)
|
||||
@expires_in_seconds = @args.delete(:expires_in) || EXPIRES_IN_SECONDS
|
||||
end
|
||||
|
||||
def save(page_struct)
|
||||
redis.setex "#{@prefix}#{page_struct['Id']}", @expires_in_seconds, Marshal::dump(page_struct)
|
||||
end
|
||||
|
||||
def load(id)
|
||||
raw = redis.get "#{@prefix}#{id}"
|
||||
if raw
|
||||
Marshal::load raw
|
||||
end
|
||||
end
|
||||
|
||||
def set_unviewed(user, id)
|
||||
redis.sadd "#{@prefix}-#{user}-v", id
|
||||
end
|
||||
|
||||
def set_viewed(user, id)
|
||||
redis.srem "#{@prefix}-#{user}-v", id
|
||||
end
|
||||
|
||||
def get_unviewed_ids(user)
|
||||
redis.smembers "#{@prefix}-#{user}-v"
|
||||
end
|
||||
|
||||
def diagnostics(user)
|
||||
"Redis prefix: #{@prefix}
|
||||
Redis location: #{redis.client.host}:#{redis.client.port} db: #{redis.client.db}
|
||||
unviewed_ids: #{get_unviewed_ids(user)}
|
||||
"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def redis
|
||||
return @redis_connection if @redis_connection
|
||||
require 'redis' unless defined? Redis
|
||||
@redis_connection ||= Redis.new @args
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,33 @@
|
|||
module Rack
|
||||
class MiniProfiler
|
||||
|
||||
# A base class for timing structures
|
||||
class TimerStruct
|
||||
|
||||
def initialize(attrs={})
|
||||
@attributes = attrs
|
||||
end
|
||||
|
||||
def attributes
|
||||
@attributes ||= {}
|
||||
end
|
||||
|
||||
def [](name)
|
||||
attributes[name]
|
||||
end
|
||||
|
||||
def []=(name, val)
|
||||
attributes[name] = val
|
||||
self
|
||||
end
|
||||
|
||||
def to_json(*a)
|
||||
# this does could take in an option hash, but the only interesting there is max_nesting.
|
||||
# if this becomes an option we could increase
|
||||
::JSON.generate( @attributes, :max_nesting => 100 )
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
module Rack
|
||||
class MiniProfiler
|
||||
VERSION = '898a13ca6797c6bc1fee313e17d388b0'.freeze
|
||||
end
|
||||
end
|
|
@ -0,0 +1,106 @@
|
|||
require 'fileutils'
|
||||
|
||||
module Rack::MiniProfilerRails
|
||||
|
||||
# call direct if needed to do a defer init
|
||||
def self.initialize!(app)
|
||||
|
||||
raise "MiniProfilerRails initialized twice. Set `require: false' for rack-mini-profiler in your Gemfile" if @already_initialized
|
||||
|
||||
c = Rack::MiniProfiler.config
|
||||
|
||||
# By default, only show the MiniProfiler in development mode, in production allow profiling if post_authorize_cb is set
|
||||
#
|
||||
# NOTE: this must be set here with = and not ||=
|
||||
# The out of the box default is "true"
|
||||
c.pre_authorize_cb = lambda { |env|
|
||||
!Rails.env.test?
|
||||
}
|
||||
|
||||
c.skip_paths ||= []
|
||||
|
||||
if Rails.env.development?
|
||||
c.skip_paths << app.config.assets.prefix if app.respond_to? :assets
|
||||
c.skip_schema_queries = true
|
||||
end
|
||||
|
||||
if Rails.env.production?
|
||||
c.authorization_mode = :whitelist
|
||||
end
|
||||
|
||||
if Rails.logger
|
||||
c.logger = Rails.logger
|
||||
end
|
||||
|
||||
# The file store is just so much less flaky
|
||||
base_path = Rails.application.config.paths['tmp'].first rescue "#{Rails.root}/tmp"
|
||||
tmp = base_path + '/miniprofiler'
|
||||
FileUtils.mkdir_p(tmp) unless File.exists?(tmp)
|
||||
|
||||
c.storage_options = {:path => tmp}
|
||||
c.storage = Rack::MiniProfiler::FileStore
|
||||
|
||||
# Quiet the SQL stack traces
|
||||
c.backtrace_remove = Rails.root.to_s + "/"
|
||||
c.backtrace_includes = [/^\/?(app|config|lib|test)/]
|
||||
c.skip_schema_queries = Rails.env != 'production'
|
||||
|
||||
# Install the Middleware
|
||||
app.middleware.insert(0, Rack::MiniProfiler)
|
||||
|
||||
# Attach to various Rails methods
|
||||
ActiveSupport.on_load(:action_controller) do
|
||||
::Rack::MiniProfiler.profile_method(ActionController::Base, :process) {|action| "Executing action: #{action}"}
|
||||
end
|
||||
ActiveSupport.on_load(:action_view) do
|
||||
::Rack::MiniProfiler.profile_method(ActionView::Template, :render) {|x,y| "Rendering: #{@virtual_path}"}
|
||||
end
|
||||
|
||||
@already_initialized = true
|
||||
end
|
||||
|
||||
class Railtie < ::Rails::Railtie
|
||||
|
||||
initializer "rack_mini_profiler.configure_rails_initialization" do |app|
|
||||
Rack::MiniProfilerRails.initialize!(app)
|
||||
end
|
||||
|
||||
# TODO: Implement something better here
|
||||
# config.after_initialize do
|
||||
#
|
||||
# class ::ActionView::Helpers::AssetTagHelper::JavascriptIncludeTag
|
||||
# alias_method :asset_tag_orig, :asset_tag
|
||||
# def asset_tag(source,options)
|
||||
# current = Rack::MiniProfiler.current
|
||||
# return asset_tag_orig(source,options) unless current
|
||||
# wrapped = ""
|
||||
# unless current.mpt_init
|
||||
# current.mpt_init = true
|
||||
# wrapped << Rack::MiniProfiler::ClientTimerStruct.init_instrumentation
|
||||
# end
|
||||
# name = source.split('/')[-1]
|
||||
# wrapped << Rack::MiniProfiler::ClientTimerStruct.instrument(name, asset_tag_orig(source,options)).html_safe
|
||||
# wrapped
|
||||
# end
|
||||
# end
|
||||
|
||||
# class ::ActionView::Helpers::AssetTagHelper::StylesheetIncludeTag
|
||||
# alias_method :asset_tag_orig, :asset_tag
|
||||
# def asset_tag(source,options)
|
||||
# current = Rack::MiniProfiler.current
|
||||
# return asset_tag_orig(source,options) unless current
|
||||
# wrapped = ""
|
||||
# unless current.mpt_init
|
||||
# current.mpt_init = true
|
||||
# wrapped << Rack::MiniProfiler::ClientTimerStruct.init_instrumentation
|
||||
# end
|
||||
# name = source.split('/')[-1]
|
||||
# wrapped << Rack::MiniProfiler::ClientTimerStruct.instrument(name, asset_tag_orig(source,options)).html_safe
|
||||
# wrapped
|
||||
# end
|
||||
# end
|
||||
|
||||
# end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
if (defined?(Net) && defined?(Net::HTTP))
|
||||
|
||||
Net::HTTP.class_eval do
|
||||
def request_with_mini_profiler(*args, &block)
|
||||
request = args[0]
|
||||
Rack::MiniProfiler.step("Net::HTTP #{request.method} #{request.path}") do
|
||||
request_without_mini_profiler(*args, &block)
|
||||
end
|
||||
end
|
||||
alias request_without_mini_profiler request
|
||||
alias request request_with_mini_profiler
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,284 @@
|
|||
class SqlPatches
|
||||
|
||||
def self.patched?
|
||||
@patched
|
||||
end
|
||||
|
||||
def self.patched=(val)
|
||||
@patched = val
|
||||
end
|
||||
|
||||
def self.class_exists?(name)
|
||||
eval(name + ".class").to_s.eql?('Class')
|
||||
rescue NameError
|
||||
false
|
||||
end
|
||||
|
||||
def self.module_exists?(name)
|
||||
eval(name + ".class").to_s.eql?('Module')
|
||||
rescue NameError
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# The best kind of instrumentation is in the actual db provider, however we don't want to double instrument
|
||||
if SqlPatches.class_exists? "Mysql2::Client"
|
||||
|
||||
class Mysql2::Result
|
||||
alias_method :each_without_profiling, :each
|
||||
def each(*args, &blk)
|
||||
return each_without_profiling(*args, &blk) unless @miniprofiler_sql_id
|
||||
|
||||
start = Time.now
|
||||
result = each_without_profiling(*args,&blk)
|
||||
elapsed_time = ((Time.now - start).to_f * 1000).round(1)
|
||||
|
||||
@miniprofiler_sql_id.report_reader_duration(elapsed_time)
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
class Mysql2::Client
|
||||
alias_method :query_without_profiling, :query
|
||||
def query(*args,&blk)
|
||||
current = ::Rack::MiniProfiler.current
|
||||
return query_without_profiling(*args,&blk) unless current && current.measure
|
||||
|
||||
start = Time.now
|
||||
result = query_without_profiling(*args,&blk)
|
||||
elapsed_time = ((Time.now - start).to_f * 1000).round(1)
|
||||
record = ::Rack::MiniProfiler.record_sql(args[0], elapsed_time)
|
||||
result.instance_variable_set("@miniprofiler_sql_id", record) if result
|
||||
|
||||
result
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
SqlPatches.patched = true
|
||||
end
|
||||
|
||||
|
||||
# PG patches, keep in mind exec and async_exec have a exec{|r| } semantics that is yet to be implemented
|
||||
if SqlPatches.class_exists? "PG::Result"
|
||||
|
||||
class PG::Result
|
||||
alias_method :each_without_profiling, :each
|
||||
alias_method :values_without_profiling, :values
|
||||
|
||||
def values(*args, &blk)
|
||||
return values_without_profiling(*args, &blk) unless @miniprofiler_sql_id
|
||||
|
||||
start = Time.now
|
||||
result = values_without_profiling(*args,&blk)
|
||||
elapsed_time = ((Time.now - start).to_f * 1000).round(1)
|
||||
|
||||
@miniprofiler_sql_id.report_reader_duration(elapsed_time)
|
||||
result
|
||||
end
|
||||
|
||||
def each(*args, &blk)
|
||||
return each_without_profiling(*args, &blk) unless @miniprofiler_sql_id
|
||||
|
||||
start = Time.now
|
||||
result = each_without_profiling(*args,&blk)
|
||||
elapsed_time = ((Time.now - start).to_f * 1000).round(1)
|
||||
|
||||
@miniprofiler_sql_id.report_reader_duration(elapsed_time)
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
class PG::Connection
|
||||
alias_method :exec_without_profiling, :exec
|
||||
alias_method :async_exec_without_profiling, :async_exec
|
||||
alias_method :exec_prepared_without_profiling, :exec_prepared
|
||||
alias_method :send_query_prepared_without_profiling, :send_query_prepared
|
||||
alias_method :prepare_without_profiling, :prepare
|
||||
|
||||
def prepare(*args,&blk)
|
||||
# we have no choice but to do this here,
|
||||
# if we do the check for profiling first, our cache may miss critical stuff
|
||||
|
||||
@prepare_map ||= {}
|
||||
@prepare_map[args[0]] = args[1]
|
||||
# dont leak more than 10k ever
|
||||
@prepare_map = {} if @prepare_map.length > 1000
|
||||
|
||||
current = ::Rack::MiniProfiler.current
|
||||
return prepare_without_profiling(*args,&blk) unless current && current.measure
|
||||
|
||||
prepare_without_profiling(*args,&blk)
|
||||
end
|
||||
|
||||
def exec(*args,&blk)
|
||||
current = ::Rack::MiniProfiler.current
|
||||
return exec_without_profiling(*args,&blk) unless current && current.measure
|
||||
|
||||
start = Time.now
|
||||
result = exec_without_profiling(*args,&blk)
|
||||
elapsed_time = ((Time.now - start).to_f * 1000).round(1)
|
||||
record = ::Rack::MiniProfiler.record_sql(args[0], elapsed_time)
|
||||
result.instance_variable_set("@miniprofiler_sql_id", record) if result
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def exec_prepared(*args,&blk)
|
||||
current = ::Rack::MiniProfiler.current
|
||||
return exec_prepared_without_profiling(*args,&blk) unless current && current.measure
|
||||
|
||||
start = Time.now
|
||||
result = exec_prepared_without_profiling(*args,&blk)
|
||||
elapsed_time = ((Time.now - start).to_f * 1000).round(1)
|
||||
mapped = args[0]
|
||||
mapped = @prepare_map[mapped] || args[0] if @prepare_map
|
||||
record = ::Rack::MiniProfiler.record_sql(mapped, elapsed_time)
|
||||
result.instance_variable_set("@miniprofiler_sql_id", record) if result
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def send_query_prepared(*args,&blk)
|
||||
current = ::Rack::MiniProfiler.current
|
||||
return send_query_prepared_without_profiling(*args,&blk) unless current && current.measure
|
||||
|
||||
start = Time.now
|
||||
result = send_query_prepared_without_profiling(*args,&blk)
|
||||
|
||||
elapsed_time = ((Time.now - start).to_f * 1000).round(1)
|
||||
mapped = args[0]
|
||||
mapped = @prepare_map[mapped] || args[0] if @prepare_map
|
||||
record = ::Rack::MiniProfiler.record_sql(mapped, elapsed_time)
|
||||
result.instance_variable_set("@miniprofiler_sql_id", record) if result
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def async_exec(*args,&blk)
|
||||
current = ::Rack::MiniProfiler.current
|
||||
return exec_without_profiling(*args,&blk) unless current && current.measure
|
||||
|
||||
start = Time.now
|
||||
result = exec_without_profiling(*args,&blk)
|
||||
elapsed_time = ((Time.now - start).to_f * 1000).round(1)
|
||||
record = ::Rack::MiniProfiler.record_sql(args[0], elapsed_time)
|
||||
result.instance_variable_set("@miniprofiler_sql_id", record) if result
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
alias_method :query, :exec
|
||||
end
|
||||
|
||||
SqlPatches.patched = true
|
||||
end
|
||||
|
||||
|
||||
# Mongoid 3 patches
|
||||
if SqlPatches.class_exists?("Moped::Node")
|
||||
class Moped::Node
|
||||
alias_method :process_without_profiling, :process
|
||||
def process(*args,&blk)
|
||||
current = ::Rack::MiniProfiler.current
|
||||
return process_without_profiling(*args,&blk) unless current && current.measure
|
||||
|
||||
start = Time.now
|
||||
result = process_without_profiling(*args,&blk)
|
||||
elapsed_time = ((Time.now - start).to_f * 1000).round(1)
|
||||
::Rack::MiniProfiler.record_sql(args[0].log_inspect, elapsed_time)
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if SqlPatches.class_exists?("RSolr::Connection") && RSolr::VERSION[0] != "0" # requires at least v1.0.0
|
||||
class RSolr::Connection
|
||||
alias_method :execute_without_profiling, :execute
|
||||
def execute_with_profiling(client, request_context)
|
||||
current = ::Rack::MiniProfiler.current
|
||||
return execute_without_profiling(client, request_context) unless current && current.measure
|
||||
|
||||
start = Time.now
|
||||
result = execute_without_profiling(client, request_context)
|
||||
elapsed_time = ((Time.now - start).to_f * 1000).round(1)
|
||||
|
||||
data = "#{request_context[:method].upcase} #{request_context[:uri]}"
|
||||
if request_context[:method] == :post and request_context[:data]
|
||||
if request_context[:headers].include?("Content-Type") and request_context[:headers]["Content-Type"] == "text/xml"
|
||||
# it's xml, unescaping isn't needed
|
||||
data << "\n#{request_context[:data]}"
|
||||
else
|
||||
data << "\n#{Rack::Utils.unescape(request_context[:data])}"
|
||||
end
|
||||
end
|
||||
::Rack::MiniProfiler.record_sql(data, elapsed_time)
|
||||
|
||||
result
|
||||
end
|
||||
alias_method :execute, :execute_with_profiling
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Fallback for sequel
|
||||
if SqlPatches.class_exists?("Sequel::Database") && !SqlPatches.patched?
|
||||
module Sequel
|
||||
class Database
|
||||
alias_method :log_duration_original, :log_duration
|
||||
def log_duration(duration, message)
|
||||
# `duration` will be in seconds, but we need it in milliseconds for internal consistency.
|
||||
::Rack::MiniProfiler.record_sql(message, duration * 1000)
|
||||
log_duration_original(duration, message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
## based off https://github.com/newrelic/rpm/blob/master/lib/new_relic/agent/instrumentation/active_record.rb
|
||||
## fallback for alls sorts of weird dbs
|
||||
if SqlPatches.module_exists?('ActiveRecord') && !SqlPatches.patched?
|
||||
module Rack
|
||||
class MiniProfiler
|
||||
module ActiveRecordInstrumentation
|
||||
def self.included(instrumented_class)
|
||||
instrumented_class.class_eval do
|
||||
unless instrumented_class.method_defined?(:log_without_miniprofiler)
|
||||
alias_method :log_without_miniprofiler, :log
|
||||
alias_method :log, :log_with_miniprofiler
|
||||
protected :log
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def log_with_miniprofiler(*args, &block)
|
||||
current = ::Rack::MiniProfiler.current
|
||||
return log_without_miniprofiler(*args, &block) unless current && current.measure
|
||||
|
||||
sql, name, binds = args
|
||||
t0 = Time.now
|
||||
rval = log_without_miniprofiler(*args, &block)
|
||||
|
||||
# Don't log schema queries if the option is set
|
||||
return rval if Rack::MiniProfiler.config.skip_schema_queries and name =~ /SCHEMA/
|
||||
|
||||
elapsed_time = ((Time.now - t0).to_f * 1000).round(1)
|
||||
Rack::MiniProfiler.record_sql(sql, elapsed_time)
|
||||
rval
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.insert_instrumentation
|
||||
ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
|
||||
include ::Rack::MiniProfiler::ActiveRecordInstrumentation
|
||||
end
|
||||
end
|
||||
|
||||
if defined?(::Rails) && !SqlPatches.patched?
|
||||
insert_instrumentation
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
require 'mini_profiler/profiler'
|
||||
require 'patches/sql_patches'
|
||||
require 'patches/net_patches'
|
||||
|
||||
if defined?(::Rails) && ::Rails::VERSION::MAJOR.to_i >= 3
|
||||
require 'mini_profiler_rails/railtie'
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
Gem::Specification.new do |s|
|
||||
s.name = "rack-mini-profiler"
|
||||
s.version = "0.9.1"
|
||||
s.summary = "Profiles loading speed for rack applications."
|
||||
s.authors = ["Sam Saffron", "Robin Ward","Aleks Totic"]
|
||||
s.description = "Profiling toolkit for Rack applications with Rails integration. Client Side profiling, DB profiling and Server profiling."
|
||||
s.email = "sam.saffron@gmail.com"
|
||||
s.homepage = "http://miniprofiler.com"
|
||||
s.license = "MIT"
|
||||
s.files = [
|
||||
'rack-mini-profiler.gemspec',
|
||||
].concat( Dir.glob('lib/**/*').reject {|f| File.directory?(f) || f =~ /~$/ } )
|
||||
s.extra_rdoc_files = [
|
||||
"README.md",
|
||||
"CHANGELOG"
|
||||
]
|
||||
s.add_runtime_dependency 'rack', '>= 1.1.3'
|
||||
if RUBY_VERSION < "1.9"
|
||||
s.add_runtime_dependency 'json', '>= 1.6'
|
||||
end
|
||||
|
||||
s.add_development_dependency 'rake'
|
||||
s.add_development_dependency 'rack-test'
|
||||
s.add_development_dependency 'activerecord', '~> 3.0'
|
||||
s.add_development_dependency 'dalli'
|
||||
s.add_development_dependency 'rspec'
|
||||
s.add_development_dependency 'ZenTest'
|
||||
s.add_development_dependency 'autotest'
|
||||
s.add_development_dependency 'redis'
|
||||
s.add_development_dependency 'therubyracer'
|
||||
s.add_development_dependency 'less'
|
||||
s.add_development_dependency 'flamegraph'
|
||||
|
||||
s.require_paths = ["lib"]
|
||||
end
|
|
@ -0,0 +1,45 @@
|
|||
require 'spec_helper'
|
||||
require 'mini_profiler/client_settings'
|
||||
require 'uri'
|
||||
require 'rack'
|
||||
|
||||
describe Rack::MiniProfiler::ClientSettings do
|
||||
|
||||
describe "with settings" do
|
||||
before do
|
||||
settings = URI.encode_www_form_component("dp=t,bt=1")
|
||||
@settings = Rack::MiniProfiler::ClientSettings.new({"HTTP_COOKIE" => "__profilin=#{settings};" })
|
||||
end
|
||||
|
||||
it 'has the cookies' do
|
||||
@settings.has_cookie?.should be_true
|
||||
end
|
||||
|
||||
it 'has profiling disabled' do
|
||||
@settings.disable_profiling?.should be_true
|
||||
end
|
||||
|
||||
it 'has backtrace set to full' do
|
||||
@settings.backtrace_full?.should be_true
|
||||
end
|
||||
|
||||
it 'should not write cookie changes if no change' do
|
||||
hash = {}
|
||||
@settings.write!(hash)
|
||||
hash.should == {}
|
||||
end
|
||||
|
||||
it 'should correctly write cookie changes if changed' do
|
||||
@settings.disable_profiling = false
|
||||
hash = {}
|
||||
@settings.write!(hash)
|
||||
hash.should_not == {}
|
||||
end
|
||||
end
|
||||
|
||||
it "should not have settings by default" do
|
||||
Rack::MiniProfiler::ClientSettings.new({}).has_cookie?.should == false
|
||||
end
|
||||
|
||||
|
||||
end
|
|
@ -0,0 +1,162 @@
|
|||
require 'spec_helper'
|
||||
require 'rack-mini-profiler'
|
||||
require 'yaml'
|
||||
|
||||
describe Rack::MiniProfiler::ClientTimerStruct do
|
||||
|
||||
def new_page
|
||||
Rack::MiniProfiler::PageTimerStruct.new({})
|
||||
end
|
||||
|
||||
def fixture(name)
|
||||
YAML.load(File.open(File.dirname(__FILE__) + "/../fixtures/#{name}.yml"))
|
||||
end
|
||||
|
||||
before do
|
||||
@client = Rack::MiniProfiler::ClientTimerStruct.new
|
||||
end
|
||||
|
||||
it 'defaults to no attributes' do
|
||||
::JSON.parse(@client.to_json).should be_empty
|
||||
end
|
||||
|
||||
describe 'init_from_form_data' do
|
||||
|
||||
describe 'without a form' do
|
||||
before do
|
||||
@client = Rack::MiniProfiler::ClientTimerStruct.init_from_form_data({}, new_page)
|
||||
end
|
||||
|
||||
it 'is null' do
|
||||
@client.should be_nil
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'with a simple request' do
|
||||
before do
|
||||
@client = Rack::MiniProfiler::ClientTimerStruct.init_from_form_data(fixture(:simple_client_request), new_page)
|
||||
end
|
||||
|
||||
it 'has the correct RedirectCount' do
|
||||
@client['RedirectCount'].should == 1
|
||||
end
|
||||
|
||||
it 'has Timings' do
|
||||
@client['Timings'].should_not be_empty
|
||||
end
|
||||
|
||||
describe "bob.js" do
|
||||
before do
|
||||
@bob = @client['Timings'].find {|t| t["Name"] == "bob.js"}
|
||||
end
|
||||
|
||||
it 'has it in the timings' do
|
||||
@bob.should_not be_nil
|
||||
end
|
||||
|
||||
it 'has the correct duration' do
|
||||
@bob["Duration"].should == 6
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "Navigation" do
|
||||
before do
|
||||
@nav = @client['Timings'].find {|t| t["Name"] == "Navigation"}
|
||||
end
|
||||
|
||||
it 'has a Timing for the Navigation' do
|
||||
@nav.should_not be_nil
|
||||
end
|
||||
|
||||
it 'has the correct start' do
|
||||
@nav['Start'].should == 0
|
||||
end
|
||||
|
||||
it 'has the correct duration' do
|
||||
@nav['Duration'].should == 16
|
||||
end
|
||||
end
|
||||
|
||||
describe "Simple" do
|
||||
before do
|
||||
@simple = @client['Timings'].find {|t| t["Name"] == "Simple"}
|
||||
end
|
||||
|
||||
it 'has a Timing for the Simple' do
|
||||
@simple.should_not be_nil
|
||||
end
|
||||
|
||||
it 'has the correct start' do
|
||||
@simple['Start'].should == 1
|
||||
end
|
||||
|
||||
it 'has the correct duration' do
|
||||
@simple['Duration'].should == 10
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'with some odd values' do
|
||||
before do
|
||||
@client = Rack::MiniProfiler::ClientTimerStruct.init_from_form_data(fixture(:weird_client_request), new_page)
|
||||
end
|
||||
|
||||
it 'has the correct RedirectCount' do
|
||||
@client['RedirectCount'].should == 99
|
||||
end
|
||||
|
||||
it 'has Timings' do
|
||||
@client['Timings'].should_not be_empty
|
||||
end
|
||||
|
||||
it 'has no timing when the start is before Navigation' do
|
||||
@client['Timings'].find {|t| t["Name"] == "Previous"}.should be_nil
|
||||
end
|
||||
|
||||
describe "weird" do
|
||||
before do
|
||||
@weird = @client['Timings'].find {|t| t["Name"] == "Weird"}
|
||||
end
|
||||
|
||||
it 'has a Timing for the Weird' do
|
||||
@weird.should_not be_nil
|
||||
end
|
||||
|
||||
it 'has the correct start' do
|
||||
@weird['Start'].should == 11
|
||||
end
|
||||
|
||||
it 'has a 0 duration because start time is greater than end time' do
|
||||
@weird['Duration'].should == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "differentFormat" do
|
||||
before do
|
||||
@diff = @client['Timings'].find {|t| t["Name"] == "differentFormat"}
|
||||
end
|
||||
|
||||
it 'has a Timing for the differentFormat' do
|
||||
@diff.should_not be_nil
|
||||
end
|
||||
|
||||
it 'has the correct start' do
|
||||
@diff['Start'].should == 1
|
||||
end
|
||||
|
||||
it 'has a -1 duration because the format was different' do
|
||||
@diff['Duration'].should == -1
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
require 'spec_helper'
|
||||
require 'rack-mini-profiler'
|
||||
|
||||
module Rack
|
||||
describe MiniProfiler::Config do
|
||||
|
||||
describe '.default' do
|
||||
it 'has "enabled" set to true' do
|
||||
MiniProfiler::Config.default.enabled.should be_true
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,46 @@
|
|||
require 'spec_helper'
|
||||
require 'rack-mini-profiler'
|
||||
require 'mini_profiler/page_timer_struct'
|
||||
require 'mini_profiler/storage/abstract_store'
|
||||
require 'mini_profiler/storage/file_store'
|
||||
|
||||
describe Rack::MiniProfiler::FileStore do
|
||||
|
||||
context 'page struct' do
|
||||
|
||||
before do
|
||||
tmp = File.expand_path(__FILE__ + "/../../../tmp")
|
||||
Dir::mkdir(tmp) unless File.exists?(tmp)
|
||||
@store = Rack::MiniProfiler::FileStore.new(:path => tmp)
|
||||
end
|
||||
|
||||
describe 'storage' do
|
||||
|
||||
it 'can store a PageStruct and retrieve it' do
|
||||
page_struct = Rack::MiniProfiler::PageTimerStruct.new({})
|
||||
page_struct['Id'] = "XYZ"
|
||||
page_struct['Random'] = "random"
|
||||
@store.save(page_struct)
|
||||
page_struct = @store.load("XYZ")
|
||||
page_struct['Random'].should == "random"
|
||||
page_struct['Id'].should == "XYZ"
|
||||
end
|
||||
|
||||
it 'can list unviewed items for a user' do
|
||||
@store.set_unviewed('a', 'XYZ')
|
||||
@store.set_unviewed('a', 'ABC')
|
||||
@store.get_unviewed_ids('a').sort.to_a.should == ['XYZ', 'ABC'].sort.to_a
|
||||
end
|
||||
|
||||
it 'can set an item to viewed once it is unviewed' do
|
||||
@store.set_unviewed('a', 'XYZ')
|
||||
@store.set_unviewed('a', 'ABC')
|
||||
@store.set_viewed('a', 'XYZ')
|
||||
@store.get_unviewed_ids('a').should == ['ABC']
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,69 @@
|
|||
require 'spec_helper'
|
||||
require 'rack-mini-profiler'
|
||||
require 'mini_profiler/gc_profiler'
|
||||
|
||||
describe Rack::MiniProfiler::GCProfiler do
|
||||
before :each do
|
||||
@app = lambda do |env|
|
||||
env
|
||||
end
|
||||
@env = {}
|
||||
@profiler = Rack::MiniProfiler::GCProfiler.new
|
||||
end
|
||||
|
||||
describe '#profile_gc_time' do
|
||||
it 'doesn\'t enable the gc if it was disabled previously' do
|
||||
GC.disable
|
||||
|
||||
expect {
|
||||
@profiler.profile_gc_time(@app, @env)
|
||||
}.to_not change { GC.disable }
|
||||
|
||||
# Let's re-enable the GC for the rest of the test suite
|
||||
GC.enable
|
||||
end
|
||||
|
||||
it 'keeps the GC enable if it was enabled previously' do
|
||||
expect {
|
||||
@profiler.profile_gc_time(@app, @env)
|
||||
}.to_not change { GC.enable }
|
||||
end
|
||||
|
||||
it 'doesn\'t leave the GC Profiler enabled if it was disabled previously' do
|
||||
GC::Profiler.enable
|
||||
|
||||
expect {
|
||||
@profiler.profile_gc_time(@app, @env)
|
||||
}.to_not change { GC::Profiler.enabled? }
|
||||
|
||||
GC::Profiler.disable
|
||||
end
|
||||
|
||||
|
||||
it 'keeps the GC Profiler disabled if it was disabled previously' do
|
||||
expect {
|
||||
@profiler.profile_gc_time(@app, @env)
|
||||
}.to_not change { GC::Profiler.enabled? }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#profile_gc' do
|
||||
it 'doesn\'t leave the GC enabled if it was disabled previously' do
|
||||
GC.disable
|
||||
|
||||
expect {
|
||||
@profiler.profile_gc(@app, @env)
|
||||
}.to_not change { GC.disable }
|
||||
|
||||
# Let's re-enable the GC for the rest of the test suite
|
||||
GC.enable
|
||||
end
|
||||
|
||||
it 'keeps the GC enabled if it was enabled previously' do
|
||||
expect {
|
||||
@profiler.profile_gc(@app, @env)
|
||||
}.to_not change { GC.enable }
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,59 @@
|
|||
require 'spec_helper'
|
||||
require 'rack-mini-profiler'
|
||||
require 'mini_profiler/page_timer_struct'
|
||||
require 'mini_profiler/storage/abstract_store'
|
||||
require 'mini_profiler/storage/memcache_store'
|
||||
|
||||
describe Rack::MiniProfiler::MemcacheStore do
|
||||
|
||||
context 'page struct' do
|
||||
|
||||
before do
|
||||
@store = Rack::MiniProfiler::MemcacheStore.new
|
||||
end
|
||||
|
||||
describe 'storage' do
|
||||
|
||||
it 'can store a PageStruct and retrieve it' do
|
||||
page_struct = Rack::MiniProfiler::PageTimerStruct.new({})
|
||||
page_struct['Id'] = "XYZ"
|
||||
page_struct['Random'] = "random"
|
||||
@store.save(page_struct)
|
||||
page_struct = @store.load("XYZ")
|
||||
page_struct['Random'].should == "random"
|
||||
page_struct['Id'].should == "XYZ"
|
||||
end
|
||||
|
||||
it 'can list unviewed items for a user' do
|
||||
@store.set_unviewed('a', 'XYZ')
|
||||
@store.set_unviewed('a', 'ABC')
|
||||
@store.get_unviewed_ids('a').length.should == 2
|
||||
@store.get_unviewed_ids('a').include?('XYZ').should be_true
|
||||
@store.get_unviewed_ids('a').include?('ABC').should be_true
|
||||
end
|
||||
|
||||
it 'can set an item to viewed once it is unviewed' do
|
||||
@store.set_unviewed('a', 'XYZ')
|
||||
@store.set_unviewed('a', 'ABC')
|
||||
@store.set_viewed('a', 'XYZ')
|
||||
@store.get_unviewed_ids('a').should == ['ABC']
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'passing in a Memcache client' do
|
||||
describe 'client' do
|
||||
it 'uses the passed in object rather than creating a new one' do
|
||||
client = double("memcache-client")
|
||||
store = Rack::MiniProfiler::MemcacheStore.new(:client => client)
|
||||
|
||||
client.should_receive(:get)
|
||||
Dalli::Client.should_not_receive(:new)
|
||||
store.load("XYZ")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
require 'spec_helper'
|
||||
require 'mini_profiler/storage/abstract_store'
|
||||
require 'mini_profiler/storage/memory_store'
|
||||
|
||||
describe Rack::MiniProfiler::MemoryStore do
|
||||
|
||||
context 'page struct' do
|
||||
|
||||
before do
|
||||
@store = Rack::MiniProfiler::MemoryStore.new
|
||||
end
|
||||
|
||||
describe 'storage' do
|
||||
|
||||
it 'can store a PageStruct and retrieve it' do
|
||||
page_struct = {'Id' => "XYZ", 'Random' => "random"}
|
||||
@store.save(page_struct)
|
||||
page_struct = @store.load("XYZ")
|
||||
page_struct['Id'].should == "XYZ"
|
||||
page_struct['Random'].should == "random"
|
||||
end
|
||||
|
||||
it 'can list unviewed items for a user' do
|
||||
@store.set_unviewed('a', 'XYZ')
|
||||
@store.set_unviewed('a', 'ABC')
|
||||
@store.get_unviewed_ids('a').should == ['XYZ', 'ABC']
|
||||
end
|
||||
|
||||
it 'can set an item to viewed once it is unviewed' do
|
||||
@store.set_unviewed('a', 'XYZ')
|
||||
@store.set_unviewed('a', 'ABC')
|
||||
@store.set_viewed('a', 'XYZ')
|
||||
@store.get_unviewed_ids('a').should == ['ABC']
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,33 @@
|
|||
require 'spec_helper'
|
||||
require 'rack-mini-profiler'
|
||||
|
||||
describe Rack::MiniProfiler::PageTimerStruct do
|
||||
|
||||
before do
|
||||
@page = Rack::MiniProfiler::PageTimerStruct.new({})
|
||||
end
|
||||
|
||||
it 'has an Id' do
|
||||
@page['Id'].should_not be_nil
|
||||
end
|
||||
|
||||
it 'has a Root' do
|
||||
@page['Root'].should_not be_nil
|
||||
end
|
||||
|
||||
describe 'to_json' do
|
||||
before do
|
||||
@json = @page.to_json
|
||||
@deserialized = ::JSON.parse(@json)
|
||||
end
|
||||
|
||||
it 'has a Started element' do
|
||||
@deserialized['Started'].should_not be_nil
|
||||
end
|
||||
|
||||
it 'has a DurationMilliseconds element' do
|
||||
@deserialized['DurationMilliseconds'].should_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,131 @@
|
|||
require 'spec_helper'
|
||||
require 'rack-mini-profiler'
|
||||
|
||||
describe Rack::MiniProfiler do
|
||||
|
||||
describe 'unique id' do
|
||||
|
||||
before do
|
||||
@unique = Rack::MiniProfiler.generate_id
|
||||
end
|
||||
|
||||
it 'is not nil' do
|
||||
@unique.should_not be_nil
|
||||
end
|
||||
|
||||
it 'is not empty' do
|
||||
@unique.should_not be_empty
|
||||
end
|
||||
|
||||
describe 'configuration' do
|
||||
|
||||
it 'allows us to set configuration settings' do
|
||||
Rack::MiniProfiler.config.auto_inject = false
|
||||
Rack::MiniProfiler.config.auto_inject.should == false
|
||||
end
|
||||
|
||||
it 'allows us to start the profiler disabled' do
|
||||
Rack::MiniProfiler.config.enabled = false
|
||||
Rack::MiniProfiler.config.enabled.should == false
|
||||
end
|
||||
|
||||
it 'can reset the settings' do
|
||||
Rack::MiniProfiler.config.auto_inject = false
|
||||
Rack::MiniProfiler.reset_config
|
||||
Rack::MiniProfiler.config.auto_inject.should be_true
|
||||
end
|
||||
|
||||
describe 'base_url_path' do
|
||||
it 'adds a trailing slash onto the base_url_path' do
|
||||
profiler = Rack::MiniProfiler.new(nil, :base_url_path => "/test-resource")
|
||||
profiler.config.base_url_path.should == "/test-resource/"
|
||||
end
|
||||
|
||||
it "doesn't add the trailing slash when it's already there" do
|
||||
profiler = Rack::MiniProfiler.new(nil, :base_url_path => "/test-resource/")
|
||||
profiler.config.base_url_path.should == "/test-resource/"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
describe 'profile method' do
|
||||
before do
|
||||
Rack::MiniProfiler.create_current
|
||||
class TestClass
|
||||
def foo(bar,baz)
|
||||
return [bar, baz, yield]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'should not destroy a method' do
|
||||
Rack::MiniProfiler.profile_method TestClass, :foo
|
||||
TestClass.new.foo("a","b"){"c"}.should == ["a","b","c"]
|
||||
Rack::MiniProfiler.unprofile_method TestClass, :foo
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'step' do
|
||||
|
||||
describe 'basic usage' do
|
||||
it 'yields the block given' do
|
||||
Rack::MiniProfiler.create_current
|
||||
Rack::MiniProfiler.step('test') { "mini profiler" }.should == "mini profiler"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe 'typical usage' do
|
||||
before(:all) do
|
||||
Rack::MiniProfiler.create_current
|
||||
Time.now = Time.new
|
||||
Time.now += 1
|
||||
Rack::MiniProfiler.step('outer') {
|
||||
Time.now += 2
|
||||
Rack::MiniProfiler.step('inner') {
|
||||
Time.now += 3
|
||||
}
|
||||
Time.now += 4
|
||||
}
|
||||
@page_struct = Rack::MiniProfiler.current.page_struct
|
||||
@root = @page_struct.root
|
||||
@root.record_time
|
||||
|
||||
@outer = @page_struct.root.children[0]
|
||||
@inner = @outer.children[0]
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
Time.back_to_normal
|
||||
end
|
||||
|
||||
it 'measures total duration correctly' do
|
||||
@page_struct.duration_ms.to_i.should == 10 * 1000
|
||||
end
|
||||
|
||||
it 'measures outer start time correctly' do
|
||||
@outer.start_ms.to_i.should == 1 * 1000
|
||||
end
|
||||
|
||||
it 'measures outer duration correctly' do
|
||||
@outer.duration_ms.to_i.should == 9 * 1000
|
||||
end
|
||||
|
||||
it 'measures inner start time correctly' do
|
||||
@inner.start_ms.to_i.should == 3 * 1000
|
||||
end
|
||||
|
||||
it 'measures inner duration correctly' do
|
||||
@inner.duration_ms.to_i.should == 3 * 1000
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
require 'spec_helper'
|
||||
require 'rack-mini-profiler'
|
||||
require 'mini_profiler/page_timer_struct'
|
||||
require 'mini_profiler/storage/abstract_store'
|
||||
require 'mini_profiler/storage/redis_store'
|
||||
|
||||
describe Rack::MiniProfiler::RedisStore do
|
||||
|
||||
context 'establishing a connection to something other than the default' do
|
||||
before do
|
||||
@store = Rack::MiniProfiler::RedisStore.new(:db=>2)
|
||||
end
|
||||
|
||||
describe "connection" do
|
||||
it 'can still store the resulting value' do
|
||||
page_struct = Rack::MiniProfiler::PageTimerStruct.new({})
|
||||
page_struct['Id'] = "XYZ"
|
||||
page_struct['Random'] = "random"
|
||||
@store.save(page_struct)
|
||||
end
|
||||
|
||||
it 'uses the correct db' do
|
||||
# redis is private, and possibly should remain so?
|
||||
underlying_client = @store.send(:redis).client
|
||||
|
||||
underlying_client.db.should == 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'passing in a Redis connection' do
|
||||
describe 'connection' do
|
||||
it 'uses the passed in object rather than creating a new one' do
|
||||
connection = double("redis-connection")
|
||||
store = Rack::MiniProfiler::RedisStore.new(:connection => connection)
|
||||
|
||||
connection.should_receive(:get)
|
||||
Redis.should_not_receive(:new)
|
||||
store.load("XYZ")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'page struct' do
|
||||
|
||||
before do
|
||||
@store = Rack::MiniProfiler::RedisStore.new(nil)
|
||||
end
|
||||
|
||||
describe 'storage' do
|
||||
|
||||
it 'can store a PageStruct and retrieve it' do
|
||||
page_struct = Rack::MiniProfiler::PageTimerStruct.new({})
|
||||
page_struct['Id'] = "XYZ"
|
||||
page_struct['Random'] = "random"
|
||||
@store.save(page_struct)
|
||||
page_struct = @store.load("XYZ")
|
||||
page_struct['Random'].should == "random"
|
||||
page_struct['Id'].should == "XYZ"
|
||||
end
|
||||
|
||||
it 'can list unviewed items for a user' do
|
||||
@store.set_unviewed('a', 'XYZ')
|
||||
@store.set_unviewed('a', 'ABC')
|
||||
@store.get_unviewed_ids('a').should =~ ['XYZ', 'ABC']
|
||||
end
|
||||
|
||||
it 'can set an item to viewed once it is unviewed' do
|
||||
@store.set_unviewed('a', 'XYZ')
|
||||
@store.set_unviewed('a', 'ABC')
|
||||
@store.set_viewed('a', 'XYZ')
|
||||
@store.get_unviewed_ids('a').should == ['ABC']
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,148 @@
|
|||
require 'spec_helper'
|
||||
require 'rack-mini-profiler'
|
||||
|
||||
describe Rack::MiniProfiler::RequestTimerStruct do
|
||||
|
||||
def new_page
|
||||
Rack::MiniProfiler::PageTimerStruct.new({})
|
||||
end
|
||||
|
||||
before do
|
||||
@name = 'cool request'
|
||||
@request = Rack::MiniProfiler::RequestTimerStruct.createRoot(@name, new_page)
|
||||
end
|
||||
|
||||
it 'sets IsRoot to true' do
|
||||
@request['IsRoot'].should be_true
|
||||
end
|
||||
|
||||
it 'has an Id' do
|
||||
@request['Id'].should_not be_nil
|
||||
end
|
||||
|
||||
it 'has a Root' do
|
||||
@request['Name'].should == @name
|
||||
end
|
||||
|
||||
it 'begins with a children duration of 0' do
|
||||
@request.children_duration.should == 0
|
||||
end
|
||||
|
||||
it 'has a false HasChildren attribute' do
|
||||
@request['HasChildren'].should be_false
|
||||
end
|
||||
|
||||
it 'has an empty Children attribute' do
|
||||
@request['Children'].should be_empty
|
||||
end
|
||||
|
||||
it 'has a depth of 0' do
|
||||
@request['Depth'].should == 0
|
||||
end
|
||||
|
||||
it 'has a false HasSqlTimings attribute' do
|
||||
@request['HasSqlTimings'].should be_false
|
||||
end
|
||||
|
||||
it 'has no sql timings at first' do
|
||||
@request['SqlTimings'].should be_empty
|
||||
end
|
||||
|
||||
it 'has a 0 for SqlTimingsDurationMilliseconds' do
|
||||
@request['SqlTimingsDurationMilliseconds'].should == 0
|
||||
end
|
||||
|
||||
describe 'add SQL' do
|
||||
|
||||
before do
|
||||
@page = new_page
|
||||
@request.add_sql("SELECT 1 FROM users", 77, @page)
|
||||
end
|
||||
|
||||
it 'has a true HasSqlTimings attribute' do
|
||||
@request['HasSqlTimings'].should be_true
|
||||
end
|
||||
|
||||
it 'has the SqlTiming object' do
|
||||
@request['SqlTimings'].should_not be_empty
|
||||
end
|
||||
|
||||
it 'has a child with the ParentTimingId of the request' do
|
||||
@request['SqlTimings'][0]['ParentTimingId'].should == @request['Id']
|
||||
end
|
||||
|
||||
it 'increases SqlTimingsDurationMilliseconds' do
|
||||
@request['SqlTimingsDurationMilliseconds'].should == 77
|
||||
end
|
||||
|
||||
it "increases the page's " do
|
||||
@page['DurationMillisecondsInSql'].should == 77
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'record time' do
|
||||
|
||||
describe 'add children' do
|
||||
|
||||
before do
|
||||
@child = @request.add_child('child')
|
||||
@child.record_time(1111)
|
||||
end
|
||||
|
||||
it 'has a IsRoot value of false' do
|
||||
@child['IsRoot'].should be_false
|
||||
end
|
||||
|
||||
it 'has a true HasChildren attribute' do
|
||||
@request['HasChildren'].should be_true
|
||||
end
|
||||
|
||||
it 'has the child in the Children attribute' do
|
||||
@request['Children'].should == [@child]
|
||||
end
|
||||
|
||||
it 'assigns its Id to the child' do
|
||||
@child['ParentTimingId'].should == @request['Id']
|
||||
end
|
||||
|
||||
it 'assigns a depth of 1 to the child' do
|
||||
@child['Depth'].should == 1
|
||||
end
|
||||
|
||||
it 'increases the children duration' do
|
||||
@request.children_duration.should == 1111
|
||||
end
|
||||
|
||||
it 'marks short timings as trivial' do
|
||||
@request.record_time(1)
|
||||
@request['IsTrivial'].should be_true
|
||||
end
|
||||
|
||||
|
||||
describe 'record time on parent' do
|
||||
before do
|
||||
@request.record_time(1234)
|
||||
end
|
||||
|
||||
it "is not a trivial query" do
|
||||
@request['IsTrivial'].should be_false
|
||||
end
|
||||
|
||||
it 'has stores the recorded time in DurationMilliseconds' do
|
||||
@request['DurationMilliseconds'].should == 1234
|
||||
end
|
||||
|
||||
it 'calculates DurationWithoutChildrenMilliseconds without the children timings' do
|
||||
@request['DurationWithoutChildrenMilliseconds'].should == 123
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
|
||||
end
|
|
@ -0,0 +1,80 @@
|
|||
require 'spec_helper'
|
||||
require 'rack-mini-profiler'
|
||||
|
||||
describe Rack::MiniProfiler::SqlTimerStruct do
|
||||
|
||||
describe 'valid sql timer' do
|
||||
before do
|
||||
@sql = Rack::MiniProfiler::SqlTimerStruct.new("SELECT * FROM users", 200, Rack::MiniProfiler::PageTimerStruct.new({}), nil)
|
||||
end
|
||||
|
||||
it 'has an ExecuteType' do
|
||||
@sql['ExecuteType'].should_not be_nil
|
||||
end
|
||||
|
||||
it 'has a FormattedCommandString' do
|
||||
@sql['FormattedCommandString'].should_not be_nil
|
||||
end
|
||||
|
||||
it 'has a StackTraceSnippet' do
|
||||
@sql['StackTraceSnippet'].should_not be_nil
|
||||
end
|
||||
|
||||
it 'has a StartMilliseconds' do
|
||||
@sql['StartMilliseconds'].should_not be_nil
|
||||
end
|
||||
|
||||
it 'has a DurationMilliseconds' do
|
||||
@sql['DurationMilliseconds'].should_not be_nil
|
||||
end
|
||||
|
||||
it 'has a IsDuplicate' do
|
||||
@sql['IsDuplicate'].should_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
describe 'backtrace' do
|
||||
it 'has a snippet' do
|
||||
sql = Rack::MiniProfiler::SqlTimerStruct.new("SELECT * FROM users", 200, Rack::MiniProfiler::PageTimerStruct.new({}), nil)
|
||||
sql['StackTraceSnippet'].should_not be nil
|
||||
end
|
||||
|
||||
it 'includes rspec in the trace (default is no filter)' do
|
||||
sql = Rack::MiniProfiler::SqlTimerStruct.new("SELECT * FROM users", 200, Rack::MiniProfiler::PageTimerStruct.new({}), nil)
|
||||
sql['StackTraceSnippet'].should match /rspec/
|
||||
end
|
||||
|
||||
it "doesn't include rspec if we filter for only app" do
|
||||
Rack::MiniProfiler.config.backtrace_includes = [/\/app/]
|
||||
sql = Rack::MiniProfiler::SqlTimerStruct.new("SELECT * FROM users", 200, Rack::MiniProfiler::PageTimerStruct.new({}), nil)
|
||||
sql['StackTraceSnippet'].should_not match /rspec/
|
||||
end
|
||||
|
||||
it "includes rspec if we filter for it" do
|
||||
Rack::MiniProfiler.config.backtrace_includes = [/\/(app|rspec)/]
|
||||
sql = Rack::MiniProfiler::SqlTimerStruct.new("SELECT * FROM users", 200, Rack::MiniProfiler::PageTimerStruct.new({}), nil)
|
||||
sql['StackTraceSnippet'].should match /rspec/
|
||||
end
|
||||
|
||||
it "ingores rspec if we specifically ignore it" do
|
||||
Rack::MiniProfiler.config.backtrace_ignores = [/\/rspec/]
|
||||
sql = Rack::MiniProfiler::SqlTimerStruct.new("SELECT * FROM users", 200, Rack::MiniProfiler::PageTimerStruct.new({}), nil)
|
||||
sql['StackTraceSnippet'].should_not match /rspec/
|
||||
end
|
||||
|
||||
it "should omit the backtrace if the query takes less than the threshold time" do
|
||||
Rack::MiniProfiler.config.backtrace_threshold_ms = 100
|
||||
sql = Rack::MiniProfiler::SqlTimerStruct.new("SELECT * FROM users", 50, Rack::MiniProfiler::PageTimerStruct.new({}), nil)
|
||||
sql['StackTraceSnippet'].should be nil
|
||||
end
|
||||
|
||||
it "should not omit the backtrace if the query takes more than the threshold time" do
|
||||
Rack::MiniProfiler.config.backtrace_threshold_ms = 100
|
||||
sql = Rack::MiniProfiler::SqlTimerStruct.new("SELECT * FROM users", 200, Rack::MiniProfiler::PageTimerStruct.new({}), nil)
|
||||
sql['StackTraceSnippet'].should_not be nil
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,54 @@
|
|||
require 'spec_helper'
|
||||
require 'mini_profiler/timer_struct'
|
||||
|
||||
require 'json'
|
||||
|
||||
describe Rack::MiniProfiler::TimerStruct do
|
||||
|
||||
before do
|
||||
@timer = Rack::MiniProfiler::TimerStruct.new('Mini' => 'Profiler')
|
||||
end
|
||||
|
||||
it 'has the the Mini attribute' do
|
||||
@timer['Mini'].should == 'Profiler'
|
||||
end
|
||||
|
||||
it 'allows us to set any attribute we want' do
|
||||
@timer['Hello'] = 'World'
|
||||
@timer['Hello'].should == 'World'
|
||||
end
|
||||
|
||||
describe 'to_json' do
|
||||
|
||||
before do
|
||||
@timer['IceIce'] = 'Baby'
|
||||
@json = @timer.to_json
|
||||
end
|
||||
|
||||
it 'has a JSON value' do
|
||||
@json.should_not be_nil
|
||||
end
|
||||
|
||||
it 'should not add a second (nil) argument if no arguments were passed' do
|
||||
::JSON.should_receive( :generate ).once.with( @timer.attributes, :max_nesting => 100 ).and_return( nil )
|
||||
@timer.to_json
|
||||
end
|
||||
|
||||
describe 'deserialized' do
|
||||
|
||||
before do
|
||||
@deserialized = ::JSON.parse(@json)
|
||||
end
|
||||
|
||||
it 'produces a hash' do
|
||||
@deserialized.is_a?(Hash).should be_true
|
||||
end
|
||||
|
||||
it 'has the element we added' do
|
||||
@deserialized['IceIce'].should == 'Baby'
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue