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