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.
@@ -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