Merge remote-tracking branch 'origin/szzh' into szzh

Conflicts:
	config/locales/zh.yml
This commit is contained in:
sw 2014-05-28 15:24:37 +08:00
commit 218fb3f831
135 changed files with 11958 additions and 243 deletions

16
Gemfile
View File

@ -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`

View File

@ -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!

View File

@ -149,7 +149,6 @@ class AdminController < ApplicationController
respond_to do |format|
format.html {
@groups = Group.all.sort
render :layout => @user_base_tag
}
format.api
end
@ -196,4 +195,43 @@ class AdminController < ApplicationController
[:text_rmagick_available, Object.const_defined?(:Magick)]
]
end
#管理功能用户列表的搜索
def search
sort_init 'login', 'asc'
sort_update %w(login firstname lastname mail admin created_on last_login_on)
case params[:format]
when 'xml', 'json'
@offset, @limit = api_offset_and_limit({:limit => 15})
else
@limit = 15#per_page_option
end
@status = params[:status] || 1
has = {
"show_changesets" => true
}
scope = User.logged.status(@status)
scope = scope.like(params[:name]) if params[:name].present?
@user_count = scope.count
@user_pages = Paginator.new @user_count, @limit, params['page']
@user_base_tag = params[:id] ? 'base_users':'base'
@offset ||= @user_pages.reverse_offset
unless @offset == 0
@users = scope.offset(@offset).limit(@limit).all.reverse
else
limit = @user_count % @limit
if limit == 0
limit = @limit
end
@users = scope.offset(@offset).limit(limit).all.reverse
end
respond_to do |format|
format.html {
@groups = Group.all.sort
}
format.api
end
end
end

View File

@ -52,9 +52,14 @@ class FilesController < ApplicationController
end
def create
if params[:tag_name]
if params[:add_tag]
@addTag=true
#render :back
tag_saveEx
render :text =>"success"
#render :text =>"success"
respond_to do |format|
format.js
end
else
@addTag=false
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
@ -64,7 +69,27 @@ class FilesController < ApplicationController
if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added')
Mailer.attachments_added(attachments[:files]).deliver
end
# 临时用
sort_init 'created_on', 'desc'
sort_update 'created_on' => "#{Attachment.table_name}.created_on",
'filename' => "#{Attachment.table_name}.filename",
'size' => "#{Attachment.table_name}.filesize",
'downloads' => "#{Attachment.table_name}.downloads"
@containers = [ Project.includes(:attachments).reorder("#{Attachment.table_name}.created_on DESC").find(@project.id)] #modify by Long Jun
@containers += @project.versions.includes(:attachments).reorder("#{Attachment.table_name}.created_on DESC").all.sort
@attachtype = 0
@contenttype = 0
respond_to do |format|
format.js
format.html {
redirect_to project_files_path(@project)
}
end
end
end

View File

@ -76,7 +76,6 @@ class MembersController < ApplicationController
# ProjectInfo.create(:name => "test", :user_id => 123)
end
## end
AppliedProject.deleteappiled(user_id, @project.id)
end
else
members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => params[:membership][:user_id])
@ -95,6 +94,13 @@ class MembersController < ApplicationController
end
end
if members.present? && members.all? {|m| m.valid? }
members.each do |member|
AppliedProject.deleteappiled(member.user_id, @project.id)
end
end
respond_to do |format|
format.html { redirect_to_settings_in_projects }
format.js { @members = members;@applied_members = applied_members; }

View File

@ -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

View File

@ -65,13 +65,13 @@ 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)
end
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 20)
@events_by_day = events.group_by(&:event_date)
#events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 20)
#@events_by_day = events.group_by(&:event_date)
@state = 0
@ -565,7 +565,7 @@ class UsersController < ApplicationController
end
def watch_projects
@watch_projects = Project.joins(:watchers).where("project_type <>? and watchable_type = ? and user_id = ?", '1','Project', @user.id)
@watch_projects = Project.joins(:watchers).where("project_type <>? and watchable_type = ? and `watchers`.user_id = ?", '1','Project', @user.id)
@state = 1
respond_to do |format|
format.html {
@ -689,7 +689,7 @@ class UsersController < ApplicationController
end
def tag_saveEx
@tags = params[:tag_name][:name]
@tags = params[:tag_name]
@obj_id = params[:obj_id]
@obj_flag = params[:obj_flag]

View File

@ -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)
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
@ -1342,7 +1347,18 @@ module ApplicationHelper
def bootstrap_head
tags = stylesheet_link_tag('bootstrap/bootstrap.min', 'bootstrap/bootstrap-theme.min')
tags << javascript_include_tag('bootstrap/bootstrap.min', 'bootstrap/jquery.transition.min')
tags << javascript_include_tag('bootstrap/affix')
tags << javascript_include_tag('bootstrap/alert')
tags << javascript_include_tag('bootstrap/button')
tags << javascript_include_tag('bootstrap/carousel')
tags << javascript_include_tag('bootstrap/collapse')
tags << javascript_include_tag('bootstrap/dropdown')
tags << javascript_include_tag('bootstrap/modal')
tags << javascript_include_tag('bootstrap/popover')
tags << javascript_include_tag('bootstrap/scrollspy')
tags << javascript_include_tag('bootstrap/tab')
tags << javascript_include_tag('bootstrap/tooltip')
tags << javascript_include_tag('bootstrap/transition')
tags
end

View File

@ -51,3 +51,7 @@ module TagsHelper
end
end
def tagname_val
("#tag_name_name").value
end

View File

@ -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}")

View File

@ -0,0 +1,68 @@
<% if User.current.admin? %>
<div class="contextual">
<%= link_to l(:label_user_new), new_user_path, :class => 'icon icon-add' %>
</div>
<h3><%= l(:label_user_plural)%></h3>
<%= form_tag(:controller => 'admin', :action => 'search', :method => :get) do %>
<fieldset>
<legend>
<%= l(:label_filter_plural) %>
</legend>
<label for='status'><%= l(:field_status) %>:</label>
<%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
<% if @groups.present? %>
<label for='group_id'><%= l(:label_group) %>:</label>
<%= select_tag 'group_id', content_tag('option') + options_from_collection_for_select(@groups, :id, :name, params[:group_id].to_i), :onchange => "this.form.submit(); return false;" %>
<% end %>
<label for='name'><%= l(:label_user) %>:</label>
<%= text_field_tag 'name', params[:name], :size => 30 %>
<%= submit_tag l(:label_search), :class => "small", :name => nil %>
</fieldset>
<% end %>
&nbsp;
<div class="autoscroll">
<table class="list">
<thead>
<tr>
<%= sort_header_tag('login', :caption => l(:field_login)) %>
<%= sort_header_tag('firstname', :caption => l(:field_firstname)) %>
<%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>
<%= sort_header_tag('mail', :caption => l(:field_mail)) %>
<%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %>
<%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
<%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %>
<th></th>
</tr>
</thead>
<tbody>
<% for user in @users -%>
<tr class="<%= user.css_classes %> <%= cycle("odd", "even") %>">
<td class="username"><%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %></td>
<td class="firstname"><%= h(user.firstname) %></td>
<td class="lastname"><%= h(user.lastname) %></td>
<td class="email"><%= mail_to(h(user.mail)) %></td>
<td align="center"><%= checked_image user.admin? %></td>
<td class="created_on" align="center"><%= format_time(user.created_on) %></td>
<td class="last_login_on" align="center"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>
<td class="buttons"> <%= change_status_link(user) %>
<%= delete_link user_path(user, :back_url => users_path(params)) unless User.current == user %> </td>
</tr>
<% end -%>
</tbody>
</table>
</div>
<div class="pagination">
<ul>
<%= pagination_links_full @user_pages, @user_count %>
</ul>
</div>
<% html_title(l(:label_user_plural)) -%>
<% end %>

View File

@ -5,7 +5,7 @@
<h3><%= l(:label_user_plural)%></h3>
<%= form_tag(:controller => 'users', :action => 'search', :method => :get) do %>
<%= form_tag(:controller => 'admin', :action => 'search', :method => :get) do %>
<fieldset>
<legend>
<%= l(:label_filter_plural) %>

View File

@ -12,6 +12,5 @@ fileSpan.find('a.remove-upload')
})
.off('click');
var divattach = fileSpan.find('div.div_attachments');
divattach.html('<%#= j(render :partial => 'tags/tagEx', :locals => {:obj => @attachment, :object_flag => "6"})%>');
divattach.html('<%= j(render :partial => 'tags/tagEx', :locals => {:obj => @attachment, :object_flag => "6"})%>');
<% end %>

View File

@ -2,7 +2,7 @@
<% versions = project.versions.sort %>
<% attachmenttypes = project.attachmenttypes %>
<%= error_messages_for 'attachment' %>
<%= form_tag(project_files_path(project), :multipart => true,:name=>"upload_form", :class => "tabular") do %>
<%= form_tag(project_files_path(project), :multipart => true,:remote => true,:method => :post,:name=>"upload_form", :class => "tabular") do %>
<div class="box">
<p>
<table>

View File

@ -1,37 +1,36 @@
<%
request.headers['REQUEST_URI'] = "" if request.headers['REQUEST_URI'].nil?
realUrl = request.original_url
if (realUrl.match(/forge\.trustie\.net\/*/))
request.headers['REQUEST_URI'] = "" if request.headers['REQUEST_URI'].nil?
realUrl = request.original_url
if (realUrl.match(/forge\.trustie\.net\/*/))
@nav_dispaly_project_label = 1
@nav_dispaly_forum_label = 1
elsif (realUrl.match(/course\.trustie\.net\/*/))
elsif (realUrl.match(/course\.trustie\.net\/*/))
@nav_dispaly_course_all_label = 1
@nav_dispaly_forum_label = 1
@nav_dispaly_course_label = nil
@nav_dispaly_store_all_label = 1
elsif (realUrl.match(/user\.trustie\.net\/*/))
elsif (realUrl.match(/user\.trustie\.net\/*/))
@nav_dispaly_home_path_label = 1
@nav_dispaly_main_course_label = 1
@nav_dispaly_main_project_label = 1
@nav_dispaly_main_contest_label = 1
elsif (realUrl.match(/contest\.trustie\.net\/*/))
elsif (realUrl.match(/contest\.trustie\.net\/*/))
@nav_dispaly_contest_label = 1
@nav_dispaly_store_all_label = 1
else
else
@nav_dispaly_project_all_label = 1
@nav_dispaly_course_all_label = 1
@nav_dispaly_forum_label = 1
@nav_dispaly_bid_label = 1
@nav_dispaly_contest_label = 1
@nav_dispaly_store_all_label = 1
@nav_dispaly_user_label = 1
end
end
%>
<nav class="navbar navbar-default" role="navigation">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
@ -78,5 +77,6 @@
<%= bootstrap_render_menu :account_menu -%>
</ul>
</div>
</nav>
</div>
</div>

View File

@ -16,21 +16,8 @@
<%= yield :header_tags -%>
</head>
<body>
<%= render :partial => 'layouts/bootstrap_base_header' %>
<div class="container">
<div class="row">
<div class="col-xs-6 col-sm-3">.col-xs-6 .col-sm-3</div>
<div class="col-xs-6 col-sm-3">.col-xs-6 .col-sm-3</div>
<!-- Add the extra clearfix for only the required viewport -->
<div class="clearfix visible-xs"></div>
<div class="col-xs-6 col-sm-3">.col-xs-6 .col-sm-3</div>
<div class="col-xs-6 col-sm-3">.col-xs-6 .col-sm-3</div>
</div>
<%= render_flash_messages %>
<%= yield %>
</div>

View File

@ -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) %>

View File

@ -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 %>

View File

@ -1,13 +1,3 @@
<script>
window.onload=function (){
$('#upload_form').submit(function() {
$(this).ajaxSubmit( {
target : '#tags_show'
});
return false;
});
}
</script>
<div id="tags">
<%#begin
@ -60,11 +50,7 @@
<%= text_field "tag_name" ,"name"%>
<input id="object_id" type="text" size="20" name="object_id" value='<%=obj.id%>' class="hidden">
<input id="object_flag" type="text" size="20" name="object_flag" value='<%=object_flag%>' class="hidden">
<%= button_tag "增加", :type=>"button", :onclick=>"tagAddClick(tags_show-"+obj.class.to_s + "-" +obj.id.to_s + ","+ obj.id.to_s + "," + object_flag.to_s + ")" %>
<%#= f.text_field :object_id,:value=> obj.id,:style=>"display:none"%>
<%#= f.text_field :object_flag,:value=> object_flag,:style=>"display:none"%>
<input type="image" name="button" onclick="submitForm('<%=obj.id%>,<%=object_flag%>')" src="/commit.png"/>
<%= submit_tag "增加", :name=>"add_tag" %>
<%= submit_tag l(:button_add), :name=>"add_tag",:remote=>"false", :format => 'js' %>
<div class='hidden'>
<% preTags = @preTags.nil? ? [] : @preTags %>
<% preTags.each do |tag|%>
@ -113,13 +99,15 @@
<script language="JavaScript">
function submitForm(objId,objTag){
function submitForm123(objId,objTag){
alert("OK");
$.ajax({
type :"POST",
type :"Get",
url :'<%= users_tag_saveEx_path%>',
remote:"true",
format:"js",
data: {
tagname: $('tag_name').value,
tagname: $('tag_name_name').val(),
obj_id: encodeURIComponent(objId),
obj_flag:encodeURIComponent(objTag)
},
@ -150,7 +138,7 @@
type :"POST",
url :'<%= users_tag_saveEx_path%>',
data: {
tagname: $('tag_name').value,
tagname: $('tag_name_name').val(),
obj_id: encodeURIComponent(objId),
obj_flag:encodeURIComponent(objTag)
},

View File

@ -1,30 +1,39 @@
<style type="text/css">
.courses_list{
font-size: 18px;
font-weight: 800;
margin: 40px 10px;
}
.homeworks{
font-size: 16px;
font-weight: 500;
margin: 5px 40px 20px;
}
.attach_item{
font-size: 14px;
font-weight: 400;
margin: auto 60px;
}
</style>
<% @courses.each do |course| %>
<div class="courses_list">
<%= course.name %>
<div class="alert alert-success">
<strong>Well done!</strong> You successfully read this important alert message.
</div>
<div class="alert alert-info">
<strong>Heads up!</strong> This alert needs your attention, but it's not super important.
</div>
<div class="alert alert-warning">
<strong>Warning!</strong> Best check yo self, you're not looking too good.
</div>
<div class="alert alert-danger">
<strong>Oh snap!</strong> Change a few things up and try submitting again.
</div>
<div class="page-header">
<div class="jumbotron">
<h1>课程作业列表</h1>
<p class="lead">测试页面提供</p>
</div>
<% @courses.each do |course| %>
<div class="courses_list row">
<div class="col-md-12">
<div class="page-header">
<h3> <%= course.name %> </h3>
</div>
<% course.homeworks.each do |homework| %>
<% homeworks_attach_path = [] %>
<div class="homeworks">
<div class="homeworks panel panel-default ">
<div class="homeworks panel-heading">
<div class="panel-title">
<%= link_to homework.name, respond_path(homework) %>(<%=homework.homeworks.count %>)<%#Bid%>
<div class="attach_item">
<%= link_to "package", zipdown_assort_path(obj_class: homework.class, obj_id: homework.id) %><br/>
<%= link_to "打包下载", zipdown_assort_path(obj_class: homework.class, obj_id: homework.id), :class => "btn btn-primary btn-sm" %><br/>
</div>
</div>
<div class="attach_item panel-body ">
<div class="col-md-offset-1">
<% homework.homeworks.each do |homeattach|%><%#homework.class == Bid %>
<% homeattach.attachments.each do |attach|%>
<%= link_to_attachment attach, author: true, :download => true %> (<%=attach.author%>)
@ -33,10 +42,13 @@
<% end %>
<% end %>
</div>
</div>
<%# 所有作业的文件列表%>
<!-- (<%=homeworks_attach_path.count%>):<%= homeworks_attach_path.to_json %> -->
</div>
<% end %>
</div>
</div>
<hr/>
<% end %>
</div>
<hr/>
<% end %>

View File

@ -75,7 +75,7 @@
<div class="pagination">
<ul>
<%= pagination_links_full @user_pages, @user_count %>
<ul>
</ul>
</div>
<!-- </p> -->
<% html_title(l(:label_user_plural)) -%>

View File

@ -1,71 +1,4 @@
<% if User.current.admin? %>
<div class="contextual">
<%= link_to l(:label_user_new), new_user_path, :class => 'icon icon-add' %>
</div>
<h3><%= l(:label_user_plural)%></h3>
<%= form_tag(:controller => 'users', :action => 'search', :method => :get) do %>
<fieldset>
<legend>
<%= l(:label_filter_plural) %>
</legend>
<label for='status'><%= l(:field_status) %>:</label>
<%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
<% if @groups.present? %>
<label for='group_id'><%= l(:label_group) %>:</label>
<%= select_tag 'group_id', content_tag('option') + options_from_collection_for_select(@groups, :id, :name, params[:group_id].to_i), :onchange => "this.form.submit(); return false;" %>
<% end %>
<label for='name'><%= l(:label_user) %>:</label>
<%= text_field_tag 'name', params[:name], :size => 30 %>
<%= submit_tag l(:label_search), :class => "small", :name => nil %>
</fieldset>
<% end %>
&nbsp;
<div class="autoscroll">
<table class="list">
<thead>
<tr>
<%= sort_header_tag('login', :caption => l(:field_login)) %>
<%= sort_header_tag('firstname', :caption => l(:field_firstname)) %>
<%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>
<%= sort_header_tag('mail', :caption => l(:field_mail)) %>
<%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %>
<%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
<%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %>
<th></th>
</tr>
</thead>
<tbody>
<% for user in @users -%>
<tr class="<%= user.css_classes %> <%= cycle("odd", "even") %>">
<td class="username"><%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %></td>
<td class="firstname"><%= h(user.firstname) %></td>
<td class="lastname"><%= h(user.lastname) %></td>
<td class="email"><%= mail_to(h(user.mail)) %></td>
<td align="center"><%= checked_image user.admin? %></td>
<td class="created_on" align="center"><%= format_time(user.created_on) %></td>
<td class="last_login_on" align="center"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>
<td class="buttons"> <%= change_status_link(user) %>
<%= delete_link user_path(user, :back_url => users_path(params)) unless User.current == user %> </td>
</tr>
<% end -%>
</tbody>
</table>
</div>
<div class="pagination">
<ul>
<%= pagination_links_full @user_pages, @user_count %>
<ul>
</div>
<% html_title(l(:label_user_plural)) -%>
<% else %>
<div class="top-content">
<%= form_tag(:controller => 'users', :action => 'search', :method => :get) do %>
@ -87,8 +20,9 @@
<td ><%=link_to l(:field_homepage), home_path %> > <%=link_to l(:label_software_user), :controller => 'users', :action => 'index' %></td>
</tr>
</table>
<% end %>
</div>
<% end %>
<div class="autoscroll">
<% if @users.size > 0 %>
@ -125,7 +59,7 @@
<div class="pagination">
<ul>
<%= pagination_links_full @user_pages, @user_count %>
<ul>
</ul>
</div>
<% html_title(l(:label_user_plural)) -%>
<% end -%>

View File

@ -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

View File

@ -184,6 +184,9 @@ RedmineApp::Application.routes.draw do
#added by young
resources :users do
collection do
match "tag_saveEx" , via: [:get, :post]
end
member do
match 'user_projects', :to => 'users#user_projects', :via => :get
match 'user_activities', :to => 'users#show', :via => :get, :as => "user_activities"
@ -235,7 +238,7 @@ RedmineApp::Application.routes.draw do
match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships'
################# added by william
match 'users/tag_save', :to => 'users#tag_save', :via => :post, :as => 'tag'
match 'users/tag_saveEx', :to => 'users#tag_saveEx', :via => :post
match 'users/tag_saveEx', :to => 'users#tag_saveEx', :via => [:get, :post]
post 'watchers/watch', :to => 'watchers#watch', :as => 'watch'
delete 'watchers/watch', :to => 'watchers#unwatch'
@ -521,6 +524,7 @@ RedmineApp::Application.routes.draw do
match 'admin', :controller => 'admin', :action => 'index', :via => :get
match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
match 'admin/users', :controller => 'admin', :action => 'users', :via => :get
match 'admin/search', :controller => 'admin', :action => 'search', :via => [:get, :post]
match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20140519074133) do
ActiveRecord::Schema.define(:version => 20140522025721) do
create_table "activities", :force => true do |t|
t.integer "act_id", :null => false
@ -23,9 +23,9 @@ ActiveRecord::Schema.define(:version => 20140519074133) do
add_index "activities", ["user_id", "act_type"], :name => "index_activities_on_user_id_and_act_type"
add_index "activities", ["user_id"], :name => "index_activities_on_user_id"
create_table "andoidcontests", :force => true do |t|
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
create_table "applied_projects", :force => true do |t|
t.integer "project_id", :null => false
t.integer "user_id", :null => false
end
create_table "apply_project_masters", :force => true do |t|
@ -57,11 +57,14 @@ ActiveRecord::Schema.define(:version => 20140519074133) do
add_index "attachments", ["container_id", "container_type"], :name => "index_attachments_on_container_id_and_container_type"
add_index "attachments", ["created_on"], :name => "index_attachments_on_created_on"
create_table "attachmentstypes", :force => true do |t|
t.integer "typeId", :null => false
create_table "attachmentstypes", :id => false, :force => true do |t|
t.integer "id", :null => false
t.integer "typeId"
t.string "typeName", :limit => 50
end
add_index "attachmentstypes", ["id"], :name => "id"
create_table "auth_sources", :force => true do |t|
t.string "type", :limit => 30, :default => "", :null => false
t.string "name", :limit => 60, :default => "", :null => false
@ -651,8 +654,8 @@ ActiveRecord::Schema.define(:version => 20140519074133) do
t.boolean "inherit_members", :default => false, :null => false
t.integer "project_type"
t.boolean "hidden_repo", :default => false, :null => false
t.integer "user_id"
t.integer "attachmenttype", :default => 1
t.integer "user_id"
end
add_index "projects", ["lft"], :name => "index_projects_on_lft"

View File

@ -0,0 +1,4 @@
language: ruby
rvm:
- 2.1.0
- 2.0.0

View File

@ -0,0 +1 @@
--markup markdown --no-private

View File

@ -0,0 +1,3 @@
# Changelog
See https://github.com/charliesome/better_errors/releases

10
lib/better_errors/Gemfile Normal file
View File

@ -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"

View File

@ -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.

103
lib/better_errors/README.md Normal file
View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 %>

View File

@ -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>&gt;&gt;</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 -->

View File

@ -0,0 +1,3 @@
module BetterErrors
VERSION = "1.1.0"
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,20 @@
one
two
three
four
five
six
seven
eight
nine
ten
eleven
twelve
thirteen
fourteen
fifteen
sixteen
seventeen
eighteen
nineteen
twenty

View File

@ -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

View File

@ -0,0 +1,5 @@
$: << File.expand_path("../../lib", __FILE__)
ENV["EDITOR"] = nil
require "better_errors"

View File

@ -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

View File

@ -0,0 +1,9 @@
language: ruby
rvm:
- 1.9.3
- 2.0.0
- 2.1.1
bundler_args: ""
services:
- redis
- memcached

View File

@ -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)

View File

@ -0,0 +1,3 @@
source 'http://rubygems.org'
gemspec

View File

@ -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.

View File

@ -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

View File

@ -0,0 +1,2 @@
Autotest.add_discovery { "rspec2" }

View File

@ -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;
}

View File

@ -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 += '&nbsp;';
}
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,"&amp;").replace(M,"&lt;").replace(N,"&gt;")}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,"&amp;").replace(M,"&lt;").replace(N,"&gt;").replace(X,"&quot;"),'"')}}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&#160;");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?"&#160;\r\n":h===7?"&#160;<br>\r":"&#160;\r":"&#160;<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=/&lt;/g,fa=/&gt;/g,ga=/&apos;/g,ha=/&quot;/g,ja=/&amp;/g,ia=/&nbsp;/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"])
;
}

View File

@ -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;
}
}
}
}

View File

@ -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 &lt; ${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} &mdash; ${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

View File

@ -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("&lt;").split(">").join("&gt;").split('"').join("&#34;").split("'").join("&#39;");
}
});
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);

View File

@ -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;}

View File

@ -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();
}
});
}
};

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
module Rack
class MiniProfiler
VERSION = '898a13ca6797c6bc1fee313e17d388b0'.freeze
end
end

View File

@ -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

View File

@ -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

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More