stackprof-webnav 0.1.0 → 1.0.0
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 +5 -5
- data/.gitignore +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +2 -0
- data/README.md +23 -18
- data/Rakefile +24 -0
- data/bin/stackprof-webnav +3 -5
- data/lib/stackprof-webnav/dump.rb +41 -0
- data/lib/stackprof-webnav/presenter.rb +0 -19
- data/lib/stackprof-webnav/public/flamegraph.js +983 -0
- data/lib/stackprof-webnav/server.rb +101 -52
- 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 +4 -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 +9 -6
- data/lib/stackprof-webnav/views/overview.haml +17 -3
- data/screenshots/callgraph.png +0 -0
- data/screenshots/directory.png +0 -0
- data/screenshots/flamegraph.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 +74 -0
- data/spec/spec_helper.rb +15 -0
- data/stackprof-webnav.gemspec +7 -3
- metadata +90 -16
- data/lib/stackprof-webnav/views/listing.haml +0 -23
- data/screenshots/file.png +0 -0
- data/screenshots/main.png +0 -0
- data/screenshots/method.png +0 -0
@@ -1,85 +1,128 @@
|
|
1
1
|
require 'sinatra'
|
2
2
|
require 'haml'
|
3
3
|
require "stackprof"
|
4
|
-
require 'net/http'
|
5
4
|
require_relative 'presenter'
|
5
|
+
require_relative 'dump'
|
6
|
+
require 'pry'
|
7
|
+
require "sinatra/reloader" if development?
|
8
|
+
require 'ruby-graphviz'
|
6
9
|
|
7
10
|
module StackProf
|
8
11
|
module Webnav
|
9
12
|
class Server < Sinatra::Application
|
10
13
|
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
|
14
|
+
attr_accessor :cmd_options
|
15
|
+
end
|
16
|
+
|
17
|
+
configure :development do
|
18
|
+
register Sinatra::Reloader
|
19
|
+
end
|
26
20
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
21
|
+
configure :production do
|
22
|
+
set :show_exceptions, false
|
23
|
+
end
|
24
|
+
|
25
|
+
error do
|
26
|
+
@error = env['sinatra.error']
|
27
|
+
haml :error
|
28
|
+
end
|
29
|
+
|
30
|
+
before do
|
31
|
+
unless request.path_info == '/'
|
32
|
+
if params[:dump] != current_dump.path
|
33
|
+
current_dump.path = params[:dump]
|
35
34
|
end
|
36
35
|
end
|
37
|
-
|
38
36
|
end
|
39
37
|
|
40
38
|
helpers do
|
41
|
-
def
|
42
|
-
|
39
|
+
def current_dump
|
40
|
+
Thread.current[:cache] = {}
|
41
|
+
Thread.current[params[:dump]] ||= Dump.new(params[:dump])
|
43
42
|
end
|
44
43
|
|
45
|
-
def
|
46
|
-
|
44
|
+
def current_report
|
45
|
+
StackProf::Report.new(
|
46
|
+
Marshal.load(current_dump.content)
|
47
|
+
)
|
47
48
|
end
|
48
49
|
|
49
|
-
def
|
50
|
-
|
50
|
+
def presenter
|
51
|
+
Presenter.new(current_report)
|
51
52
|
end
|
52
53
|
|
53
|
-
def
|
54
|
-
|
54
|
+
def ensure_file_generated(path, &block)
|
55
|
+
return if File.exist?(path)
|
56
|
+
File.open(path, 'wb', &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def url_for(path, options={})
|
60
|
+
query = URI.encode_www_form({dump: params[:dump]}.merge(options))
|
61
|
+
path + "?" + query
|
55
62
|
end
|
56
63
|
end
|
57
64
|
|
58
65
|
get '/' do
|
59
|
-
|
60
|
-
|
61
|
-
redirect
|
62
|
-
else
|
63
|
-
redirect '/overview'
|
66
|
+
if Server.cmd_options[:filepath]
|
67
|
+
query = URI.encode_www_form(dump: Server.cmd_options[:filepath])
|
68
|
+
next redirect to("/overview?#{query}")
|
64
69
|
end
|
70
|
+
|
71
|
+
@directory = File.expand_path(Server.cmd_options[:directory] || '.')
|
72
|
+
@files = Dir.entries(@directory).sort.map do |file|
|
73
|
+
path = File.expand_path(file, @directory)
|
74
|
+
|
75
|
+
OpenStruct.new(
|
76
|
+
name: file,
|
77
|
+
path: path,
|
78
|
+
modified: File.mtime(path)
|
79
|
+
)
|
80
|
+
end.select do |file|
|
81
|
+
File.file?(file.path)
|
82
|
+
end
|
83
|
+
|
84
|
+
haml :index
|
65
85
|
end
|
66
86
|
|
67
87
|
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
88
|
@action = "overview"
|
74
|
-
|
75
|
-
|
89
|
+
|
90
|
+
begin
|
91
|
+
@frames = presenter.overview_frames
|
92
|
+
haml :overview
|
93
|
+
rescue => error
|
94
|
+
haml :invalid_dump
|
95
|
+
end
|
76
96
|
end
|
77
97
|
|
78
|
-
get '/
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
98
|
+
get '/flames.json' do
|
99
|
+
ensure_file_generated(current_dump.flame_graph_path) do |file|
|
100
|
+
current_report.print_flamegraph(file, true, true)
|
101
|
+
end
|
102
|
+
|
103
|
+
send_file(current_dump.flame_graph_path, type: 'text/javascript')
|
104
|
+
end
|
105
|
+
|
106
|
+
get '/graph.png' do
|
107
|
+
ensure_file_generated(current_dump.graph_path) do |file|
|
108
|
+
current_report.print_graphviz({}, file)
|
109
|
+
end
|
110
|
+
|
111
|
+
ensure_file_generated(current_dump.graph_image_path) do |file|
|
112
|
+
GraphViz
|
113
|
+
.parse(current_dump.graph_path)
|
114
|
+
.output(png: current_dump.graph_image_path)
|
115
|
+
end
|
116
|
+
|
117
|
+
send_file(current_dump.graph_image_path, type: 'image/png')
|
118
|
+
end
|
119
|
+
|
120
|
+
get '/graph' do
|
121
|
+
haml :graph
|
122
|
+
end
|
123
|
+
|
124
|
+
get '/flamegraph' do
|
125
|
+
haml :flamegraph
|
83
126
|
end
|
84
127
|
|
85
128
|
get '/method' do
|
@@ -90,8 +133,14 @@ module StackProf
|
|
90
133
|
|
91
134
|
get '/file' do
|
92
135
|
path = params[:path]
|
93
|
-
|
94
|
-
|
136
|
+
|
137
|
+
if File.exist?(path)
|
138
|
+
@path = path
|
139
|
+
@data = presenter.file_overview(path)
|
140
|
+
else
|
141
|
+
@data = nil
|
142
|
+
end
|
143
|
+
|
95
144
|
haml :file
|
96
145
|
end
|
97
146
|
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
|
@@ -1,12 +1,15 @@
|
|
1
1
|
%h3 StackProf Navigator - #{@action} (#{@frames.count} frames)
|
2
2
|
%hr
|
3
3
|
|
4
|
-
%
|
5
|
-
%
|
4
|
+
%p
|
5
|
+
%a{:href => url_for("/overview")} ← Back to overview
|
6
6
|
|
7
7
|
- @frames.each do |frame|
|
8
|
-
%
|
9
|
-
%
|
8
|
+
%a{:href => url_for("/file", path: frame[:location])}
|
9
|
+
%button.btn
|
10
|
+
Browse
|
11
|
+
= frame[:location]
|
12
|
+
|
10
13
|
- if frame[:callers].any?
|
11
14
|
%table
|
12
15
|
%thead
|
@@ -21,7 +24,7 @@
|
|
21
24
|
%td= caller[:weight]
|
22
25
|
%td= caller[:pct]
|
23
26
|
%td
|
24
|
-
%a{:href =>
|
27
|
+
%a{:href => url_for("/method", name: caller[:method])}
|
25
28
|
= caller[:method]
|
26
29
|
|
27
30
|
- if frame[:callees].any?
|
@@ -38,7 +41,7 @@
|
|
38
41
|
%td= caller[:weight]
|
39
42
|
%td= caller[:pct]
|
40
43
|
%td
|
41
|
-
%a{:href =>
|
44
|
+
%a{:href => url_for("/method", name: caller[:method])}
|
42
45
|
= caller[:method]
|
43
46
|
|
44
47
|
%h4 Code
|
@@ -3,7 +3,21 @@
|
|
3
3
|
|
4
4
|
%p
|
5
5
|
Viewing dump
|
6
|
-
%b=
|
6
|
+
%b= current_dump.path
|
7
|
+
|
8
|
+
%a{href: "/"}
|
9
|
+
%button.btn Browse directory
|
10
|
+
|
11
|
+
|
12
|
+
%a{href: url_for("/graph")}
|
13
|
+
%button.btn View call graph
|
14
|
+
|
15
|
+
- if current_report.data[:raw]
|
16
|
+
%a{href: url_for("/flamegraph")}
|
17
|
+
%button View flamegraph
|
18
|
+
- else
|
19
|
+
%a{href: "https://github.com/tmm1/stackprof#all-options"}
|
20
|
+
%button.secondary Flamegraph is not available
|
7
21
|
|
8
22
|
%table.centered
|
9
23
|
%thead
|
@@ -21,5 +35,5 @@
|
|
21
35
|
%td= frame[:samples]
|
22
36
|
%td= frame[:samples_pct]
|
23
37
|
%td
|
24
|
-
%a{:href =>
|
25
|
-
= frame[:method]
|
38
|
+
%a{:href => url_for("/method", name: frame[:method])}
|
39
|
+
= frame[:method]
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/spec/helpers.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rack/test'
|
2
|
+
|
3
|
+
module Helpers
|
4
|
+
def build_app(options={})
|
5
|
+
StackProf::Webnav::Server.cmd_options = options
|
6
|
+
Rack::Test::Session.new(Rack::MockSession.new(StackProf::Webnav::Server))
|
7
|
+
end
|
8
|
+
|
9
|
+
def fixture_path(name)
|
10
|
+
File.join(File.dirname(__FILE__), "fixtures", name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def build_presenter(path)
|
14
|
+
report = StackProf::Report.new(Marshal.load(File.read(path)))
|
15
|
+
StackProf::Webnav::Presenter.new(report)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe "integration tests" do
|
4
|
+
let(:app) { build_app }
|
5
|
+
let(:presenter) { build_presenter(fixture_path("test.dump")) }
|
6
|
+
let(:response) { app.last_response }
|
7
|
+
|
8
|
+
after(:each) do
|
9
|
+
Dir.glob("spec/fixtures/*.flames.json").each {|file| File.delete(file) }
|
10
|
+
Dir.glob("spec/fixtures/*.digraph.dot").each {|file| File.delete(file) }
|
11
|
+
Dir.glob("spec/fixtures/*.graph.png").each {|file| File.delete(file) }
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "index" do
|
15
|
+
it "lists the files in a folder" do
|
16
|
+
app = build_app(directory: "spec/fixtures")
|
17
|
+
app.get "/"
|
18
|
+
expect(app.last_response.body).to include("test.dump")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "redirects to overview if path is specified" do
|
22
|
+
app = build_app(filepath: fixture_path("test.dump"))
|
23
|
+
app.get "/"
|
24
|
+
app.follow_redirect!
|
25
|
+
expect(app.last_response.body).to include("Dummy")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "overview" do
|
30
|
+
it "works with a valid dump" do
|
31
|
+
app.get "/overview", dump: fixture_path("test.dump")
|
32
|
+
|
33
|
+
frame = presenter.overview_frames.first
|
34
|
+
expect(response.body).to include(frame[:method])
|
35
|
+
end
|
36
|
+
|
37
|
+
it "communicates that flamegraph is not available for dump" do
|
38
|
+
app.get "/overview", dump: fixture_path("test.dump")
|
39
|
+
end
|
40
|
+
|
41
|
+
it "does not crash if selected file is not a dump" do
|
42
|
+
app.get "/overview", dump: "Rakefile"
|
43
|
+
expect(response.body).to include("Unable to open")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it "is able to generate flames.json file" do
|
48
|
+
app.get "/flames.json", dump: fixture_path("test-raw.dump")
|
49
|
+
expect(response.body).to include("flamegraph")
|
50
|
+
end
|
51
|
+
|
52
|
+
it "is able to render graph" do
|
53
|
+
app.get "/graph.png", dump: fixture_path("test.dump")
|
54
|
+
expect(response.get_header("Content-Type")).to eq("image/png")
|
55
|
+
expect(response.body.size).to_not eq(0)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "method page renders information about a method" do
|
59
|
+
frame = presenter.overview_frames.first
|
60
|
+
app.get "/method", dump: fixture_path("test.dump"), name: frame[:method]
|
61
|
+
expect(response.body).to include("Callers")
|
62
|
+
end
|
63
|
+
|
64
|
+
it "does not crash on a file page" do
|
65
|
+
frame = presenter.overview_frames.first
|
66
|
+
method_info = presenter.method_info frame[:method]
|
67
|
+
|
68
|
+
expect {
|
69
|
+
app.get "/file",
|
70
|
+
dump: fixture_path("test.dump"),
|
71
|
+
path: method_info.first[:location]
|
72
|
+
}.to_not raise_error
|
73
|
+
end
|
74
|
+
end
|