gem_footprint_analyzer 0.1.6 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +65 -18
- data/exe/analyze_requires +2 -0
- data/lib/gem_footprint_analyzer.rb +2 -0
- data/lib/gem_footprint_analyzer/analyzer.rb +8 -6
- data/lib/gem_footprint_analyzer/child_context.rb +43 -8
- data/lib/gem_footprint_analyzer/child_process.rb +5 -2
- data/lib/gem_footprint_analyzer/cli.rb +11 -63
- data/lib/gem_footprint_analyzer/cli/opts.rb +87 -0
- data/lib/gem_footprint_analyzer/cli/utils.rb +16 -0
- data/lib/gem_footprint_analyzer/formatters/json.rb +2 -2
- data/lib/gem_footprint_analyzer/formatters/text_base.rb +2 -2
- data/lib/gem_footprint_analyzer/formatters/tree.rb +12 -2
- data/lib/gem_footprint_analyzer/require_spy.rb +45 -27
- data/lib/gem_footprint_analyzer/transport.rb +4 -7
- data/lib/gem_footprint_analyzer/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 13ad6f4a90d9f22d88152496807371138eecc8456b195ecf63597771c3e5f43d
|
4
|
+
data.tar.gz: 6f78c45ebac12fb4602c61d3ff59b71b9c8dc0dff0fb88a7344ce56506e0c4f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5aa544dacfe1ac6d74622d07ed884e92ddeb40203f460701a2afe05f691bd0df2d495bac22efb3dba8b32d0b678e925ede6d00fbfaf66fe1fcef472c541f83b0
|
7
|
+
data.tar.gz: 52ee797d7c5a24d16effc451eb9d64b5bfd577601767e2efd974d9b7fe013cddceca41d9d89aaffcefb7931f8ac75920869a63fb53e89696dc97b4d169e64459
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,32 +1,28 @@
|
|
1
1
|
# GemFootprintAnalyzer
|
2
2
|
[![Gem Version](https://badge.fury.io/rb/gem_footprint_analyzer.svg)](https://badge.fury.io/rb/gem_footprint_analyzer)
|
3
|
-
[![Build Status](https://travis-ci.
|
3
|
+
[![Build Status](https://api.travis-ci.org/irvingwashington/gem_footprint_analyzer.svg?branch=master)](https://travis-ci.org/irvingwashington/gem_footprint_analyzer)
|
4
4
|
|
5
5
|
A tool for analyzing time and RSS footprint of gems or standard library requires.
|
6
6
|
Requires Ruby >= 2.2.0, works on Linux and MacOS.
|
7
7
|
|
8
|
-
## Installation
|
9
|
-
|
10
|
-
Add this line to your application's Gemfile:
|
11
|
-
|
12
|
-
```ruby
|
13
|
-
gem 'gem_footprint_analyzer', require: false
|
14
|
-
```
|
15
|
-
|
16
|
-
And then execute:
|
17
|
-
|
18
|
-
$ bundle
|
19
|
-
|
20
|
-
Or install it yourself as:
|
21
|
-
|
22
|
-
$ gem install gem_footprint_analyzer
|
23
|
-
|
24
8
|
## Usage
|
25
9
|
|
26
10
|
You can use this gem with or without Bundler. Using Bundler is more convenient and does not have any
|
27
11
|
impact on results, however you have to have `gem_footprint_analyzer` present in your Gemfile
|
28
12
|
(use the development section).
|
29
13
|
|
14
|
+
Banner
|
15
|
+
```bash
|
16
|
+
GemFootprintAnalyzer (0.1.6)
|
17
|
+
Usage: bundle exec analyze_requires library_to_analyze [require]
|
18
|
+
-f, --formatter FORMATTER Format output using selected formatter (json tree)
|
19
|
+
-n, --runs-num NUMBER Number of runs
|
20
|
+
-r, --rubygems Require rubygems before the actual analyze
|
21
|
+
-g, --gemfile Analyze current Gemfile
|
22
|
+
-d, --debug Show debug information
|
23
|
+
-h, --help Show this message
|
24
|
+
```
|
25
|
+
|
30
26
|
1) with Bundler
|
31
27
|
```bash
|
32
28
|
# Standard library
|
@@ -45,6 +41,57 @@ $ analyze_requires net/http
|
|
45
41
|
$ RUBYLIB=/Users/irving/.gem/ruby/2.5.3/gems/activerecord-5.2.1/lib:/Users/irving/.gem/ruby/2.5.3/gems/activesupport-5.2.1/lib/:/Users/irving/.gem/ruby/2.5.3/gems/concurrent-ruby-1.0.5/lib:/Users/irving/.gem/ruby/2.5.3/gems/i18n-1.1.1/lib:/Users/irving/.gem/ruby/2.5.3/gems/activemodel-5.2.1/lib:/Users/irving/.gem/ruby/2.5.3/gems/arel-9.0.0/lib:/Users/irving/.gem/ruby/2.5.3/gems/tzinfo-1.2.5/lib:/Users/irving/.gem/ruby/2.5.3/gems/thread_safe-0.3.6/lib analyze_requries activerecord active_record
|
46
42
|
```
|
47
43
|
|
44
|
+
## Analyzing Gemfile
|
45
|
+
|
46
|
+
GemFootprintAnalyzer can be used to analyze the whole Gemfile of a given project.
|
47
|
+
This way, it is cleary visible which gems take most time or consume most RSS on application start.
|
48
|
+
You can also see what kind of impact does adding a new dependency have on the the project footprint.
|
49
|
+
As processing of all gems might take a lot of time, it is advisable to set the number of runs to 1.
|
50
|
+
|
51
|
+
```bash
|
52
|
+
# Analyze the entire Gemfile, with a single run
|
53
|
+
irving:~/Workspace/motoperf (master)$ bundle exec analyze_requires -gn1
|
54
|
+
GemFootprintAnalyzer (0.1.6)
|
55
|
+
|
56
|
+
Analyze results (average measured from 1 run(s))
|
57
|
+
time is the amount of time given require has taken to complete
|
58
|
+
RSS is total memory increase up to the point after the require
|
59
|
+
|
60
|
+
name time RSS after
|
61
|
+
--------------------------------------------
|
62
|
+
newrelic_rpm 1540ms 38540KB
|
63
|
+
draper 242ms 31544KB
|
64
|
+
activemodel-serializers-xml 46ms 29248KB
|
65
|
+
slim 261ms 29088KB
|
66
|
+
dalli 107ms 26036KB
|
67
|
+
bcrypt 31ms 25872KB
|
68
|
+
will_paginate 45ms 25848KB
|
69
|
+
jbuilder 190ms 25844KB
|
70
|
+
jquery-rails 17ms 25808KB
|
71
|
+
uglifier 135ms 25808KB
|
72
|
+
sass-rails 3379ms 25676KB
|
73
|
+
pg 108ms 13260KB
|
74
|
+
rails 3516ms 12612KB
|
75
|
+
|
76
|
+
Total runtime 10.0740s
|
77
|
+
```
|
78
|
+
|
79
|
+
## Installation
|
80
|
+
|
81
|
+
Add this line to your application's Gemfile:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
gem 'gem_footprint_analyzer', require: false
|
85
|
+
```
|
86
|
+
|
87
|
+
And then execute:
|
88
|
+
|
89
|
+
$ bundle
|
90
|
+
|
91
|
+
Or install it yourself as:
|
92
|
+
|
93
|
+
$ gem install gem_footprint_analyzer
|
94
|
+
|
48
95
|
## Example analyses (Ruby 2.5.3):
|
49
96
|
|
50
97
|
### timeout
|
@@ -397,7 +444,7 @@ Total runtime 0.9878s
|
|
397
444
|
|
398
445
|
## Contributing
|
399
446
|
|
400
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
447
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/irvingwashington/gem_footprint_analyzer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
401
448
|
|
402
449
|
## License
|
403
450
|
|
data/exe/analyze_requires
CHANGED
@@ -4,6 +4,8 @@ require 'gem_footprint_analyzer/child_process'
|
|
4
4
|
require 'gem_footprint_analyzer/analyzer'
|
5
5
|
require 'gem_footprint_analyzer/average_runner'
|
6
6
|
require 'gem_footprint_analyzer/cli'
|
7
|
+
require 'gem_footprint_analyzer/cli/opts'
|
8
|
+
require 'gem_footprint_analyzer/cli/utils'
|
7
9
|
require 'gem_footprint_analyzer/core_ext/array'
|
8
10
|
require 'gem_footprint_analyzer/core_ext/hash'
|
9
11
|
require 'gem_footprint_analyzer/core_ext/file'
|
@@ -6,18 +6,20 @@ module GemFootprintAnalyzer
|
|
6
6
|
# is gathered in the child process and passed along to the parent.
|
7
7
|
class Analyzer
|
8
8
|
# @param fifos [Hash<Symbol>] Hash containing filenames of :child and :parent FIFO files
|
9
|
-
def initialize(fifos)
|
9
|
+
def initialize(fifos, options = {})
|
10
|
+
@options = options
|
10
11
|
@fifos = fifos
|
11
12
|
end
|
12
13
|
|
13
|
-
# @param library [String] name of the library or parameter for the gem method
|
14
|
-
# (ex. 'activerecord', 'activesupport')
|
14
|
+
# @param library [String|nil] name of the library or parameter for the gem method
|
15
|
+
# (ex. 'activerecord', 'activesupport').
|
16
|
+
# Nil if user wants to analyze the whole Gemfile
|
15
17
|
# @param require_string [String|nil] optional require string, if it differs from the gem name
|
16
18
|
# (ex. 'active_record', 'active_support/time')
|
17
19
|
# @return [Array<Hash>] list of require-data-hashes, first element contains base level RSS,
|
18
20
|
# last element can be treated as a summary as effectively it consists of all the previous.
|
19
|
-
def test_library(library, require_string = nil)
|
20
|
-
child = ChildProcess.new(library, require_string, fifos)
|
21
|
+
def test_library(library = nil, require_string = nil)
|
22
|
+
child = ChildProcess.new(library, require_string, fifos, options)
|
21
23
|
child.start_child
|
22
24
|
parent_transport = init_transport
|
23
25
|
requires = collect_requires(parent_transport, child.pid)
|
@@ -28,7 +30,7 @@ module GemFootprintAnalyzer
|
|
28
30
|
|
29
31
|
private
|
30
32
|
|
31
|
-
attr_reader :fifos
|
33
|
+
attr_reader :fifos, :options
|
32
34
|
|
33
35
|
def collect_requires(transport, process_id)
|
34
36
|
requires_context = {base_rss: nil, requires: [], process_id: process_id, transport: transport}
|
@@ -12,19 +12,18 @@ module GemFootprintAnalyzer
|
|
12
12
|
def initialize
|
13
13
|
output Process.pid
|
14
14
|
init_transport
|
15
|
+
require_rubygems_if_needed
|
16
|
+
require_bundler_if_needed
|
15
17
|
end
|
16
18
|
|
17
19
|
# Installs the require-spying code and starts requiring
|
18
20
|
def start
|
19
21
|
RequireSpy.spy_require(transport)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
exit 1
|
26
|
-
end
|
27
|
-
transport.done_and_wait_for_ack
|
22
|
+
error = try_require(require_string)
|
23
|
+
return transport.done_and_wait_for_ack unless error
|
24
|
+
|
25
|
+
transport.exit_with_error(error)
|
26
|
+
exit(1)
|
28
27
|
end
|
29
28
|
|
30
29
|
private
|
@@ -43,6 +42,26 @@ module GemFootprintAnalyzer
|
|
43
42
|
ENV['require_string']
|
44
43
|
end
|
45
44
|
|
45
|
+
def analyze_gemfile
|
46
|
+
ENV['analyze_gemfile']
|
47
|
+
end
|
48
|
+
|
49
|
+
def require_rubygems
|
50
|
+
ENV['require_rubygems']
|
51
|
+
end
|
52
|
+
|
53
|
+
def try_require(require_string)
|
54
|
+
error = nil
|
55
|
+
begin
|
56
|
+
analyze_gemfile ? Bundler.require : require(require_string)
|
57
|
+
rescue LoadError => error
|
58
|
+
warn_about_load_error(error)
|
59
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
60
|
+
warn_about_error(error)
|
61
|
+
end
|
62
|
+
error
|
63
|
+
end
|
64
|
+
|
46
65
|
def init_transport
|
47
66
|
write_stream = File.open(child_fifo, 'w')
|
48
67
|
read_stream = File.open(parent_fifo, 'r')
|
@@ -55,12 +74,28 @@ module GemFootprintAnalyzer
|
|
55
74
|
STDOUT.flush
|
56
75
|
end
|
57
76
|
|
77
|
+
def require_rubygems_if_needed
|
78
|
+
return unless require_rubygems
|
79
|
+
|
80
|
+
require 'rubygems'
|
81
|
+
end
|
82
|
+
|
83
|
+
def require_bundler_if_needed
|
84
|
+
return unless analyze_gemfile
|
85
|
+
|
86
|
+
require 'bundler/setup'
|
87
|
+
end
|
88
|
+
|
58
89
|
def warn_about_load_error(error)
|
59
90
|
possible_gem = error.message.split.last
|
60
91
|
STDERR.puts "Cannot load '#{possible_gem}', this might be an issue with implicit require in" \
|
61
92
|
" the '#{require_string}'"
|
62
93
|
STDERR.puts "If '#{possible_gem}' is a gem, you can try adding it to the Gemfile explicitly" \
|
63
94
|
", running `bundle install` and trying again\n\n"
|
95
|
+
warn_about_error(error)
|
96
|
+
end
|
97
|
+
|
98
|
+
def warn_about_error(error)
|
64
99
|
STDERR.puts error.backtrace
|
65
100
|
STDERR.flush
|
66
101
|
end
|
@@ -7,11 +7,12 @@ module GemFootprintAnalyzer
|
|
7
7
|
LEGACY_RUBY_CMD = [RbConfig.ruby, '--disable=gem'].freeze
|
8
8
|
RUBY_CMD = [RbConfig.ruby, '--disable=did_you_mean', '--disable=gem'].freeze
|
9
9
|
|
10
|
-
def initialize(library, require_string, fifos)
|
10
|
+
def initialize(library, require_string, fifos, options = {})
|
11
11
|
@library = library
|
12
12
|
@require_string = require_string || library
|
13
13
|
@fifos = fifos
|
14
14
|
@pid = nil
|
15
|
+
@options = options
|
15
16
|
end
|
16
17
|
|
17
18
|
def start_child
|
@@ -35,7 +36,7 @@ module GemFootprintAnalyzer
|
|
35
36
|
|
36
37
|
private
|
37
38
|
|
38
|
-
attr_reader :require_string, :child_thread, :fifos
|
39
|
+
attr_reader :require_string, :child_thread, :fifos, :options
|
39
40
|
|
40
41
|
def ruby_command
|
41
42
|
if RbConfig::CONFIG['MAJOR'].to_i >= 2 && RbConfig::CONFIG['MINOR'].to_i >= 3
|
@@ -48,6 +49,8 @@ module GemFootprintAnalyzer
|
|
48
49
|
def child_env_vars
|
49
50
|
{
|
50
51
|
'require_string' => require_string,
|
52
|
+
'require_rubygems' => options.key?(:rubygems) && 'true' || nil,
|
53
|
+
'analyze_gemfile' => options.key?(:analyze_gemfile) && 'true' || nil,
|
51
54
|
'start_child_context' => 'true',
|
52
55
|
'child_fifo' => fifos[:child],
|
53
56
|
'parent_fifo' => fifos[:parent],
|
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'optparse'
|
2
1
|
require 'tmpdir'
|
3
2
|
|
4
3
|
module GemFootprintAnalyzer
|
@@ -11,16 +10,16 @@ module GemFootprintAnalyzer
|
|
11
10
|
@options[:debug] = false
|
12
11
|
@options[:formatter] = 'tree'
|
13
12
|
|
14
|
-
|
13
|
+
@opts = Opts.new(@options)
|
15
14
|
end
|
16
15
|
|
17
16
|
# @param args [Array<String>] runs the analyzer with parsed args taken as options
|
18
17
|
# @return [void]
|
19
18
|
def run(args = ARGV)
|
20
|
-
|
19
|
+
opts.parse!(args)
|
21
20
|
|
22
|
-
if args.empty?
|
23
|
-
puts
|
21
|
+
if !analyze_gemfile? && args.empty?
|
22
|
+
puts opts.parser
|
24
23
|
exit 1
|
25
24
|
end
|
26
25
|
|
@@ -31,18 +30,19 @@ module GemFootprintAnalyzer
|
|
31
30
|
|
32
31
|
def print_requires(options, args)
|
33
32
|
requires_list_average = capture_requires(options, args)
|
34
|
-
at_exit { clean_up }
|
35
33
|
formatter = formatter_instance(options)
|
36
|
-
|
34
|
+
output = formatter.new(options).format_list(requires_list_average)
|
35
|
+
|
36
|
+
Utils.safe_puts(output)
|
37
37
|
end
|
38
38
|
|
39
|
-
attr_reader :options
|
39
|
+
attr_reader :options, :opts
|
40
40
|
|
41
41
|
def capture_requires(options, args)
|
42
42
|
GemFootprintAnalyzer::AverageRunner.new(options[:runs]) do
|
43
43
|
fifos = init_fifos
|
44
44
|
|
45
|
-
GemFootprintAnalyzer::Analyzer.new(fifos).test_library(*args).tap do
|
45
|
+
GemFootprintAnalyzer::Analyzer.new(fifos, options).test_library(*args).tap do
|
46
46
|
clean_up_fifos(fifos)
|
47
47
|
end
|
48
48
|
end.run
|
@@ -68,60 +68,8 @@ module GemFootprintAnalyzer
|
|
68
68
|
GemFootprintAnalyzer::Formatters.const_get(options[:formatter].capitalize)
|
69
69
|
end
|
70
70
|
|
71
|
-
def
|
72
|
-
|
73
|
-
opts.banner = banner
|
74
|
-
opts.on('-f', '--formatter FORMATTER', %w[json tree],
|
75
|
-
'Format output using selected formatter (json tree)') do |formatter|
|
76
|
-
|
77
|
-
options[:formatter] = formatter
|
78
|
-
end
|
79
|
-
|
80
|
-
opts.on('-n', '--runs-num NUMBER', OptParse::DecimalInteger, 'Number of runs') do |runs|
|
81
|
-
fail OptionParser::InvalidArgument, 'must be a number greater than 0' if runs < 1
|
82
|
-
|
83
|
-
options[:runs] = runs
|
84
|
-
end
|
85
|
-
|
86
|
-
opts.on('-d', '--debug', 'Show debug information') do |debug|
|
87
|
-
opts.banner += debug_banner if debug
|
88
|
-
|
89
|
-
options[:debug] = debug
|
90
|
-
end
|
91
|
-
|
92
|
-
opts.on_tail('-h', '--help', 'Show this message') do
|
93
|
-
puts opts
|
94
|
-
exit
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def banner
|
100
|
-
script_name = "bundle exec #{File.basename($PROGRAM_NAME)}"
|
101
|
-
|
102
|
-
"GemFootprintAnalyzer (#{GemFootprintAnalyzer::VERSION})\n" \
|
103
|
-
"Usage: #{script_name} library_to_analyze [require]"
|
104
|
-
end
|
105
|
-
|
106
|
-
def debug_banner
|
107
|
-
"\n(#{File.expand_path(File.join(File.dirname(__FILE__), '..'))})"
|
108
|
-
end
|
109
|
-
|
110
|
-
def clean_up
|
111
|
-
fork_waiters = Thread.list.select { |th| th.is_a?(Process::Waiter) }
|
112
|
-
fork_waiters.each do |waiter|
|
113
|
-
begin
|
114
|
-
Process.kill('TERM', waiter.pid)
|
115
|
-
rescue Errno::ESRCH
|
116
|
-
nil
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
def try_require_bundler
|
122
|
-
require 'bundler/setup'
|
123
|
-
rescue LoadError
|
124
|
-
nil
|
71
|
+
def analyze_gemfile?
|
72
|
+
options[:analyze_gemfile]
|
125
73
|
end
|
126
74
|
end
|
127
75
|
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module GemFootprintAnalyzer
|
4
|
+
class CLI
|
5
|
+
# A class dealing with command line options parsing, validation and displaying banner and
|
6
|
+
# help messages.
|
7
|
+
class Opts
|
8
|
+
# @param options [Hash<Symbol>]
|
9
|
+
def initialize(options)
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param args [Array<String>]
|
14
|
+
def parse!(args)
|
15
|
+
parser.parse!(args)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [OptionParser]
|
19
|
+
def parser # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
20
|
+
@parser ||= OptionParser.new do |opts|
|
21
|
+
opts.banner = banner
|
22
|
+
opts.on('-f', '--formatter FORMATTER', %w[json tree],
|
23
|
+
'Format output using selected formatter (json tree)') do |formatter|
|
24
|
+
|
25
|
+
options[:formatter] = formatter
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on('-n', '--runs-num NUMBER', OptParse::DecimalInteger, 'Number of runs') do |runs|
|
29
|
+
fail OptionParser::InvalidArgument, 'must be a number greater than 0' if runs < 1
|
30
|
+
|
31
|
+
options[:runs] = runs
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on('-r', '--rubygems', 'Require rubygems before the actual analyze') do |rubygems|
|
35
|
+
options[:rubygems] = rubygems
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on('-g', '--gemfile', 'Analyze current Gemfile') do
|
39
|
+
validate_bundler_presence
|
40
|
+
|
41
|
+
options[:rubygems] = true
|
42
|
+
options[:analyze_gemfile] = true
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on('-d', '--debug', 'Show debug information') do |debug|
|
46
|
+
opts.banner += debug_banner if debug
|
47
|
+
|
48
|
+
options[:debug] = debug
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
52
|
+
puts opts
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
attr_reader :options
|
59
|
+
|
60
|
+
def banner
|
61
|
+
script_name = "bundle exec #{File.basename($PROGRAM_NAME)}"
|
62
|
+
|
63
|
+
"GemFootprintAnalyzer (#{GemFootprintAnalyzer::VERSION})\n" \
|
64
|
+
"Usage: #{script_name} library_to_analyze [require]"
|
65
|
+
end
|
66
|
+
|
67
|
+
def debug_banner
|
68
|
+
"\n(#{File.expand_path(File.join(File.dirname(__FILE__), '..'))})"
|
69
|
+
end
|
70
|
+
|
71
|
+
def validate_bundler_presence
|
72
|
+
require 'rubygems'
|
73
|
+
require 'bundler/setup'
|
74
|
+
|
75
|
+
Bundler.root
|
76
|
+
rescue LoadError => e
|
77
|
+
puts "Bundler gem is not available, please install it first (#{e})"
|
78
|
+
|
79
|
+
exit 1
|
80
|
+
rescue GemfileNotFound => e
|
81
|
+
puts "No Gemfile found (#{e})"
|
82
|
+
|
83
|
+
exit 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module GemFootprintAnalyzer
|
2
|
+
class CLI
|
3
|
+
# A module containing helper methods for CLI
|
4
|
+
module Utils
|
5
|
+
def self.safe_puts(output)
|
6
|
+
output ||= "\n"
|
7
|
+
|
8
|
+
string_output = output.is_a?(String) ? output : output.join("\n")
|
9
|
+
|
10
|
+
puts string_output
|
11
|
+
rescue Errno::EPIPE
|
12
|
+
exit
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -3,12 +3,12 @@ module GemFootprintAnalyzer
|
|
3
3
|
# A formatter class outputting bare JSON.
|
4
4
|
# Useful for integrating with other tools.
|
5
5
|
class Json
|
6
|
-
require 'json'
|
7
|
-
|
8
6
|
def initialize(*); end
|
9
7
|
|
10
8
|
# @return [String] A JSON form of the requires_list array, last entry is the cumulated result.
|
11
9
|
def format_list(requires_list)
|
10
|
+
require 'json'
|
11
|
+
|
12
12
|
JSON.dump(requires_list)
|
13
13
|
end
|
14
14
|
end
|
@@ -16,8 +16,8 @@ module GemFootprintAnalyzer
|
|
16
16
|
lines << "Analyze results (average measured from #{@options[:runs]} run(s))"
|
17
17
|
lines << 'time is the amount of time given require has taken to complete'
|
18
18
|
lines << 'RSS is total memory increase up to the point after the require'
|
19
|
-
lines <<
|
20
|
-
lines
|
19
|
+
lines << ''
|
20
|
+
lines
|
21
21
|
end
|
22
22
|
|
23
23
|
# @return [String] Awesome text separator
|
@@ -7,6 +7,8 @@ module GemFootprintAnalyzer
|
|
7
7
|
INDENT = ' '.freeze
|
8
8
|
# Formatter helper class representing a single results require entry.
|
9
9
|
class Entry
|
10
|
+
BUNDLER_RUNTIME = 'bundler/runtime'.freeze
|
11
|
+
|
10
12
|
def initialize(entry_hash, options = {})
|
11
13
|
@entry_hash = entry_hash
|
12
14
|
@options = options
|
@@ -34,6 +36,10 @@ module GemFootprintAnalyzer
|
|
34
36
|
"#{name}#{debug_parent}"
|
35
37
|
end
|
36
38
|
|
39
|
+
def top_level?
|
40
|
+
parent.nil? || parent == BUNDLER_RUNTIME
|
41
|
+
end
|
42
|
+
|
37
43
|
private
|
38
44
|
|
39
45
|
def debug_parent
|
@@ -62,7 +68,7 @@ module GemFootprintAnalyzer
|
|
62
68
|
format_entry(entry, indent_level, ljust_value)
|
63
69
|
end
|
64
70
|
|
65
|
-
(legend(ljust_value) + lines)
|
71
|
+
(legend(ljust_value) + lines)
|
66
72
|
end
|
67
73
|
|
68
74
|
def legend(ljust_value)
|
@@ -81,9 +87,13 @@ module GemFootprintAnalyzer
|
|
81
87
|
end
|
82
88
|
|
83
89
|
def init_entries(requires_list)
|
84
|
-
requires_list.last(requires_list.size - 1).map do |entry_hash|
|
90
|
+
entries = requires_list.last(requires_list.size - 1).map do |entry_hash|
|
85
91
|
Entry.new(entry_hash, @options)
|
86
92
|
end
|
93
|
+
|
94
|
+
entries.select!(&:top_level?) if @options[:analyze_gemfile]
|
95
|
+
|
96
|
+
entries
|
87
97
|
end
|
88
98
|
|
89
99
|
def max_indent(entries)
|
@@ -2,6 +2,10 @@ module GemFootprintAnalyzer
|
|
2
2
|
# A module keeping hacks required to hijack {Kernel.require} and {Kernel.require_relative}
|
3
3
|
# and plug in versions of them that communicate meta data to the {Analyzer}.
|
4
4
|
module RequireSpy
|
5
|
+
# Suitable for versions 3.0.stable up to 5.2.1
|
6
|
+
ACTIVESUPPORT_REQUIRE_DEPENDENCY =
|
7
|
+
%r{active_support/dependencies\.rb.+(`require'|`load_dependency'|`block in require')\z}.freeze
|
8
|
+
|
5
9
|
class << self
|
6
10
|
def relative_path(caller_entry, require_name = nil)
|
7
11
|
caller_file = caller_entry.split(':')[0]
|
@@ -11,67 +15,81 @@ module GemFootprintAnalyzer
|
|
11
15
|
else
|
12
16
|
full_path = caller_file
|
13
17
|
end
|
14
|
-
load_path =
|
18
|
+
load_path = load_paths.find { |lp| full_path.start_with?(lp) }
|
15
19
|
full_path.sub(%r{\A#{load_path}/}, '')
|
16
20
|
end
|
17
21
|
|
22
|
+
def load_paths
|
23
|
+
@load_paths ||= $LOAD_PATH.map { |path| File.expand_path(path) }
|
24
|
+
end
|
25
|
+
|
18
26
|
def without_extension(name)
|
19
27
|
name.sub(/\.rb\z/, '')
|
20
28
|
end
|
21
29
|
|
22
30
|
def first_foreign_caller(caller_list)
|
23
31
|
ffc = caller_list.find do |c|
|
24
|
-
|
32
|
+
c !~ ACTIVESUPPORT_REQUIRE_DEPENDENCY &&
|
33
|
+
relative_path(c) !~ /gem_footprint_analyzer/
|
25
34
|
end
|
26
35
|
without_extension(relative_path(ffc)) if ffc
|
27
36
|
end
|
28
37
|
|
29
38
|
def spy_require(transport)
|
30
39
|
alias_require_methods
|
31
|
-
define_timed_exec
|
32
40
|
|
33
|
-
|
34
|
-
|
41
|
+
define_require_relatives
|
42
|
+
define_requires(transport)
|
35
43
|
end
|
36
44
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
45
|
+
def timed_exec
|
46
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
47
|
+
result = yield
|
48
|
+
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time).round(4)
|
49
|
+
[duration, result]
|
40
50
|
end
|
41
51
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time).round(4)
|
52
|
+
def alias_require_methods
|
53
|
+
kernels.each do |k|
|
54
|
+
k.send :alias_method, :regular_require, :require
|
55
|
+
k.send :alias_method, :regular_require_relative, :require_relative
|
47
56
|
end
|
48
57
|
end
|
49
58
|
|
50
|
-
def
|
51
|
-
Kernel
|
52
|
-
|
59
|
+
def kernels
|
60
|
+
@kernels ||= [(class << ::Kernel; self; end), Kernel]
|
61
|
+
end
|
62
|
+
|
63
|
+
def define_requires(transport)
|
64
|
+
kernels.each { |k| define_require(k, transport) }
|
65
|
+
end
|
53
66
|
|
54
|
-
|
55
|
-
|
67
|
+
def define_require(klass, transport)
|
68
|
+
klass.send :define_method, :require do |name|
|
69
|
+
transport.ready_and_wait_for_start
|
70
|
+
duration, result = GemFootprintAnalyzer::RequireSpy.timed_exec { regular_require(name) }
|
71
|
+
bare_name = GemFootprintAnalyzer::RequireSpy.relative_path(name)
|
56
72
|
|
57
|
-
|
73
|
+
transport.report_require(GemFootprintAnalyzer::RequireSpy.without_extension(bare_name),
|
74
|
+
GemFootprintAnalyzer::RequireSpy.first_foreign_caller(caller),
|
75
|
+
duration)
|
58
76
|
|
59
|
-
first_foreign_caller = GemFootprintAnalyzer::RequireSpy.first_foreign_caller(caller)
|
60
|
-
bare_name = GemFootprintAnalyzer::RequireSpy.without_extension(name)
|
61
|
-
transport.report_require(bare_name, first_foreign_caller || '', t)
|
62
77
|
result
|
63
78
|
end
|
64
79
|
end
|
65
80
|
|
66
|
-
def
|
81
|
+
def define_require_relatives
|
67
82
|
# As of Ruby 2.5.1, both :require and :require_relative use an unexposed native method
|
68
83
|
# rb_safe_require, however it's challenging to plug into it and using original
|
69
84
|
# :require_relative is not really possible (it does path calculation magic) so instead
|
70
85
|
# we're redirecting :require_relative to the regular :require
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
86
|
+
kernels.each do |k|
|
87
|
+
k.send :define_method, :require_relative do |name|
|
88
|
+
last_caller = caller(1..1).first
|
89
|
+
relative_path = GemFootprintAnalyzer::RequireSpy.relative_path(last_caller, name)
|
90
|
+
|
91
|
+
require(relative_path)
|
92
|
+
end
|
75
93
|
end
|
76
94
|
end
|
77
95
|
end
|
@@ -24,7 +24,9 @@ module GemFootprintAnalyzer
|
|
24
24
|
end
|
25
25
|
|
26
26
|
# Blocks until a :start command is received from the read stream
|
27
|
-
def
|
27
|
+
def ready_and_wait_for_start
|
28
|
+
write_raw_command 'ready'
|
29
|
+
|
28
30
|
while (cmd = read_one_command)
|
29
31
|
msg, = cmd
|
30
32
|
break if msg == :start
|
@@ -40,11 +42,6 @@ module GemFootprintAnalyzer
|
|
40
42
|
end
|
41
43
|
end
|
42
44
|
|
43
|
-
# Sends a ready command
|
44
|
-
def ready
|
45
|
-
write_raw_command 'ready'
|
46
|
-
end
|
47
|
-
|
48
45
|
# Sends a start command
|
49
46
|
def start
|
50
47
|
write_raw_command 'start'
|
@@ -59,7 +56,7 @@ module GemFootprintAnalyzer
|
|
59
56
|
# @param source [String] Name of the source file that required the library
|
60
57
|
# @param duration [Float] Time which it took to complete the require
|
61
58
|
def report_require(library, source, duration)
|
62
|
-
write_raw_command "rq: #{library.inspect},#{source.inspect},#{duration.inspect}"
|
59
|
+
write_raw_command "rq: #{library.inspect},#{(source || '').inspect},#{duration.inspect}"
|
63
60
|
end
|
64
61
|
|
65
62
|
# @param library [String] Name of the library that was required, but was already required before
|
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.7
|
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-11-
|
11
|
+
date: 2018-11-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -102,6 +102,8 @@ files:
|
|
102
102
|
- lib/gem_footprint_analyzer/child_context.rb
|
103
103
|
- lib/gem_footprint_analyzer/child_process.rb
|
104
104
|
- lib/gem_footprint_analyzer/cli.rb
|
105
|
+
- lib/gem_footprint_analyzer/cli/opts.rb
|
106
|
+
- lib/gem_footprint_analyzer/cli/utils.rb
|
105
107
|
- lib/gem_footprint_analyzer/core_ext/array.rb
|
106
108
|
- lib/gem_footprint_analyzer/core_ext/file.rb
|
107
109
|
- lib/gem_footprint_analyzer/core_ext/hash.rb
|