stackprof-webnav 0.1.0 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +39 -0
  3. data/.gitignore +2 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +57 -16
  6. data/README.md +27 -18
  7. data/Rakefile +24 -0
  8. data/bin/stackprof-webnav +7 -6
  9. data/lib/stackprof-webnav.rb +1 -1
  10. data/lib/stackprof-webnav/dump.rb +41 -0
  11. data/lib/stackprof-webnav/presenter.rb +0 -19
  12. data/lib/stackprof-webnav/public/css/application.css +17 -0
  13. data/lib/stackprof-webnav/public/flamegraph.js +983 -0
  14. data/lib/stackprof-webnav/public/lib/string_score.js +78 -0
  15. data/lib/stackprof-webnav/public/overview.js +29 -0
  16. data/lib/stackprof-webnav/server.rb +101 -53
  17. data/lib/stackprof-webnav/version.rb +1 -1
  18. data/lib/stackprof-webnav/views/error.haml +8 -0
  19. data/lib/stackprof-webnav/views/file.haml +7 -4
  20. data/lib/stackprof-webnav/views/flamegraph.haml +77 -0
  21. data/lib/stackprof-webnav/views/graph.haml +5 -0
  22. data/lib/stackprof-webnav/views/index.haml +18 -0
  23. data/lib/stackprof-webnav/views/invalid_dump.haml +4 -0
  24. data/lib/stackprof-webnav/views/layout.haml +1 -3
  25. data/lib/stackprof-webnav/views/method.haml +11 -8
  26. data/lib/stackprof-webnav/views/overview.haml +25 -5
  27. data/screenshots/callgraph.png +0 -0
  28. data/screenshots/directory.png +0 -0
  29. data/screenshots/file.png +0 -0
  30. data/screenshots/flamegraph.png +0 -0
  31. data/screenshots/method.png +0 -0
  32. data/screenshots/overview.png +0 -0
  33. data/spec/fixtures/test-raw.dump +0 -0
  34. data/spec/fixtures/test.dump +0 -0
  35. data/spec/helpers.rb +17 -0
  36. data/spec/integration_spec.rb +79 -0
  37. data/spec/spec_helper.rb +15 -0
  38. data/stackprof-webnav.gemspec +9 -4
  39. metadata +111 -20
  40. data/lib/stackprof-webnav/views/listing.haml +0 -23
  41. 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 "stackprof"
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, :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
13
+ attr_accessor :cmd_options
14
+ end
15
+
16
+ configure :development do
17
+ register Sinatra::Reloader
18
+ end
26
19
 
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]
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 presenter
42
- Server.presenter
38
+ def current_dump
39
+ Thread.current[:cache] = {}
40
+ Thread.current[params[:dump]] ||= Dump.new(params[:dump])
43
41
  end
44
42
 
45
- def method_url name
46
- "/method?name=#{URI.escape(name)}"
43
+ def current_report
44
+ StackProf::Report.new(
45
+ Marshal.load(current_dump.content)
46
+ )
47
47
  end
48
48
 
49
- def file_url path
50
- "/file?path=#{URI.escape(path)}"
49
+ def presenter
50
+ Presenter.new(current_report)
51
51
  end
52
52
 
53
- def overview_url path
54
- "/overview?path=#{URI.escape(path)}"
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
- presenter
60
- if Server.report_dump_listing
61
- redirect '/listing'
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
- @frames = presenter.overview_frames
75
- haml :overview
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 '/listing' do
79
- @file = Server.report_dump_listing
80
- @action = "listing"
81
- @dumps = presenter.listing_dumps
82
- haml :listing
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
- @path = path
94
- @data = presenter.file_overview(path)
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
@@ -1,5 +1,5 @@
1
1
  module StackProf
2
2
  module Webnav
3
- VERSION = '0.1.0'
3
+ VERSION = '1.0.3'
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,5 @@
1
+ #graph
2
+ %a{:href => url_for("/overview")}
3
+ %button Back to overview
4
+ %div
5
+ %img{src: url_for("/graph.svg")}
@@ -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