gem_footprint_analyzer 0.1.2 → 0.1.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.
- checksums.yaml +4 -4
- data/.rubocop +0 -0
- data/.rubocop.yml +15 -0
- data/Gemfile +2 -2
- data/Gemfile.lock +21 -0
- data/Rakefile +3 -3
- data/exe/analyze_requires +9 -48
- data/gem_footprint_analyzer.gemspec +2 -0
- data/lib/gem_footprint_analyzer.rb +2 -1
- data/lib/gem_footprint_analyzer/analyzer.rb +67 -37
- data/lib/gem_footprint_analyzer/average_runner.rb +11 -10
- data/lib/gem_footprint_analyzer/cli.rb +106 -0
- data/lib/gem_footprint_analyzer/formatters/json.rb +5 -3
- data/lib/gem_footprint_analyzer/formatters/text_base.rb +7 -2
- data/lib/gem_footprint_analyzer/formatters/tree.rb +49 -21
- data/lib/gem_footprint_analyzer/require_spy.rb +56 -38
- data/lib/gem_footprint_analyzer/transport.rb +81 -0
- data/lib/gem_footprint_analyzer/version.rb +1 -1
- metadata +34 -4
- data/lib/gem_footprint_analyzer/formatters/text.rb +0 -50
- data/lib/gem_footprint_analyzer/pipe_transport.rb +0 -70
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 69c4c34c88102817c70c7d4a69e1716152213337d19edcb9139437c8c4ce80f0
|
4
|
+
data.tar.gz: 98e61556aa5bf8fdc6c8498f2157e16abb8f45294ed0e5345958652604d0c7a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b582135703728eaaa2ca71217ed4ad796e7c4add2e78c503887017a9f0666dad5bf09022b6dbc83a63cab5d60102524220ffc7e9b14797cb715dec8e16c64e37
|
7
|
+
data.tar.gz: 3c0b25663dd56f72928c0b3e7319e0f2c8c9dffcbefd7d5e6dcac3fa64636b5e10eec91673853cc2e2d2f739cff31851f35d8cc5fe7b9bf6444db95141611e3e
|
data/.rubocop
ADDED
File without changes
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
AllCops:
|
2
|
+
Include:
|
3
|
+
- 'lib/gem_footprint_analyzer/**/*.rb'
|
4
|
+
- 'exe/*'
|
5
|
+
- 'spec/**/*'
|
6
|
+
TargetRubyVersion: 2.2
|
7
|
+
|
8
|
+
Metrics/LineLength:
|
9
|
+
Max: 100
|
10
|
+
|
11
|
+
Style/RegexpLiteral:
|
12
|
+
EnforcedStyle: slashes
|
13
|
+
|
14
|
+
Layout/SpaceInsideHashLiteralBraces:
|
15
|
+
EnforcedStyle: no_space
|
data/Gemfile
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
source
|
1
|
+
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
3
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
4
4
|
|
5
5
|
# Specify your gem's dependencies in gem_footprint_analyzer.gemspec
|
6
6
|
gemspec
|
data/Gemfile.lock
CHANGED
@@ -6,7 +6,14 @@ PATH
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
+
ast (2.4.0)
|
9
10
|
diff-lcs (1.3)
|
11
|
+
jaro_winkler (1.5.1)
|
12
|
+
parallel (1.12.1)
|
13
|
+
parser (2.5.1.2)
|
14
|
+
ast (~> 2.4.0)
|
15
|
+
powerpack (0.1.2)
|
16
|
+
rainbow (3.0.0)
|
10
17
|
rake (10.5.0)
|
11
18
|
rspec (3.8.0)
|
12
19
|
rspec-core (~> 3.8.0)
|
@@ -21,6 +28,18 @@ GEM
|
|
21
28
|
diff-lcs (>= 1.2.0, < 2.0)
|
22
29
|
rspec-support (~> 3.8.0)
|
23
30
|
rspec-support (3.8.0)
|
31
|
+
rubocop (0.60.0)
|
32
|
+
jaro_winkler (~> 1.5.1)
|
33
|
+
parallel (~> 1.10)
|
34
|
+
parser (>= 2.5, != 2.5.1.1)
|
35
|
+
powerpack (~> 0.1)
|
36
|
+
rainbow (>= 2.2.2, < 4.0)
|
37
|
+
ruby-progressbar (~> 1.7)
|
38
|
+
unicode-display_width (~> 1.4.0)
|
39
|
+
rubocop-rspec (1.30.0)
|
40
|
+
rubocop (>= 0.58.0)
|
41
|
+
ruby-progressbar (1.10.0)
|
42
|
+
unicode-display_width (1.4.0)
|
24
43
|
|
25
44
|
PLATFORMS
|
26
45
|
ruby
|
@@ -30,6 +49,8 @@ DEPENDENCIES
|
|
30
49
|
gem_footprint_analyzer!
|
31
50
|
rake (~> 10.0)
|
32
51
|
rspec (~> 3.0)
|
52
|
+
rubocop (~> 0.60.0)
|
53
|
+
rubocop-rspec
|
33
54
|
|
34
55
|
BUNDLED WITH
|
35
56
|
1.16.4
|
data/Rakefile
CHANGED
data/exe/analyze_requires
CHANGED
@@ -1,52 +1,13 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
require 'gem_footprint_analyzer'
|
3
|
-
require 'optparse'
|
4
|
-
|
5
|
-
options = {}
|
6
|
-
options[:runs] = 10
|
7
|
-
options[:debug] = false
|
8
|
-
options[:formatter] = 'tree'
|
9
|
-
|
10
|
-
opts_parser = OptionParser.new do |opts|
|
11
|
-
script_name = "bundle exec #{File.basename($0)}"
|
12
|
-
opts.banner = "GemFootprintAnalyzer (#{GemFootprintAnalyzer::VERSION})"
|
13
|
-
opts.banner += "\nUsage: #{script_name} library_to_analyze [require]"
|
14
|
-
|
15
|
-
opts.on('-f', '--formatter FORMATTER', %w[json text tree], 'Format output using selected formatter (json text tree)') do |formatter|
|
16
|
-
options[:formatter] = formatter
|
17
|
-
end
|
1
|
+
#!/usr/bin/env ruby --disable=did_you_mean --disable=gem --disable=rubyopt
|
18
2
|
|
19
|
-
|
20
|
-
|
21
|
-
fail OptionParser::InvalidArgument, 'must be a number greater than 0'
|
22
|
-
end
|
23
|
-
options[:runs] = runs
|
24
|
-
end
|
3
|
+
$LOAD_PATH.unshift(File.join(__dir__, '..', 'lib'))
|
4
|
+
$LOAD_PATH.unshift('./lib') if Dir.exist?('lib')
|
25
5
|
|
26
|
-
|
27
|
-
opts.banner += "\n(#{File.expand_path(File.join(File.dirname(__FILE__), '..'))})" if debug
|
28
|
-
options[:debug] = debug
|
29
|
-
end
|
30
|
-
|
31
|
-
opts.on_tail('-h', '--help', 'Show this message') do
|
32
|
-
puts opts
|
33
|
-
exit
|
34
|
-
end
|
35
|
-
end
|
36
|
-
opts_parser.parse!(ARGV)
|
37
|
-
|
38
|
-
if ARGV.size < 1
|
39
|
-
puts opts_parser
|
40
|
-
exit 1
|
41
|
-
end
|
42
|
-
|
43
|
-
requires_list_average = GemFootprintAnalyzer::AverageRunner.new(options[:runs]) do
|
44
|
-
GemFootprintAnalyzer::Analyzer.new.test_library(*ARGV)
|
45
|
-
end.run
|
6
|
+
require 'gem_footprint_analyzer'
|
46
7
|
|
47
|
-
|
48
|
-
require 'gem_footprint_analyzer/formatters/tree'
|
49
|
-
require 'gem_footprint_analyzer/formatters/json'
|
8
|
+
cli = GemFootprintAnalyzer::CLI.new
|
50
9
|
|
51
|
-
|
52
|
-
|
10
|
+
t1 = Time.now.to_f
|
11
|
+
cli.run(ARGV)
|
12
|
+
t2 = Time.now.to_f
|
13
|
+
puts format("\nTotal runtime %0.4fs", (t2 - t1))
|
@@ -25,4 +25,6 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_development_dependency "bundler", "~> 1.16"
|
26
26
|
spec.add_development_dependency "rake", "~> 10.0"
|
27
27
|
spec.add_development_dependency "rspec", "~> 3.0"
|
28
|
+
spec.add_development_dependency "rubocop", "~> 0.60.0"
|
29
|
+
spec.add_development_dependency "rubocop-rspec"
|
28
30
|
end
|
@@ -1,8 +1,9 @@
|
|
1
1
|
require 'gem_footprint_analyzer/version'
|
2
|
-
require 'gem_footprint_analyzer/
|
2
|
+
require 'gem_footprint_analyzer/transport'
|
3
3
|
require 'gem_footprint_analyzer/require_spy'
|
4
4
|
require 'gem_footprint_analyzer/analyzer'
|
5
5
|
require 'gem_footprint_analyzer/average_runner'
|
6
|
+
require 'gem_footprint_analyzer/cli'
|
6
7
|
|
7
8
|
module GemFootprintAnalyzer
|
8
9
|
end
|
@@ -1,12 +1,24 @@
|
|
1
1
|
module GemFootprintAnalyzer
|
2
|
+
# A class that faciliates sampling of the original require and subsequent require calls from
|
3
|
+
# within the library.
|
4
|
+
# It forks the original process and runs the first require in that fork explicitly.
|
5
|
+
# Require calls are interwoven with RSS checks done from the parent process, require timing
|
6
|
+
# is gathered in the fork and passed along to the parent.
|
2
7
|
class Analyzer
|
3
|
-
|
4
|
-
|
8
|
+
# @param library [String] name of the library or parameter for the gem method
|
9
|
+
# (ex. 'activerecord', 'activesupport')
|
10
|
+
# @param require_string [String|nil] optional require string, if it differs from the gem name
|
11
|
+
# (ex. 'active_record', 'active_support/time')
|
12
|
+
# @return [Array<Hash>] list of require-data-hashes, first element contains base level RSS,
|
13
|
+
# last element can be treated as a summary as effectively it consists of all the previous.
|
14
|
+
def test_library(library, require_string = nil)
|
5
15
|
try_activate_gem(library)
|
6
16
|
|
7
17
|
child_transport, parent_transport = init_transports
|
8
18
|
|
9
19
|
process_id = fork_and_require(require_string || library, child_transport)
|
20
|
+
fail 'Unable to fork' unless process_id
|
21
|
+
detach_process(process_id)
|
10
22
|
requires = collect_requires(parent_transport, process_id)
|
11
23
|
|
12
24
|
parent_transport.ack
|
@@ -16,8 +28,7 @@ module GemFootprintAnalyzer
|
|
16
28
|
private
|
17
29
|
|
18
30
|
def fork_and_require(require_string, child_transport)
|
19
|
-
|
20
|
-
process_id = fork do
|
31
|
+
fork do
|
21
32
|
RequireSpy.spy_require(child_transport)
|
22
33
|
begin
|
23
34
|
require(require_string)
|
@@ -25,47 +36,66 @@ module GemFootprintAnalyzer
|
|
25
36
|
child_transport.exit_with_error(e)
|
26
37
|
exit
|
27
38
|
end
|
28
|
-
child_transport.
|
29
|
-
child_transport.wait_for_ack
|
39
|
+
child_transport.done_and_wait_for_ack
|
30
40
|
end
|
31
|
-
Process.detach(process_id)
|
32
|
-
process_id
|
33
41
|
end
|
34
42
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
unless base_rss
|
47
|
-
base_rss = rss(process_id)
|
48
|
-
requires << {base: true, rss: base_rss}
|
49
|
-
end
|
50
|
-
parent_transport.start
|
51
|
-
elsif msg == :exit
|
52
|
-
puts "Exiting because of exception: #{payload}"
|
53
|
-
exit 1
|
54
|
-
elsif msg == :done
|
55
|
-
break
|
56
|
-
else
|
57
|
-
fail "Unknown message: #{msg} (#{payload.inspect})"
|
58
|
-
end
|
43
|
+
def detach_process(pid)
|
44
|
+
Process.detach(pid)
|
45
|
+
end
|
46
|
+
|
47
|
+
def collect_requires(transport, process_id)
|
48
|
+
requires_context = {base_rss: nil, requires: [], process_id: process_id, transport: transport}
|
49
|
+
|
50
|
+
while (cmd = transport.read_one_command)
|
51
|
+
msg, payload = cmd
|
52
|
+
|
53
|
+
break unless handle_command(msg, payload, requires_context)
|
59
54
|
end
|
60
|
-
|
55
|
+
|
56
|
+
requires_context[:requires]
|
57
|
+
end
|
58
|
+
|
59
|
+
def handle_command(msg, payload, context)
|
60
|
+
case msg
|
61
|
+
when :require
|
62
|
+
context[:requires] << handle_require(context[:process_id], context[:base_rss], payload)
|
63
|
+
when :ready
|
64
|
+
handle_ready(context)
|
65
|
+
context[:transport].start
|
66
|
+
when :done then return false
|
67
|
+
when :exit then handle_exit(payload)
|
68
|
+
end
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
def handle_require(process_id, base_rss, payload)
|
73
|
+
cur_rss = rss(process_id) - base_rss
|
74
|
+
name, parent_name, time = payload
|
75
|
+
|
76
|
+
{name: name, parent_name: parent_name, time: Float(time) * 1000, rss: cur_rss}
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle_exit(payload)
|
80
|
+
puts "Exiting because of exception: #{payload}"
|
81
|
+
exit 1
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle_ready(context)
|
85
|
+
return if context[:base_rss]
|
86
|
+
|
87
|
+
context[:base_rss] = rss(context[:process_id])
|
88
|
+
context[:requires] << {base: true, rss: context[:base_rss]}
|
61
89
|
end
|
62
90
|
|
63
91
|
def try_activate_gem(library)
|
92
|
+
return unless Kernel.respond_to?(:gem)
|
93
|
+
|
64
94
|
gem(library)
|
65
95
|
rescue Gem::LoadError
|
96
|
+
nil
|
66
97
|
end
|
67
98
|
|
68
|
-
|
69
99
|
def pkill(process_id)
|
70
100
|
Process.kill('TERM', process_id)
|
71
101
|
end
|
@@ -78,10 +108,10 @@ module GemFootprintAnalyzer
|
|
78
108
|
child_reader, parent_writer = IO.pipe
|
79
109
|
parent_reader, child_writer = IO.pipe
|
80
110
|
|
81
|
-
child_transport = GemFootprintAnalyzer::
|
82
|
-
parent_transport = GemFootprintAnalyzer::
|
111
|
+
child_transport = GemFootprintAnalyzer::Transport.new(child_reader, child_writer)
|
112
|
+
parent_transport = GemFootprintAnalyzer::Transport.new(parent_reader, parent_writer)
|
83
113
|
|
84
114
|
[child_transport, parent_transport]
|
85
115
|
end
|
86
116
|
end
|
87
|
-
end
|
117
|
+
end
|
@@ -1,15 +1,20 @@
|
|
1
1
|
module GemFootprintAnalyzer
|
2
|
+
# A class handling sampling and calculating basic statistical values from the set of runs.
|
2
3
|
class AverageRunner
|
3
4
|
RUNS = 10
|
4
|
-
AVERAGED_FIELDS = %i[rss time]
|
5
|
+
AVERAGED_FIELDS = %i[rss time].freeze
|
5
6
|
|
6
|
-
|
7
|
-
|
7
|
+
# @param runs [Integer] optional number of runs to perform
|
8
|
+
# @param run_block [proc] actual unit of work to be done runs times
|
9
|
+
def initialize(runs = RUNS, &run_block)
|
10
|
+
raise ArgumentError, 'runs must be > 0' unless runs > 0
|
8
11
|
|
9
12
|
@run_block = run_block
|
10
13
|
@runs = runs
|
11
14
|
end
|
12
15
|
|
16
|
+
# @return [Array<Hash>] Array of hashes that now include average metrics in place of fields
|
17
|
+
# present in {AVERAGED_FIELDS}. The rest of the columns is copied from the first sample.
|
13
18
|
def run
|
14
19
|
results = []
|
15
20
|
@runs.times do
|
@@ -26,10 +31,7 @@ module GemFootprintAnalyzer
|
|
26
31
|
|
27
32
|
# Take corresponding results array values and compare them
|
28
33
|
def calculate_averages(results)
|
29
|
-
|
30
|
-
first_run = results[0]
|
31
|
-
|
32
|
-
first_run.size.times do |require_number|
|
34
|
+
Array.new(results.first.size) do |require_number|
|
33
35
|
samples = results.map { |r| r[require_number] }
|
34
36
|
first_sample = samples.first
|
35
37
|
|
@@ -39,9 +41,8 @@ module GemFootprintAnalyzer
|
|
39
41
|
|
40
42
|
average[field] = calculate_average(samples.map { |s| s[field] })
|
41
43
|
end
|
42
|
-
|
44
|
+
average
|
43
45
|
end
|
44
|
-
average_results
|
45
46
|
end
|
46
47
|
|
47
48
|
def calculate_average(values)
|
@@ -49,7 +50,7 @@ module GemFootprintAnalyzer
|
|
49
50
|
sum = values.sum.to_f
|
50
51
|
mean = sum / num
|
51
52
|
|
52
|
-
stddev = Math.sqrt(values.sum { |v| (v - mean)
|
53
|
+
stddev = Math.sqrt(values.sum { |v| (v - mean)**2 } / num)
|
53
54
|
{mean: mean, sttdev: stddev}
|
54
55
|
end
|
55
56
|
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module GemFootprintAnalyzer
|
4
|
+
# A command line interface class for the gem.
|
5
|
+
# Provides options parsing and help messages for the user.
|
6
|
+
class CLI
|
7
|
+
def initialize
|
8
|
+
@options = {}
|
9
|
+
@options[:runs] = 10
|
10
|
+
@options[:debug] = false
|
11
|
+
@options[:formatter] = 'tree'
|
12
|
+
@options[:skip_rubygems] = false
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param args [Array<String>] runs the analyzer with parsed args taken as options
|
16
|
+
# @return [void]
|
17
|
+
def run(args = ARGV)
|
18
|
+
opts_parser.parse!(args)
|
19
|
+
|
20
|
+
if args.empty?
|
21
|
+
puts opts_parser
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
require 'rubygems' unless options[:skip_rubygems]
|
25
|
+
|
26
|
+
print_requires(options, args)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def print_requires(options, args)
|
32
|
+
requires_list_average = capture_requires(options, args)
|
33
|
+
at_exit { clean_up }
|
34
|
+
formatter = formatter_instance(options)
|
35
|
+
puts formatter.new(options).format_list(requires_list_average)
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :options
|
39
|
+
|
40
|
+
def capture_requires(options, args)
|
41
|
+
GemFootprintAnalyzer::AverageRunner.new(options[:runs]) do
|
42
|
+
GemFootprintAnalyzer::Analyzer.new.test_library(*args)
|
43
|
+
end.run
|
44
|
+
end
|
45
|
+
|
46
|
+
def formatter_instance(options)
|
47
|
+
require 'gem_footprint_analyzer/formatters/text_base'
|
48
|
+
require 'gem_footprint_analyzer/formatters/tree'
|
49
|
+
require 'gem_footprint_analyzer/formatters/json'
|
50
|
+
|
51
|
+
GemFootprintAnalyzer::Formatters.const_get(options[:formatter].capitalize)
|
52
|
+
end
|
53
|
+
|
54
|
+
def opts_parser # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
55
|
+
@opts_parser ||= OptionParser.new do |opts|
|
56
|
+
opts.banner = banner
|
57
|
+
opts.on('-f', '--formatter FORMATTER', %w[json tree],
|
58
|
+
'Format output using selected formatter (json tree)') do |formatter|
|
59
|
+
|
60
|
+
options[:formatter] = formatter
|
61
|
+
end
|
62
|
+
|
63
|
+
opts.on('-n', '--runs-num NUMBER', OptParse::DecimalInteger, 'Number of runs') do |runs|
|
64
|
+
raise OptionParser::InvalidArgument, 'must be a number greater than 0' if runs < 1
|
65
|
+
|
66
|
+
options[:runs] = runs
|
67
|
+
end
|
68
|
+
|
69
|
+
opts.on('-d', '--debug', 'Show debug information') do |debug|
|
70
|
+
opts.banner += debug_banner if debug
|
71
|
+
|
72
|
+
options[:debug] = debug
|
73
|
+
end
|
74
|
+
|
75
|
+
opts.on(
|
76
|
+
'-g', '--disable-gems',
|
77
|
+
'Don\'t require rubygems (recommended for standard library analyses)'
|
78
|
+
) do |skip_rubygems|
|
79
|
+
|
80
|
+
options[:skip_rubygems] = skip_rubygems
|
81
|
+
end
|
82
|
+
|
83
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
84
|
+
puts opts
|
85
|
+
exit
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def banner
|
91
|
+
script_name = "bundle exec #{File.basename($PROGRAM_NAME)}"
|
92
|
+
|
93
|
+
"GemFootprintAnalyzer (#{GemFootprintAnalyzer::VERSION})\n" \
|
94
|
+
"Usage: #{script_name} library_to_analyze [require]"
|
95
|
+
end
|
96
|
+
|
97
|
+
def debug_banner
|
98
|
+
"\n(#{File.expand_path(File.join(File.dirname(__FILE__), '..'))})"
|
99
|
+
end
|
100
|
+
|
101
|
+
def clean_up
|
102
|
+
fork_waiters = Thread.list.select { |th| th.is_a?(Process::Waiter) }
|
103
|
+
fork_waiters.each { |waiter| Process.kill('TERM', waiter.pid) }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -1,12 +1,14 @@
|
|
1
1
|
module GemFootprintAnalyzer
|
2
2
|
module Formatters
|
3
|
+
# A formatter class outputting bare JSON.
|
4
|
+
# Useful for integrating with other tools.
|
3
5
|
class Json
|
4
6
|
require 'json'
|
5
7
|
|
6
|
-
def initialize(*)
|
7
|
-
end
|
8
|
+
def initialize(*); end
|
8
9
|
|
9
|
-
|
10
|
+
# @return [String] A JSON form of the requires_list array, last entry is the cumulated result.
|
11
|
+
def format_list(requires_list)
|
10
12
|
JSON.dump(requires_list)
|
11
13
|
end
|
12
14
|
end
|
@@ -1,10 +1,14 @@
|
|
1
1
|
module GemFootprintAnalyzer
|
2
2
|
module Formatters
|
3
|
+
# Base class for all text formatters.
|
4
|
+
# Houses boilerplate and disclaimer text methods.
|
3
5
|
class TextBase
|
4
|
-
|
6
|
+
# @param options [Hash<Symbol>] A hash of CLI options, to be used in disclaimer text
|
7
|
+
def initialize(options = {})
|
5
8
|
@options = options
|
6
9
|
end
|
7
10
|
|
11
|
+
# Displays explanatory words for text formatter results
|
8
12
|
def info
|
9
13
|
lines = []
|
10
14
|
lines << "GemFootprintAnalyzer (#{GemFootprintAnalyzer::VERSION})\n"
|
@@ -15,9 +19,10 @@ module GemFootprintAnalyzer
|
|
15
19
|
lines.join("\n")
|
16
20
|
end
|
17
21
|
|
22
|
+
# @return [String] Awesome text separator
|
18
23
|
def dash(length)
|
19
24
|
'-' * length
|
20
25
|
end
|
21
26
|
end
|
22
27
|
end
|
23
|
-
end
|
28
|
+
end
|
@@ -1,10 +1,13 @@
|
|
1
1
|
module GemFootprintAnalyzer
|
2
|
+
# Tree type formatter for results.
|
2
3
|
module Formatters
|
4
|
+
# Prints results with the cumulated entry first and details below, indented to give
|
5
|
+
# information about the calling order of subsequent requires.
|
3
6
|
class Tree < TextBase
|
4
7
|
INDENT = ' '.freeze
|
5
|
-
|
8
|
+
# Formatter helper class representing a single results require entry.
|
6
9
|
class Entry
|
7
|
-
def initialize(entry_hash,
|
10
|
+
def initialize(entry_hash, _options = {})
|
8
11
|
@entry_hash = entry_hash
|
9
12
|
@options = {}
|
10
13
|
end
|
@@ -18,11 +21,13 @@ module GemFootprintAnalyzer
|
|
18
21
|
end
|
19
22
|
|
20
23
|
def time
|
21
|
-
time = @entry_hash.dig(:time, :mean)
|
24
|
+
time = @entry_hash.dig(:time, :mean)
|
25
|
+
time && time.round
|
22
26
|
end
|
23
27
|
|
24
28
|
def rss
|
25
|
-
@entry_hash.dig(:rss, :mean)
|
29
|
+
rss = @entry_hash.dig(:rss, :mean)
|
30
|
+
rss && rss.round
|
26
31
|
end
|
27
32
|
|
28
33
|
def formatted_name
|
@@ -38,35 +43,58 @@ module GemFootprintAnalyzer
|
|
38
43
|
end
|
39
44
|
end
|
40
45
|
|
41
|
-
def
|
46
|
+
def format_list(requires_list)
|
42
47
|
return if requires_list.size == 1
|
43
48
|
|
44
|
-
entries =
|
49
|
+
entries = init_entries(requires_list)
|
50
|
+
info + formatted_entries(entries)
|
51
|
+
end
|
45
52
|
|
46
|
-
|
47
|
-
indent_levels = {root.name => 0}
|
53
|
+
private
|
48
54
|
|
55
|
+
def formatted_entries(entries)
|
56
|
+
indent_levels, max_indent = count_indent_levels(entries)
|
57
|
+
max_name_length = entries.map { |e| e.name.length }.max
|
58
|
+
ljust_value = max_name_length + max_indent + 1
|
49
59
|
|
50
|
-
|
51
|
-
|
60
|
+
lines = entries.reverse.map do |entry|
|
61
|
+
format_entry(entry, indent_levels, ljust_value)
|
52
62
|
end
|
53
63
|
|
54
|
-
|
55
|
-
|
64
|
+
(legend(ljust_value) + lines).join("\n")
|
65
|
+
end
|
56
66
|
|
57
|
-
|
67
|
+
def legend(ljust_value)
|
68
|
+
[
|
69
|
+
'name'.ljust(ljust_value + 2) + 'time' + ' RSS after',
|
70
|
+
dash(ljust_value + 16)
|
71
|
+
]
|
72
|
+
end
|
58
73
|
|
74
|
+
def format_entry(entry, indent_levels, ljust_value)
|
75
|
+
indent = INDENT * indent_levels[entry.name]
|
76
|
+
time = format('%5dms', entry.time)
|
77
|
+
rss = format('%7dKB', entry.rss)
|
59
78
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
79
|
+
"#{indent}#{entry.formatted_name}".ljust(ljust_value) + time + rss
|
80
|
+
end
|
81
|
+
|
82
|
+
def init_entries(requires_list)
|
83
|
+
requires_list.last(requires_list.size - 1).map do |entry_hash|
|
84
|
+
Entry.new(entry_hash, @options)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def count_indent_levels(entries)
|
89
|
+
root = entries.last
|
90
|
+
indent_levels = {root.name => 0}
|
64
91
|
|
65
|
-
|
92
|
+
(entries - [root]).reverse_each do |entry|
|
93
|
+
indent_levels[entry.name] ||= indent_levels.fetch(entry.parent, 0) + 1
|
66
94
|
end
|
67
|
-
|
68
|
-
|
69
|
-
|
95
|
+
max_indent = indent_levels.values.max
|
96
|
+
|
97
|
+
[indent_levels, max_indent * INDENT.size]
|
70
98
|
end
|
71
99
|
end
|
72
100
|
end
|
@@ -1,55 +1,73 @@
|
|
1
1
|
module GemFootprintAnalyzer
|
2
|
+
# A module keeping hacks required to hijack {Kernel.require} and {Kernel.require_relative}
|
3
|
+
# and plug in versions of them that communicate meta data to the {Analyzer}.
|
2
4
|
module RequireSpy
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
class << self
|
6
|
+
def relative_path(caller_entry, require_name = nil)
|
7
|
+
caller_file = caller_entry.split(':')[0]
|
8
|
+
if require_name
|
9
|
+
caller_dir = File.dirname(caller_file)
|
10
|
+
full_path = File.join(caller_dir, require_name)
|
11
|
+
else
|
12
|
+
full_path = caller_file
|
13
|
+
end
|
14
|
+
load_path = $LOAD_PATH.find { |lp| full_path.start_with?(lp) }
|
15
|
+
full_path.sub(%r{\A#{load_path}/}, '')
|
10
16
|
end
|
11
|
-
load_path = $LOAD_PATH.find { |lp| full_path.start_with?(lp) }
|
12
|
-
full_path.sub(/\A#{load_path}\//, '')
|
13
|
-
end
|
14
17
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
18
|
+
def first_foreign_caller(caller)
|
19
|
+
ffc = caller.find do |c|
|
20
|
+
GemFootprintAnalyzer::RequireSpy.relative_path(c) !~ /gem_footprint_analyzer/
|
21
|
+
end
|
22
|
+
GemFootprintAnalyzer::RequireSpy.relative_path(ffc).sub(/\.rb\z/, '') if ffc
|
19
23
|
end
|
20
|
-
end
|
21
24
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
+
def spy_require(transport)
|
26
|
+
alias_require_methods
|
27
|
+
define_timed_exec
|
25
28
|
|
26
|
-
|
27
|
-
|
28
|
-
block.call
|
29
|
-
(Time.now.to_f - start_time).round(4)
|
29
|
+
define_require_relative
|
30
|
+
define_require(transport)
|
30
31
|
end
|
31
32
|
|
32
|
-
|
33
|
-
|
33
|
+
def alias_require_methods
|
34
|
+
Kernel.send :alias_method, :regular_require, :require
|
35
|
+
Kernel.send :alias_method, :regular_require_relative, :require_relative
|
36
|
+
end
|
34
37
|
|
35
|
-
|
36
|
-
|
38
|
+
def define_timed_exec
|
39
|
+
Kernel.send :define_method, :timed_exec do |&block|
|
40
|
+
start_time = Time.now.to_f
|
41
|
+
block.call
|
42
|
+
(Time.now.to_f - start_time).round(4)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def define_require(transport)
|
47
|
+
Kernel.send :define_method, :require do |name|
|
48
|
+
result = nil
|
49
|
+
|
50
|
+
transport.ready
|
51
|
+
transport.wait_for_start
|
37
52
|
|
38
|
-
|
39
|
-
|
53
|
+
t = timed_exec { result = regular_require(name) }
|
54
|
+
|
55
|
+
first_foreign_caller = GemFootprintAnalyzer::RequireSpy.first_foreign_caller(caller)
|
56
|
+
transport.report_require(name, first_foreign_caller || '', t)
|
57
|
+
result
|
40
58
|
end
|
41
|
-
first_foreign_caller = GemFootprintAnalyzer::RequireSpy.first_foreign_caller(caller)
|
42
|
-
interactor.report_require(name, first_foreign_caller || '', t)
|
43
|
-
result
|
44
59
|
end
|
45
60
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
61
|
+
def define_require_relative
|
62
|
+
# As of Ruby 2.5.1, both :require and :require_relative use an unexposed native method
|
63
|
+
# rb_safe_require, however it's challenging to plug into it and using original
|
64
|
+
# :require_relative is not really possible (it does path calculation magic) so instead
|
65
|
+
# we're redirecting :require_relative to the regular :require
|
66
|
+
Kernel.send :define_method, :require_relative do |name|
|
67
|
+
last_caller = caller(1..1).first
|
68
|
+
relative_path = GemFootprintAnalyzer::RequireSpy.relative_path(last_caller, name)
|
69
|
+
return require(relative_path)
|
70
|
+
end
|
53
71
|
end
|
54
72
|
end
|
55
73
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module GemFootprintAnalyzer
|
2
|
+
# A basic transport class that provides a simple text interface and faciliates the
|
3
|
+
# transmission via 2 streams.
|
4
|
+
class Transport
|
5
|
+
# @param read_stream [IO] stream that will be used to read from by this {Transport} instance
|
6
|
+
# @param write_stream [IO] stream that will be used to write to by this {Transport} instance
|
7
|
+
def initialize(read_stream, write_stream)
|
8
|
+
@read_stream = read_stream
|
9
|
+
@write_stream = write_stream
|
10
|
+
end
|
11
|
+
|
12
|
+
# @return [Array] A tuple with command and *payload
|
13
|
+
def read_one_command
|
14
|
+
case read_raw_command
|
15
|
+
when /\A(done|ack|start|ready)\z/
|
16
|
+
[Regexp.last_match(1).to_sym, nil]
|
17
|
+
when /\Arq: "([^"]+)","([^"]*)",([^,]+)\z/
|
18
|
+
[:require, [Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3)]]
|
19
|
+
when /\Aarq: "([^"]+)"\z/
|
20
|
+
[:already_required, Regexp.last_match(1)]
|
21
|
+
when /\Aexit: "([^"]+)"\z/
|
22
|
+
[:exit, Regexp.last_match(1)]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Blocks until a :start command is received from the read stream
|
27
|
+
def wait_for_start
|
28
|
+
while (cmd = read_one_command)
|
29
|
+
msg, = cmd
|
30
|
+
break if msg == :start
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Sends a done command and blocks until ack command is received
|
35
|
+
def done_and_wait_for_ack
|
36
|
+
@write_stream.puts 'done'
|
37
|
+
while (cmd = read_one_command)
|
38
|
+
msg, = cmd
|
39
|
+
break if msg == :ack
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Sends a ready command
|
44
|
+
def ready
|
45
|
+
@write_stream.puts 'ready'
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sends a start command
|
49
|
+
def start
|
50
|
+
@write_stream.puts 'start'
|
51
|
+
end
|
52
|
+
|
53
|
+
# Sends an ack command
|
54
|
+
def ack
|
55
|
+
@write_stream.puts 'ack'
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param library [String] Name of the library that was required
|
59
|
+
# @param source [String] Name of the source file that required the library
|
60
|
+
# @param duration [Float] Time which it took to complete the require
|
61
|
+
def report_require(library, source, duration)
|
62
|
+
@write_stream.puts "rq: #{library.inspect},#{source.inspect},#{duration.inspect}"
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param library [String] Name of the library that was required, but was already required before
|
66
|
+
def report_already_required(library)
|
67
|
+
@write_stream.puts "arq: #{library.inspect}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param error [Exception] Exception object that should halt the program
|
71
|
+
def exit_with_error(error)
|
72
|
+
@write_stream.puts "exit: #{error.to_s.inspect}"
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def read_raw_command
|
78
|
+
@read_stream.gets.strip
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gem_footprint_analyzer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maciek Dubiński
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-10-
|
11
|
+
date: 2018-10-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,34 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.60.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.60.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop-rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
55
83
|
description:
|
56
84
|
email:
|
57
85
|
- maciek@dubinski.net
|
@@ -62,6 +90,8 @@ extra_rdoc_files: []
|
|
62
90
|
files:
|
63
91
|
- ".gitignore"
|
64
92
|
- ".rspec"
|
93
|
+
- ".rubocop"
|
94
|
+
- ".rubocop.yml"
|
65
95
|
- ".travis.yml"
|
66
96
|
- CODE_OF_CONDUCT.md
|
67
97
|
- Gemfile
|
@@ -74,12 +104,12 @@ files:
|
|
74
104
|
- lib/gem_footprint_analyzer.rb
|
75
105
|
- lib/gem_footprint_analyzer/analyzer.rb
|
76
106
|
- lib/gem_footprint_analyzer/average_runner.rb
|
107
|
+
- lib/gem_footprint_analyzer/cli.rb
|
77
108
|
- lib/gem_footprint_analyzer/formatters/json.rb
|
78
|
-
- lib/gem_footprint_analyzer/formatters/text.rb
|
79
109
|
- lib/gem_footprint_analyzer/formatters/text_base.rb
|
80
110
|
- lib/gem_footprint_analyzer/formatters/tree.rb
|
81
|
-
- lib/gem_footprint_analyzer/pipe_transport.rb
|
82
111
|
- lib/gem_footprint_analyzer/require_spy.rb
|
112
|
+
- lib/gem_footprint_analyzer/transport.rb
|
83
113
|
- lib/gem_footprint_analyzer/version.rb
|
84
114
|
homepage: https://github.com/irvingwashington/gem_footprint_analyzer
|
85
115
|
licenses:
|
@@ -1,50 +0,0 @@
|
|
1
|
-
require_relative 'text_base'
|
2
|
-
|
3
|
-
module GemFootprintAnalyzer
|
4
|
-
module Formatters
|
5
|
-
class Text < TextBase
|
6
|
-
TABULATION = ' '.freeze
|
7
|
-
NEWLINE = "\n".freeze
|
8
|
-
|
9
|
-
def format(requires_list)
|
10
|
-
return if requires_list.size == 1
|
11
|
-
entries_num = requires_list.size
|
12
|
-
lines = []
|
13
|
-
longest_name_length = requires_list.map { |el| el[:name]&.length }.compact.max
|
14
|
-
|
15
|
-
lines << [format_name('name', longest_name_length, false), ' time ', 'RSS after'].join(' ')
|
16
|
-
lines << dash(longest_name_length + 22) if requires_list.size > 2
|
17
|
-
|
18
|
-
requires_list.each_with_index do |entry, i|
|
19
|
-
next if i.zero?
|
20
|
-
|
21
|
-
last_element = (i == entries_num - 1)
|
22
|
-
|
23
|
-
name, time, rss = entry.values_at(:name, :time, :rss)
|
24
|
-
lines << dash(longest_name_length + 22) if last_element
|
25
|
-
lines << [format_name(name, longest_name_length, last_element), format_time(time), format_rss(rss)].join(' ')
|
26
|
-
end
|
27
|
-
info + lines.join(NEWLINE)
|
28
|
-
end
|
29
|
-
|
30
|
-
private
|
31
|
-
|
32
|
-
def format_name(name, longest_name_length, last_element = false)
|
33
|
-
left_just_size = longest_name_length + 3
|
34
|
-
left_just_size += TABULATION.size if last_element
|
35
|
-
tabulation = last_element ? '' : TABULATION
|
36
|
-
tabulation + name.ljust(left_just_size)
|
37
|
-
end
|
38
|
-
|
39
|
-
def format_time(time)
|
40
|
-
value = time.is_a?(Hash) ? time[:mean] : time
|
41
|
-
"%4dms" % value.round
|
42
|
-
end
|
43
|
-
|
44
|
-
def format_rss(rss)
|
45
|
-
value = rss.is_a?(Hash) ? rss[:mean] : rss
|
46
|
-
"%6dKB" % value
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
@@ -1,70 +0,0 @@
|
|
1
|
-
module GemFootprintAnalyzer
|
2
|
-
class PipeTransport
|
3
|
-
def initialize(read_stream, write_stream)
|
4
|
-
@read_stream = read_stream
|
5
|
-
@write_stream = write_stream
|
6
|
-
end
|
7
|
-
|
8
|
-
def read_one_command
|
9
|
-
str = @read_stream.gets.strip
|
10
|
-
|
11
|
-
case str
|
12
|
-
when /\Adone\z/
|
13
|
-
[:done, nil]
|
14
|
-
when /\Aack\z/
|
15
|
-
[:ack, nil]
|
16
|
-
when /\Arq: "([^"]+)","([^"]*)",(.+)\z/
|
17
|
-
[:require, [$1, $2, $3]]
|
18
|
-
when /\Aarq: "([^"]+)"\z/
|
19
|
-
[:already_required, $1]
|
20
|
-
when /\Astart\z/
|
21
|
-
[:start, nil]
|
22
|
-
when /\Aready\z/
|
23
|
-
[:ready, nil]
|
24
|
-
when /\Aexit: "([^"]+)"\z/
|
25
|
-
[:exit, $1]
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def wait_for_start
|
30
|
-
while (msg, payload = read_one_command)
|
31
|
-
break if msg == :start
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def wait_for_ack
|
36
|
-
while (msg, data = read_one_command)
|
37
|
-
break if msg == :ack
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def ready
|
42
|
-
@write_stream.puts 'ready'
|
43
|
-
end
|
44
|
-
|
45
|
-
def start
|
46
|
-
@write_stream.puts 'start'
|
47
|
-
end
|
48
|
-
|
49
|
-
def ack
|
50
|
-
@write_stream.puts 'ack'
|
51
|
-
end
|
52
|
-
|
53
|
-
# Signalize finalization
|
54
|
-
def done
|
55
|
-
@write_stream.puts 'done'
|
56
|
-
end
|
57
|
-
|
58
|
-
def report_require(library, source, duration)
|
59
|
-
@write_stream.puts "rq: #{library.inspect},#{source.inspect},#{duration.inspect}"
|
60
|
-
end
|
61
|
-
|
62
|
-
def report_already_required(library)
|
63
|
-
@write_stream.puts "arq: #{library.inspect}"
|
64
|
-
end
|
65
|
-
|
66
|
-
def exit_with_error(e)
|
67
|
-
@write_stream.puts "exit: #{e.to_s.inspect}"
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|