stackprof-webnav 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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, :report_dump_path, :report_dump_uri, :report_dump_listing
12
-
13
- def presenter regenerate=false
14
- return @presenter unless regenerate || @presenter.nil?
15
- process_options
16
- if self.report_dump_path || self.report_dump_uri
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
- private
28
- def process_options
29
- if cmd_options[:filepath]
30
- self.report_dump_path = cmd_options[:filepath]
31
- elsif cmd_options[:uri]
32
- self.report_dump_uri = cmd_options[:uri]
33
- elsif cmd_options[:bucket]
34
- self.report_dump_listing = cmd_options[:bucket]
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 presenter
42
- Server.presenter
39
+ def current_dump
40
+ Thread.current[:cache] = {}
41
+ Thread.current[params[:dump]] ||= Dump.new(params[:dump])
43
42
  end
44
43
 
45
- def method_url name
46
- "/method?name=#{URI.escape(name)}"
44
+ def current_report
45
+ StackProf::Report.new(
46
+ Marshal.load(current_dump.content)
47
+ )
47
48
  end
48
49
 
49
- def file_url path
50
- "/file?path=#{URI.escape(path)}"
50
+ def presenter
51
+ Presenter.new(current_report)
51
52
  end
52
53
 
53
- def overview_url path
54
- "/overview?path=#{URI.escape(path)}"
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
- presenter
60
- if Server.report_dump_listing
61
- redirect '/listing'
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
- @frames = presenter.overview_frames
75
- haml :overview
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 '/listing' do
79
- @file = Server.report_dump_listing
80
- @action = "listing"
81
- @dumps = presenter.listing_dumps
82
- haml :listing
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
- @path = path
94
- @data = presenter.file_overview(path)
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
@@ -1,5 +1,5 @@
1
1
  module StackProf
2
2
  module Webnav
3
- VERSION = '0.1.0'
3
+ VERSION = '1.0.0'
4
4
  end
5
5
  end
@@ -0,0 +1,8 @@
1
+ %h1 Oops, something went wrong
2
+
3
+ %h2
4
+ = @error.class
5
+ \-
6
+ = @error.message
7
+
8
+ %h3 Are you looking at a valid stackprof dump?
@@ -5,7 +5,10 @@
5
5
  %br
6
6
  %br
7
7
 
8
- .code_linenums
9
- - @data[:lineinfo].each do |l|
10
- %span= l
11
- != @data[:code]
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,4 @@
1
+ %a{:href => url_for("/overview")}
2
+ %button Back to overview
3
+
4
+ %img{src: url_for("/graph.png")}
@@ -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
@@ -0,0 +1,4 @@
1
+ %h3 StackProf Navigator - #{@action}
2
+ %hr
3
+
4
+ %h4 Unable to open the specified file as a dump
@@ -7,6 +7,4 @@
7
7
  %link(rel="stylesheet" href="/css/application.css")
8
8
 
9
9
  %body
10
- .row
11
- .large-12.columns
12
- = yield
10
+ = yield
@@ -1,12 +1,15 @@
1
1
  %h3 StackProf Navigator - #{@action} (#{@frames.count} frames)
2
2
  %hr
3
3
 
4
- %a{:href => '/'} &#8592; Back to overview
5
- %br
4
+ %p
5
+ %a{:href => url_for("/overview")} &#8592; Back to overview
6
6
 
7
7
  - @frames.each do |frame|
8
- %h4
9
- %a{:href => file_url(frame[:location])}= frame[:location]
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 => method_url(caller[:method])}
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 => method_url(caller[:method])}
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= @file
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 => method_url(frame[:method])}
25
- = frame[:method]
38
+ %a{:href => url_for("/method", name: frame[:method])}
39
+ = frame[:method]
Binary file
Binary file
Binary file
Binary file
@@ -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