stackprof-webnav 0.1.0 → 1.0.3
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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +39 -0
- data/.gitignore +2 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +57 -16
- data/README.md +27 -18
- data/Rakefile +24 -0
- data/bin/stackprof-webnav +7 -6
- data/lib/stackprof-webnav.rb +1 -1
- data/lib/stackprof-webnav/dump.rb +41 -0
- data/lib/stackprof-webnav/presenter.rb +0 -19
- data/lib/stackprof-webnav/public/css/application.css +17 -0
- data/lib/stackprof-webnav/public/flamegraph.js +983 -0
- data/lib/stackprof-webnav/public/lib/string_score.js +78 -0
- data/lib/stackprof-webnav/public/overview.js +29 -0
- data/lib/stackprof-webnav/server.rb +101 -53
- data/lib/stackprof-webnav/version.rb +1 -1
- data/lib/stackprof-webnav/views/error.haml +8 -0
- data/lib/stackprof-webnav/views/file.haml +7 -4
- data/lib/stackprof-webnav/views/flamegraph.haml +77 -0
- data/lib/stackprof-webnav/views/graph.haml +5 -0
- data/lib/stackprof-webnav/views/index.haml +18 -0
- data/lib/stackprof-webnav/views/invalid_dump.haml +4 -0
- data/lib/stackprof-webnav/views/layout.haml +1 -3
- data/lib/stackprof-webnav/views/method.haml +11 -8
- data/lib/stackprof-webnav/views/overview.haml +25 -5
- data/screenshots/callgraph.png +0 -0
- data/screenshots/directory.png +0 -0
- data/screenshots/file.png +0 -0
- data/screenshots/flamegraph.png +0 -0
- data/screenshots/method.png +0 -0
- data/screenshots/overview.png +0 -0
- data/spec/fixtures/test-raw.dump +0 -0
- data/spec/fixtures/test.dump +0 -0
- data/spec/helpers.rb +17 -0
- data/spec/integration_spec.rb +79 -0
- data/spec/spec_helper.rb +15 -0
- data/stackprof-webnav.gemspec +9 -4
- metadata +111 -20
- data/lib/stackprof-webnav/views/listing.haml +0 -23
- data/screenshots/main.png +0 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
/*
|
2
|
+
* string_score.js: Quicksilver-like string scoring algorithm.
|
3
|
+
*
|
4
|
+
* Copyright (C) 2009-2011 Joshaven Potter <yourtech@gmail.com>
|
5
|
+
* Copyright (C) 2010-2011 Yesudeep Mangalapilly <yesudeep@gmail.com>
|
6
|
+
* MIT license: http://www.opensource.org/licenses/mit-license.php
|
7
|
+
*
|
8
|
+
* This is a javascript port of the above mentionned, with a tiny change:
|
9
|
+
* it avoids string monkey patch.
|
10
|
+
*/
|
11
|
+
;(function(global) {
|
12
|
+
global.stringScore = (string, abbreviation) => {
|
13
|
+
// Perfect match if the spring equals the abbreviation
|
14
|
+
if (string == abbreviation) return 1.0
|
15
|
+
|
16
|
+
// Initializing variables.
|
17
|
+
let string_length = string.length
|
18
|
+
let abbreviation_length = abbreviation.length
|
19
|
+
let total_character_score = 0
|
20
|
+
|
21
|
+
// Awarded only if the string and the abbreviation have a common prefix.
|
22
|
+
let should_award_common_prefix_bonus = 0
|
23
|
+
|
24
|
+
// # Sum character scores
|
25
|
+
|
26
|
+
// Add up scores for each character in the abbreviation.
|
27
|
+
for (let i = 0, c = abbreviation[i]; i < abbreviation_length; c = abbreviation[++i]) {
|
28
|
+
// Find the index of current character (case-insensitive) in remaining part of string.
|
29
|
+
let index_c_lowercase = string.indexOf(c.toLowerCase())
|
30
|
+
let index_c_uppercase = string.indexOf(c.toUpperCase())
|
31
|
+
let min_index = Math.min(index_c_lowercase, index_c_uppercase)
|
32
|
+
let index_in_string = min_index > -1 ? min_index : Math.max(index_c_lowercase, index_c_uppercase)
|
33
|
+
|
34
|
+
// # Identical Strings
|
35
|
+
// Bail out if current character is not found (case-insensitive) in remaining part of string.
|
36
|
+
if (index_in_string == -1) return 0
|
37
|
+
|
38
|
+
// Set base score for current character.
|
39
|
+
let character_score = 0.1
|
40
|
+
|
41
|
+
|
42
|
+
// # Case-match bonus
|
43
|
+
// If the current abbreviation character has the same case
|
44
|
+
// as that of the character in the string, we add a bonus.
|
45
|
+
if (string[index_in_string] == c) character_score += 0.1
|
46
|
+
|
47
|
+
// # Consecutive character match and common prefix bonuses
|
48
|
+
// Increase the score when each consecutive character of
|
49
|
+
// the abbreviation matches the first character of the
|
50
|
+
// remaining string.
|
51
|
+
if (index_in_string == 0) {
|
52
|
+
character_score += 0.8
|
53
|
+
// String and abbreviation have common prefix, so award bonus.
|
54
|
+
if (i == 0) should_award_common_prefix_bonus = 1
|
55
|
+
}
|
56
|
+
|
57
|
+
// # Acronym bonus
|
58
|
+
// Typing the first character of an acronym is as
|
59
|
+
// though you preceded it with two perfect character
|
60
|
+
// matches.
|
61
|
+
if (string.charAt(index_in_string - 1) == ' ') // TODO: better accro
|
62
|
+
character_score += 0.8 // * Math.min(index_in_string, 5) # Cap bonus at 0.4 * 5
|
63
|
+
|
64
|
+
// Left trim the matched part of the string
|
65
|
+
// (forces sequential matching).
|
66
|
+
string = string.substring(index_in_string + 1, string_length)
|
67
|
+
|
68
|
+
// Add to total character score.
|
69
|
+
total_character_score += character_score
|
70
|
+
}
|
71
|
+
|
72
|
+
let abbreviation_score = total_character_score / abbreviation_length
|
73
|
+
|
74
|
+
let final_score = ((abbreviation_score * (abbreviation_length / string_length)) + abbreviation_score) / 2
|
75
|
+
if (should_award_common_prefix_bonus && final_score + 0.1 < 1) final_score += 0.1
|
76
|
+
return final_score
|
77
|
+
}
|
78
|
+
})(window)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
;(function() {
|
2
|
+
const table = document.getElementsByTagName('table')[0]
|
3
|
+
const input = document.getElementsByClassName('filter')[0]
|
4
|
+
const rows = {}
|
5
|
+
const filterKeys = Array.from(document.querySelectorAll('tr[data-filter-key]')).map(el => {
|
6
|
+
key = el.dataset.filterKey
|
7
|
+
rows[key] = el
|
8
|
+
return key
|
9
|
+
})
|
10
|
+
const scores = {}
|
11
|
+
const computeScores = () => {
|
12
|
+
const searchString = input.value
|
13
|
+
filterKeys.forEach(filterKey => scores[filterKey] = stringScore(filterKey, searchString))
|
14
|
+
}
|
15
|
+
|
16
|
+
const redrawTable = (sorted) => {
|
17
|
+
const newBody = document.createElement('tbody')
|
18
|
+
sorted.forEach(key => newBody.appendChild(rows[key]))
|
19
|
+
table.removeChild(table.getElementsByTagName('tbody')[0])
|
20
|
+
table.appendChild(newBody)
|
21
|
+
}
|
22
|
+
|
23
|
+
input.addEventListener('input', () => {
|
24
|
+
computeScores()
|
25
|
+
redrawTable(
|
26
|
+
filterKeys.filter(k => scores[k] !== 0).sort((a, b) => scores[b] - scores[a])
|
27
|
+
)
|
28
|
+
})
|
29
|
+
})()
|
@@ -1,85 +1,127 @@
|
|
1
1
|
require 'sinatra'
|
2
2
|
require 'haml'
|
3
|
-
require
|
4
|
-
require 'net/http'
|
3
|
+
require 'stackprof'
|
5
4
|
require_relative 'presenter'
|
5
|
+
require_relative 'dump'
|
6
|
+
require 'sinatra/reloader' if development?
|
7
|
+
require 'ruby-graphviz'
|
6
8
|
|
7
9
|
module StackProf
|
8
10
|
module Webnav
|
9
11
|
class Server < Sinatra::Application
|
10
12
|
class << self
|
11
|
-
attr_accessor :cmd_options
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
report_contents = if report_dump_path.nil?
|
18
|
-
Net::HTTP.get(URI.parse(report_dump_uri))
|
19
|
-
else
|
20
|
-
File.open(report_dump_path).read
|
21
|
-
end
|
22
|
-
report = StackProf::Report.new(Marshal.load(report_contents))
|
23
|
-
end
|
24
|
-
@presenter = Presenter.new(report)
|
25
|
-
end
|
13
|
+
attr_accessor :cmd_options
|
14
|
+
end
|
15
|
+
|
16
|
+
configure :development do
|
17
|
+
register Sinatra::Reloader
|
18
|
+
end
|
26
19
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
20
|
+
configure :production do
|
21
|
+
set :show_exceptions, false
|
22
|
+
end
|
23
|
+
|
24
|
+
error do
|
25
|
+
@error = env['sinatra.error']
|
26
|
+
haml :error
|
27
|
+
end
|
28
|
+
|
29
|
+
before do
|
30
|
+
unless request.path_info == '/'
|
31
|
+
if params[:dump] && params[:dump] != current_dump.path
|
32
|
+
current_dump.path = params[:dump]
|
35
33
|
end
|
36
34
|
end
|
37
|
-
|
38
35
|
end
|
39
36
|
|
40
37
|
helpers do
|
41
|
-
def
|
42
|
-
|
38
|
+
def current_dump
|
39
|
+
Thread.current[:cache] = {}
|
40
|
+
Thread.current[params[:dump]] ||= Dump.new(params[:dump])
|
43
41
|
end
|
44
42
|
|
45
|
-
def
|
46
|
-
|
43
|
+
def current_report
|
44
|
+
StackProf::Report.new(
|
45
|
+
Marshal.load(current_dump.content)
|
46
|
+
)
|
47
47
|
end
|
48
48
|
|
49
|
-
def
|
50
|
-
|
49
|
+
def presenter
|
50
|
+
Presenter.new(current_report)
|
51
51
|
end
|
52
52
|
|
53
|
-
def
|
54
|
-
|
53
|
+
def ensure_file_generated(path, &block)
|
54
|
+
return if File.exist?(path)
|
55
|
+
File.open(path, 'wb', &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def url_for(path, options={})
|
59
|
+
query = URI.encode_www_form({dump: params[:dump]}.merge(options))
|
60
|
+
path + "?" + query
|
55
61
|
end
|
56
62
|
end
|
57
63
|
|
58
64
|
get '/' do
|
59
|
-
|
60
|
-
|
61
|
-
redirect
|
62
|
-
else
|
63
|
-
redirect '/overview'
|
65
|
+
if Server.cmd_options[:filepath]
|
66
|
+
query = URI.encode_www_form(dump: Server.cmd_options[:filepath])
|
67
|
+
next redirect to("/overview?#{query}")
|
64
68
|
end
|
69
|
+
|
70
|
+
@directory = File.expand_path(Server.cmd_options[:directory] || '.')
|
71
|
+
@files = Dir.entries(@directory).sort.map do |file|
|
72
|
+
path = File.expand_path(file, @directory)
|
73
|
+
|
74
|
+
OpenStruct.new(
|
75
|
+
name: file,
|
76
|
+
path: path,
|
77
|
+
modified: File.mtime(path)
|
78
|
+
)
|
79
|
+
end.select do |file|
|
80
|
+
File.file?(file.path)
|
81
|
+
end
|
82
|
+
|
83
|
+
haml :index
|
65
84
|
end
|
66
85
|
|
67
86
|
get '/overview' do
|
68
|
-
if params[:path]
|
69
|
-
Server.report_dump_uri = params[:path]
|
70
|
-
Server.presenter(true)
|
71
|
-
end
|
72
|
-
@file = Server.report_dump_path || Server.report_dump_uri
|
73
87
|
@action = "overview"
|
74
|
-
|
75
|
-
|
88
|
+
|
89
|
+
begin
|
90
|
+
@frames = presenter.overview_frames
|
91
|
+
haml :overview
|
92
|
+
rescue => error
|
93
|
+
haml :invalid_dump
|
94
|
+
end
|
76
95
|
end
|
77
96
|
|
78
|
-
get '/
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
97
|
+
get '/flames.json' do
|
98
|
+
ensure_file_generated(current_dump.flame_graph_path) do |file|
|
99
|
+
current_report.print_flamegraph(file, true, true)
|
100
|
+
end
|
101
|
+
|
102
|
+
send_file(current_dump.flame_graph_path, type: 'text/javascript')
|
103
|
+
end
|
104
|
+
|
105
|
+
get '/graph.svg' do
|
106
|
+
ensure_file_generated(current_dump.graph_path) do |file|
|
107
|
+
current_report.print_graphviz({}, file)
|
108
|
+
end
|
109
|
+
|
110
|
+
ensure_file_generated(current_dump.graph_image_path) do |file|
|
111
|
+
GraphViz
|
112
|
+
.parse(current_dump.graph_path)
|
113
|
+
.output(svg: current_dump.graph_image_path)
|
114
|
+
end
|
115
|
+
|
116
|
+
send_file(current_dump.graph_image_path, type: 'image/svg+xml')
|
117
|
+
end
|
118
|
+
|
119
|
+
get '/graph' do
|
120
|
+
haml :graph
|
121
|
+
end
|
122
|
+
|
123
|
+
get '/flamegraph' do
|
124
|
+
haml :flamegraph
|
83
125
|
end
|
84
126
|
|
85
127
|
get '/method' do
|
@@ -90,8 +132,14 @@ module StackProf
|
|
90
132
|
|
91
133
|
get '/file' do
|
92
134
|
path = params[:path]
|
93
|
-
|
94
|
-
|
135
|
+
|
136
|
+
if File.exist?(path)
|
137
|
+
@path = path
|
138
|
+
@data = presenter.file_overview(path)
|
139
|
+
else
|
140
|
+
@data = nil
|
141
|
+
end
|
142
|
+
|
95
143
|
haml :file
|
96
144
|
end
|
97
145
|
end
|
@@ -5,7 +5,10 @@
|
|
5
5
|
%br
|
6
6
|
%br
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
- if @data
|
9
|
+
.code_linenums
|
10
|
+
- @data[:lineinfo].each do |l|
|
11
|
+
%span= l
|
12
|
+
!= @data[:code]
|
13
|
+
- else
|
14
|
+
%h3 Unable to load file, was the dump captured on another machine?
|
@@ -0,0 +1,77 @@
|
|
1
|
+
%a{:href => url_for("/overview")}
|
2
|
+
%button Back to overview
|
3
|
+
|
4
|
+
%br
|
5
|
+
%br
|
6
|
+
|
7
|
+
:css
|
8
|
+
body {
|
9
|
+
margin: 0;
|
10
|
+
padding: 0;
|
11
|
+
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
12
|
+
font-size: 10pt;
|
13
|
+
}
|
14
|
+
.overview-container {
|
15
|
+
position: relative;
|
16
|
+
}
|
17
|
+
.overview {
|
18
|
+
cursor: col-resize;
|
19
|
+
}
|
20
|
+
.overview-viewport-overlay {
|
21
|
+
position: absolute;
|
22
|
+
top: 0;
|
23
|
+
left: 0;
|
24
|
+
width: 1;
|
25
|
+
height: 1;
|
26
|
+
background-color: rgba(0, 0, 0, 0.25);
|
27
|
+
transform-origin: top left;
|
28
|
+
cursor: -moz-grab;
|
29
|
+
cursor: -webkit-grab;
|
30
|
+
cursor: grab;
|
31
|
+
}
|
32
|
+
.moving {
|
33
|
+
cursor: -moz-grabbing;
|
34
|
+
cursor: -webkit-grabbing;
|
35
|
+
cursor: grabbing;
|
36
|
+
}
|
37
|
+
.info {
|
38
|
+
display: block;
|
39
|
+
height: 40px;
|
40
|
+
margin: 3px 6px;
|
41
|
+
margin-right: 206px;
|
42
|
+
padding: 3px 6px;
|
43
|
+
line-height: 18px;
|
44
|
+
}
|
45
|
+
.legend {
|
46
|
+
display: block;
|
47
|
+
float: right;
|
48
|
+
width: 195px;
|
49
|
+
max-height: 100%;
|
50
|
+
overflow-y: scroll;
|
51
|
+
}
|
52
|
+
.legend > div {
|
53
|
+
padding: 6px;
|
54
|
+
clear: right;
|
55
|
+
}
|
56
|
+
.legend > div span {
|
57
|
+
opacity: 0.75;
|
58
|
+
display: block;
|
59
|
+
text-align: right;
|
60
|
+
}
|
61
|
+
.legend > div .name {
|
62
|
+
max-width: 70%;
|
63
|
+
word-wrap: break-word;
|
64
|
+
}
|
65
|
+
%script{:src => "flamegraph.js"}
|
66
|
+
.legend
|
67
|
+
.overview-container
|
68
|
+
%canvas.overview
|
69
|
+
.overview-viewport-overlay
|
70
|
+
.info
|
71
|
+
%div{:style => "float: right; text-align: right"}
|
72
|
+
.samples
|
73
|
+
.exclusive
|
74
|
+
.frame
|
75
|
+
.file
|
76
|
+
%canvas.flamegraph
|
77
|
+
%script{:src => "/flames.json?dump=#{params[:dump]}"}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
%h3 StackProf Navigator
|
2
|
+
%hr
|
3
|
+
|
4
|
+
%p
|
5
|
+
Listing dumps in
|
6
|
+
%b= @directory
|
7
|
+
|
8
|
+
%table.centered
|
9
|
+
%thead
|
10
|
+
%th Filename
|
11
|
+
%th Modified
|
12
|
+
|
13
|
+
%tbody
|
14
|
+
- @files.each do |file|
|
15
|
+
%tr
|
16
|
+
%td
|
17
|
+
%a{href: url_for("/overview", dump: file.path)}= file.name
|
18
|
+
%td= file.modified
|