mini-mini-profiler 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Ruby/CHANGELOG +135 -0
- data/Ruby/README.md +161 -0
- data/Ruby/lib/html/flamegraph.html +325 -0
- data/Ruby/lib/html/includes.css +451 -0
- data/Ruby/lib/html/includes.js +945 -0
- data/Ruby/lib/html/includes.less +471 -0
- data/Ruby/lib/html/includes.tmpl +108 -0
- data/Ruby/lib/html/jquery.1.7.1.js +4 -0
- data/Ruby/lib/html/jquery.tmpl.js +486 -0
- data/Ruby/lib/html/list.css +9 -0
- data/Ruby/lib/html/list.js +38 -0
- data/Ruby/lib/html/list.tmpl +34 -0
- data/Ruby/lib/html/profile_handler.js +1 -0
- data/Ruby/lib/html/share.html +11 -0
- data/Ruby/lib/mini_profiler/client_settings.rb +65 -0
- data/Ruby/lib/mini_profiler/client_timer_struct.rb +78 -0
- data/Ruby/lib/mini_profiler/config.rb +57 -0
- data/Ruby/lib/mini_profiler/context.rb +11 -0
- data/Ruby/lib/mini_profiler/custom_timer_struct.rb +22 -0
- data/Ruby/lib/mini_profiler/flame_graph.rb +54 -0
- data/Ruby/lib/mini_profiler/gc_profiler.rb +107 -0
- data/Ruby/lib/mini_profiler/page_timer_struct.rb +58 -0
- data/Ruby/lib/mini_profiler/profiler.rb +544 -0
- data/Ruby/lib/mini_profiler/profiling_methods.rb +133 -0
- data/Ruby/lib/mini_profiler/request_timer_struct.rb +115 -0
- data/Ruby/lib/mini_profiler/sql_timer_struct.rb +58 -0
- data/Ruby/lib/mini_profiler/storage/abstract_store.rb +31 -0
- data/Ruby/lib/mini_profiler/storage/file_store.rb +111 -0
- data/Ruby/lib/mini_profiler/storage/memcache_store.rb +53 -0
- data/Ruby/lib/mini_profiler/storage/memory_store.rb +65 -0
- data/Ruby/lib/mini_profiler/storage/redis_store.rb +54 -0
- data/Ruby/lib/mini_profiler/timer_struct.rb +33 -0
- data/Ruby/lib/mini_profiler/version.rb +5 -0
- data/Ruby/lib/mini_profiler_rails/railtie.rb +107 -0
- data/Ruby/lib/patches/net_patches.rb +14 -0
- data/Ruby/lib/patches/sql_patches.rb +272 -0
- data/Ruby/lib/rack-mini-profiler.rb +7 -0
- data/mini-mini-profiler.gemspec +26 -0
- metadata +154 -0
@@ -0,0 +1,9 @@
|
|
1
|
+
tbody tr:nth-child(odd) { background-color:#eee; }
|
2
|
+
tbody tr:nth-child(even) { background-color:#fff; }
|
3
|
+
table { border: 0; border-spacing:0;}
|
4
|
+
tr {border: 0;}
|
5
|
+
.date {font-size: 11px; color: #666;}
|
6
|
+
td {padding: 8px;}
|
7
|
+
.time {text-align:center;}
|
8
|
+
thead tr {background-color: #bbb; color: #444; font-size: 12px;}
|
9
|
+
thead tr th { padding: 5px 15px;}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
var MiniProfiler = MiniProfiler || {};
|
2
|
+
MiniProfiler.list = {
|
3
|
+
init:
|
4
|
+
function (options) {
|
5
|
+
var $ = MiniProfiler.jQuery;
|
6
|
+
var opt = options || {};
|
7
|
+
|
8
|
+
var updateGrid = function (id) {
|
9
|
+
$.ajax({
|
10
|
+
url: options.path + 'results-list',
|
11
|
+
data: { "last-id": id },
|
12
|
+
dataType: 'json',
|
13
|
+
type: 'GET',
|
14
|
+
success: function (data) {
|
15
|
+
$('table tbody').append($("#rowTemplate").tmpl(data));
|
16
|
+
var oldId = id;
|
17
|
+
var oldData = data;
|
18
|
+
setTimeout(function () {
|
19
|
+
var newId = oldId;
|
20
|
+
if (oldData.length > 0) {
|
21
|
+
newId = oldData[oldData.length - 1].Id;
|
22
|
+
}
|
23
|
+
updateGrid(newId);
|
24
|
+
}, 4000);
|
25
|
+
}
|
26
|
+
});
|
27
|
+
}
|
28
|
+
|
29
|
+
MiniProfiler.path = options.path;
|
30
|
+
$.get(options.path + 'list.tmpl?v=' + options.version, function (data) {
|
31
|
+
if (data) {
|
32
|
+
$('body').append(data);
|
33
|
+
$('body').append($('#tableTemplate').tmpl());
|
34
|
+
updateGrid();
|
35
|
+
}
|
36
|
+
});
|
37
|
+
}
|
38
|
+
};
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<script id="tableTemplate" type="text/x-jquery-tmpl">
|
2
|
+
<table>
|
3
|
+
<thead>
|
4
|
+
<tr>
|
5
|
+
<th>Name</th>
|
6
|
+
<th>Started</th>
|
7
|
+
<th>Sql Duration</th>
|
8
|
+
<th>Total Duration</th>
|
9
|
+
<th>Request Start</th>
|
10
|
+
<th>Response Start</th>
|
11
|
+
<th>Dom Complete</th>
|
12
|
+
</tr>
|
13
|
+
</thead>
|
14
|
+
<tbody>
|
15
|
+
|
16
|
+
</tbody>
|
17
|
+
</table>
|
18
|
+
</script>
|
19
|
+
<script id="rowTemplate" type="text/x-jquery-tmpl">
|
20
|
+
<tr>
|
21
|
+
<td>
|
22
|
+
<a href="${MiniProfiler.path}results?id=${Id}">${Name}</a></td>
|
23
|
+
<td class="date">${MiniProfiler.renderDate(Started)}</td>
|
24
|
+
<td class="time">${DurationMillisecondsInSql}</td>
|
25
|
+
<td class="time">${DurationMilliseconds}</td>
|
26
|
+
{{if ClientTimings}}
|
27
|
+
<td class="time">${MiniProfiler.getClientTimingByName(ClientTimings,"Request").Start}</td>
|
28
|
+
<td class="time">${MiniProfiler.getClientTimingByName(ClientTimings,"Response").Start}</td>
|
29
|
+
<td class="time">${MiniProfiler.getClientTimingByName(ClientTimings,"Dom Complete").Start}</td>
|
30
|
+
{{else}}
|
31
|
+
<td colspan="3"></td>
|
32
|
+
{{/if}}
|
33
|
+
</tr>
|
34
|
+
</script>
|
@@ -0,0 +1 @@
|
|
1
|
+
<script async type="text/javascript" id="mini-profiler" src="{path}includes.js?v={version}" data-version="{version}" data-path="{path}" data-current-id="{currentId}" data-ids="{ids}" data-position="{position}" data-trivial="{showTrivial}" data-children="{showChildren}" data-max-traces="{maxTracesToShow}" data-controls="{showControls}" data-authorized="{authorized}" data-toggle-shortcut="{toggleShortcut}" data-start-hidden="{startHidden}"></script>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<title>{name} ({duration} ms) - Profiling Results</title>
|
4
|
+
<script type='text/javascript' src='{path}jquery.1.7.1.js?v={version}'></script>
|
5
|
+
<script type='text/javascript'> var profiler = {json}; </script>
|
6
|
+
{includes}
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<div class='profiler-result-full'></div>
|
10
|
+
</body>
|
11
|
+
</html>
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Rack
|
2
|
+
class MiniProfiler
|
3
|
+
class ClientSettings
|
4
|
+
|
5
|
+
COOKIE_NAME = "__profilin"
|
6
|
+
|
7
|
+
BACKTRACE_DEFAULT = nil
|
8
|
+
BACKTRACE_FULL = 1
|
9
|
+
BACKTRACE_NONE = 2
|
10
|
+
|
11
|
+
attr_accessor :disable_profiling
|
12
|
+
attr_accessor :backtrace_level
|
13
|
+
|
14
|
+
|
15
|
+
def initialize(env)
|
16
|
+
request = ::Rack::Request.new(env)
|
17
|
+
@cookie = request.cookies[COOKIE_NAME]
|
18
|
+
if @cookie
|
19
|
+
@cookie.split(",").map{|pair| pair.split("=")}.each do |k,v|
|
20
|
+
@orig_disable_profiling = @disable_profiling = (v=='t') if k == "dp"
|
21
|
+
@backtrace_level = v.to_i if k == "bt"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
@backtrace_level = nil if !@backtrace_level.nil? && (@backtrace_level == 0 || @backtrace_level > BACKTRACE_NONE)
|
26
|
+
@orig_backtrace_level = @backtrace_level
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def write!(headers)
|
31
|
+
if @orig_disable_profiling != @disable_profiling || @orig_backtrace_level != @backtrace_level || @cookie.nil?
|
32
|
+
settings = {"p" => "t" }
|
33
|
+
settings["dp"] = "t" if @disable_profiling
|
34
|
+
settings["bt"] = @backtrace_level if @backtrace_level
|
35
|
+
settings_string = settings.map{|k,v| "#{k}=#{v}"}.join(",")
|
36
|
+
Rack::Utils.set_cookie_header!(headers, COOKIE_NAME, :value => settings_string, :path => '/')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def discard_cookie!(headers)
|
41
|
+
Rack::Utils.delete_cookie_header!(headers, COOKIE_NAME, :path => '/')
|
42
|
+
end
|
43
|
+
|
44
|
+
def has_cookie?
|
45
|
+
!@cookie.nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
def disable_profiling?
|
49
|
+
@disable_profiling
|
50
|
+
end
|
51
|
+
|
52
|
+
def backtrace_full?
|
53
|
+
@backtrace_level == BACKTRACE_FULL
|
54
|
+
end
|
55
|
+
|
56
|
+
def backtrace_default?
|
57
|
+
@backtrace_level == BACKTRACE_DEFAULT
|
58
|
+
end
|
59
|
+
|
60
|
+
def backtrace_none?
|
61
|
+
@backtrace_level == BACKTRACE_NONE
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'mini_profiler/timer_struct'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class MiniProfiler
|
5
|
+
|
6
|
+
# This class holds the client timings
|
7
|
+
class ClientTimerStruct < TimerStruct
|
8
|
+
|
9
|
+
def self.init_instrumentation
|
10
|
+
"<script type=\"text/javascript\">mPt=function(){var t=[];return{t:t,probe:function(n){t.push({d:new Date(),n:n})}}}()</script>"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.instrument(name,orig)
|
14
|
+
probe = "<script>mPt.probe('#{name}')</script>"
|
15
|
+
wrapped = probe
|
16
|
+
wrapped << orig
|
17
|
+
wrapped << probe
|
18
|
+
wrapped
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def initialize(env={})
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.init_from_form_data(env, page_struct)
|
27
|
+
timings = []
|
28
|
+
clientTimes, clientPerf, baseTime = nil
|
29
|
+
form = env['rack.request.form_hash']
|
30
|
+
|
31
|
+
clientPerf = form['clientPerformance'] if form
|
32
|
+
clientTimes = clientPerf['timing'] if clientPerf
|
33
|
+
|
34
|
+
baseTime = clientTimes['navigationStart'].to_i if clientTimes
|
35
|
+
return unless clientTimes && baseTime
|
36
|
+
|
37
|
+
probes = form['clientProbes']
|
38
|
+
translated = {}
|
39
|
+
if probes && !["null", ""].include?(probes)
|
40
|
+
probes.each do |id, val|
|
41
|
+
name = val["n"]
|
42
|
+
translated[name] ||= {}
|
43
|
+
if translated[name][:start]
|
44
|
+
translated[name][:finish] = val["d"]
|
45
|
+
else
|
46
|
+
translated[name][:start] = val["d"]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
translated.each do |name, data|
|
52
|
+
h = {"Name" => name, "Start" => data[:start].to_i - baseTime}
|
53
|
+
h["Duration"] = data[:finish].to_i - data[:start].to_i if data[:finish]
|
54
|
+
timings.push(h)
|
55
|
+
end
|
56
|
+
|
57
|
+
clientTimes.keys.find_all{|k| k =~ /Start$/ }.each do |k|
|
58
|
+
start = clientTimes[k].to_i - baseTime
|
59
|
+
finish = clientTimes[k.sub(/Start$/, "End")].to_i - baseTime
|
60
|
+
duration = 0
|
61
|
+
duration = finish - start if finish > start
|
62
|
+
name = k.sub(/Start$/, "").split(/(?=[A-Z])/).map{|s| s.capitalize}.join(' ')
|
63
|
+
timings.push({"Name" => name, "Start" => start, "Duration" => duration}) if start >= 0
|
64
|
+
end
|
65
|
+
|
66
|
+
clientTimes.keys.find_all{|k| !(k =~ /(End|Start)$/)}.each do |k|
|
67
|
+
timings.push("Name" => k, "Start" => clientTimes[k].to_i - baseTime, "Duration" => -1)
|
68
|
+
end
|
69
|
+
|
70
|
+
rval = self.new
|
71
|
+
rval['RedirectCount'] = env['rack.request.form_hash']['clientPerformance']['navigation']['redirectCount']
|
72
|
+
rval['Timings'] = timings
|
73
|
+
rval
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Rack
|
2
|
+
class MiniProfiler
|
3
|
+
class Config
|
4
|
+
|
5
|
+
def self.attr_accessor(*vars)
|
6
|
+
@attributes ||= []
|
7
|
+
@attributes.concat vars
|
8
|
+
super(*vars)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.attributes
|
12
|
+
@attributes
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :auto_inject, :base_url_path, :pre_authorize_cb, :position,
|
16
|
+
:backtrace_remove, :backtrace_includes, :backtrace_ignores, :skip_schema_queries,
|
17
|
+
:storage, :user_provider, :storage_instance, :storage_options, :skip_paths, :authorization_mode,
|
18
|
+
:toggle_shortcut, :start_hidden
|
19
|
+
|
20
|
+
# Deprecated options
|
21
|
+
attr_accessor :use_existing_jquery
|
22
|
+
|
23
|
+
def self.default
|
24
|
+
new.instance_eval {
|
25
|
+
@auto_inject = true # automatically inject on every html page
|
26
|
+
@base_url_path = "/mini-profiler-resources/"
|
27
|
+
|
28
|
+
# called prior to rack chain, to ensure we are allowed to profile
|
29
|
+
@pre_authorize_cb = lambda {|env| true}
|
30
|
+
|
31
|
+
# called after rack chain, to ensure we are REALLY allowed to profile
|
32
|
+
@position = 'left' # Where it is displayed
|
33
|
+
@skip_schema_queries = false
|
34
|
+
@storage = MiniProfiler::MemoryStore
|
35
|
+
@user_provider = Proc.new{|env| Rack::Request.new(env).ip}
|
36
|
+
@authorization_mode = :allow_all
|
37
|
+
@toggle_shortcut = 'Alt+P'
|
38
|
+
@start_hidden = false
|
39
|
+
self
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def merge!(config)
|
44
|
+
return unless config
|
45
|
+
if Hash === config
|
46
|
+
config.each{|k,v| instance_variable_set "@#{k}",v}
|
47
|
+
else
|
48
|
+
self.class.attributes.each{ |k|
|
49
|
+
v = config.send k
|
50
|
+
instance_variable_set "@#{k}", v if v
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Rack::MiniProfiler::Context
|
2
|
+
attr_accessor :inject_js,:current_timer,:page_struct,:skip_backtrace,:full_backtrace,:discard, :mpt_init, :measure
|
3
|
+
|
4
|
+
def initialize(opts = {})
|
5
|
+
opts["measure"] = true unless opts.key? "measure"
|
6
|
+
opts.each do |k,v|
|
7
|
+
self.instance_variable_set('@' + k, v)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'mini_profiler/timer_struct'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class MiniProfiler
|
5
|
+
|
6
|
+
# Timing system for a custom timers such as cache, redis, RPC, external API
|
7
|
+
# calls, etc.
|
8
|
+
class CustomTimerStruct < TimerStruct
|
9
|
+
def initialize(type, duration_ms, page, parent)
|
10
|
+
@parent = parent
|
11
|
+
@page = page
|
12
|
+
@type = type
|
13
|
+
|
14
|
+
super("Type" => type,
|
15
|
+
"StartMilliseconds" => ((Time.now.to_f * 1000).to_i - page['Started']) - duration_ms,
|
16
|
+
"DurationMilliseconds" => duration_ms,
|
17
|
+
"ParentTimingId" => nil)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# inspired by https://github.com/brendangregg/FlameGraph
|
2
|
+
|
3
|
+
class Rack::MiniProfiler::FlameGraph
|
4
|
+
def initialize(stacks)
|
5
|
+
@stacks = stacks
|
6
|
+
end
|
7
|
+
|
8
|
+
def graph_data
|
9
|
+
height = 0
|
10
|
+
|
11
|
+
table = []
|
12
|
+
prev = []
|
13
|
+
|
14
|
+
# a 2d array makes collapsing easy
|
15
|
+
@stacks.each_with_index do |stack, pos|
|
16
|
+
col = []
|
17
|
+
|
18
|
+
stack.reverse.map{|r| r.to_s}.each_with_index do |frame, i|
|
19
|
+
|
20
|
+
if !prev[i].nil?
|
21
|
+
last_col = prev[i]
|
22
|
+
if last_col[0] == frame
|
23
|
+
last_col[1] += 1
|
24
|
+
col << nil
|
25
|
+
next
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
prev[i] = [frame, 1]
|
30
|
+
col << prev[i]
|
31
|
+
end
|
32
|
+
prev = prev[0..col.length-1].to_a
|
33
|
+
table << col
|
34
|
+
end
|
35
|
+
|
36
|
+
data = []
|
37
|
+
|
38
|
+
# a 1d array makes rendering easy
|
39
|
+
table.each_with_index do |col, col_num|
|
40
|
+
col.each_with_index do |row, row_num|
|
41
|
+
next unless row && row.length == 2
|
42
|
+
data << {
|
43
|
+
:x => col_num + 1,
|
44
|
+
:y => row_num + 1,
|
45
|
+
:width => row[1],
|
46
|
+
:frame => row[0]
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
data
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
class Rack::MiniProfiler::GCProfiler
|
2
|
+
|
3
|
+
def object_space_stats
|
4
|
+
stats = {}
|
5
|
+
ids = Set.new
|
6
|
+
i=0
|
7
|
+
ObjectSpace.each_object { |o|
|
8
|
+
begin
|
9
|
+
i = stats[o.class] || 0
|
10
|
+
i += 1
|
11
|
+
stats[o.class] = i
|
12
|
+
ids << o.object_id if Integer === o.object_id
|
13
|
+
rescue NoMethodError
|
14
|
+
# Redis::Future undefines .class and .object_id super weird
|
15
|
+
end
|
16
|
+
}
|
17
|
+
{:stats => stats, :ids => ids}
|
18
|
+
end
|
19
|
+
|
20
|
+
def diff_object_stats(before,after)
|
21
|
+
diff = {}
|
22
|
+
after.each do |k,v|
|
23
|
+
diff[k] = v - (before[k] || 0)
|
24
|
+
end
|
25
|
+
before.each do |k,v|
|
26
|
+
diff[k] = 0 - v unless after[k]
|
27
|
+
end
|
28
|
+
|
29
|
+
diff
|
30
|
+
end
|
31
|
+
|
32
|
+
def analyze_strings(ids_before,ids_after)
|
33
|
+
result = {}
|
34
|
+
ids_after.each do |id|
|
35
|
+
obj = ObjectSpace._id2ref(id)
|
36
|
+
if String === obj && !ids_before.include?(obj.object_id)
|
37
|
+
result[obj] ||= 0
|
38
|
+
result[obj] += 1
|
39
|
+
end
|
40
|
+
end
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
def profile_gc_time(app,env)
|
45
|
+
body = []
|
46
|
+
|
47
|
+
begin
|
48
|
+
GC::Profiler.clear
|
49
|
+
GC::Profiler.enable
|
50
|
+
b = app.call(env)[2]
|
51
|
+
b.close if b.respond_to? :close
|
52
|
+
body << "GC Profiler ran during this request, if it fired you will see the cost below:\n\n"
|
53
|
+
body << GC::Profiler.result
|
54
|
+
ensure
|
55
|
+
GC.enable
|
56
|
+
GC::Profiler.disable
|
57
|
+
end
|
58
|
+
|
59
|
+
return [200, {'Content-Type' => 'text/plain'}, body]
|
60
|
+
end
|
61
|
+
|
62
|
+
def profile_gc(app,env)
|
63
|
+
|
64
|
+
body = [];
|
65
|
+
|
66
|
+
stat_before,stat_after,diff,string_analysis = nil
|
67
|
+
begin
|
68
|
+
GC.disable
|
69
|
+
stat_before = object_space_stats
|
70
|
+
b = app.call(env)[2]
|
71
|
+
b.close if b.respond_to? :close
|
72
|
+
stat_after = object_space_stats
|
73
|
+
|
74
|
+
diff = diff_object_stats(stat_before[:stats],stat_after[:stats])
|
75
|
+
string_analysis = analyze_strings(stat_before[:ids], stat_after[:ids])
|
76
|
+
ensure
|
77
|
+
GC.enable
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
body << "
|
82
|
+
ObjectSpace delta caused by request:
|
83
|
+
--------------------------------------------\n"
|
84
|
+
diff.to_a.reject{|k,v| v == 0}.sort{|x,y| y[1] <=> x[1]}.each do |k,v|
|
85
|
+
body << "#{k} : #{v}\n" if v != 0
|
86
|
+
end
|
87
|
+
|
88
|
+
body << "\n
|
89
|
+
ObjectSpace stats:
|
90
|
+
-----------------\n"
|
91
|
+
|
92
|
+
stat_after[:stats].to_a.sort{|x,y| y[1] <=> x[1]}.each do |k,v|
|
93
|
+
body << "#{k} : #{v}\n"
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
body << "\n
|
98
|
+
String stats:
|
99
|
+
------------\n"
|
100
|
+
|
101
|
+
string_analysis.to_a.sort{|x,y| y[1] <=> x[1] }.take(1000).each do |string,count|
|
102
|
+
body << "#{count} : #{string}\n"
|
103
|
+
end
|
104
|
+
|
105
|
+
return [200, {'Content-Type' => 'text/plain'}, body]
|
106
|
+
end
|
107
|
+
end
|