mini-mini-profiler 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|