gem_footprint_analyzer 0.1.4 → 0.1.5
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.yml +5 -1
- data/.travis.yml +9 -2
- data/Gemfile +1 -0
- data/Gemfile.lock +2 -2
- data/README.md +332 -274
- data/lib/gem_footprint_analyzer.rb +7 -1
- data/lib/gem_footprint_analyzer/analyzer.rb +15 -46
- data/lib/gem_footprint_analyzer/child_context.rb +58 -0
- data/lib/gem_footprint_analyzer/child_process.rb +55 -0
- data/lib/gem_footprint_analyzer/cli.rb +37 -16
- data/lib/gem_footprint_analyzer/core_ext/array.rb +16 -0
- data/lib/gem_footprint_analyzer/core_ext/file.rb +11 -0
- data/lib/gem_footprint_analyzer/core_ext/hash.rb +18 -0
- data/lib/gem_footprint_analyzer/transport.rb +13 -8
- data/lib/gem_footprint_analyzer/version.rb +1 -1
- metadata +7 -2
@@ -1,9 +1,15 @@
|
|
1
1
|
require 'gem_footprint_analyzer/version'
|
2
2
|
require 'gem_footprint_analyzer/transport'
|
3
|
-
require 'gem_footprint_analyzer/
|
3
|
+
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/core_ext/array'
|
8
|
+
require 'gem_footprint_analyzer/core_ext/hash'
|
9
|
+
require 'gem_footprint_analyzer/core_ext/file'
|
10
|
+
require 'gem_footprint_analyzer/formatters/text_base'
|
11
|
+
require 'gem_footprint_analyzer/formatters/tree'
|
12
|
+
require 'gem_footprint_analyzer/formatters/json'
|
7
13
|
|
8
14
|
module GemFootprintAnalyzer
|
9
15
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module GemFootprintAnalyzer
|
2
2
|
# A class that faciliates sampling of the original require and subsequent require calls from
|
3
3
|
# within the library.
|
4
|
-
# It
|
4
|
+
# It initializes ChildProcess and uses it to start the require cycle in a controlled environment.
|
5
5
|
# Require calls are interwoven with RSS checks done from the parent process, require timing
|
6
|
-
# is gathered in the
|
6
|
+
# is gathered in the child process and passed along to the parent.
|
7
7
|
class Analyzer
|
8
8
|
# @param library [String] name of the library or parameter for the gem method
|
9
9
|
# (ex. 'activerecord', 'activesupport')
|
@@ -11,16 +11,15 @@ module GemFootprintAnalyzer
|
|
11
11
|
# (ex. 'active_record', 'active_support/time')
|
12
12
|
# @return [Array<Hash>] list of require-data-hashes, first element contains base level RSS,
|
13
13
|
# last element can be treated as a summary as effectively it consists of all the previous.
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
child_transport, parent_transport = init_transports
|
18
|
-
|
19
|
-
process_id = fork_and_require(require_string || library, child_transport)
|
20
|
-
fail 'Unable to fork' unless process_id
|
14
|
+
def initialize(fifos)
|
15
|
+
@fifos = fifos
|
16
|
+
end
|
21
17
|
|
22
|
-
|
23
|
-
|
18
|
+
def test_library(library, require_string = nil)
|
19
|
+
child = ChildProcess.new(library, require_string, fifos)
|
20
|
+
child.start_child
|
21
|
+
parent_transport = init_transport
|
22
|
+
requires = collect_requires(parent_transport, child.pid)
|
24
23
|
|
25
24
|
parent_transport.ack
|
26
25
|
requires
|
@@ -28,22 +27,7 @@ module GemFootprintAnalyzer
|
|
28
27
|
|
29
28
|
private
|
30
29
|
|
31
|
-
|
32
|
-
fork do
|
33
|
-
RequireSpy.spy_require(child_transport)
|
34
|
-
begin
|
35
|
-
require(require_string)
|
36
|
-
rescue LoadError => e
|
37
|
-
child_transport.exit_with_error(e)
|
38
|
-
exit
|
39
|
-
end
|
40
|
-
child_transport.done_and_wait_for_ack
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def detach_process(pid)
|
45
|
-
Process.detach(pid)
|
46
|
-
end
|
30
|
+
attr_reader :fifos
|
47
31
|
|
48
32
|
def collect_requires(transport, process_id)
|
49
33
|
requires_context = {base_rss: nil, requires: [], process_id: process_id, transport: transport}
|
@@ -89,30 +73,15 @@ module GemFootprintAnalyzer
|
|
89
73
|
context[:requires] << {base: true, rss: context[:base_rss]}
|
90
74
|
end
|
91
75
|
|
92
|
-
def try_activate_gem(library)
|
93
|
-
return unless Kernel.respond_to?(:gem)
|
94
|
-
|
95
|
-
gem(library)
|
96
|
-
rescue Gem::LoadError
|
97
|
-
nil
|
98
|
-
end
|
99
|
-
|
100
|
-
def pkill(process_id)
|
101
|
-
Process.kill('TERM', process_id)
|
102
|
-
end
|
103
|
-
|
104
76
|
def rss(process_id)
|
105
77
|
`ps -o rss -p #{process_id}`.split.last.strip.to_i
|
106
78
|
end
|
107
79
|
|
108
|
-
def
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
child_transport = GemFootprintAnalyzer::Transport.new(child_reader, child_writer)
|
113
|
-
parent_transport = GemFootprintAnalyzer::Transport.new(parent_reader, parent_writer)
|
80
|
+
def init_transport
|
81
|
+
reader = File.open(fifos[:child], 'r')
|
82
|
+
writer = File.open(fifos[:parent], 'w')
|
114
83
|
|
115
|
-
|
84
|
+
Transport.new(read_stream: reader, write_stream: writer)
|
116
85
|
end
|
117
86
|
end
|
118
87
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require_relative 'require_spy'
|
2
|
+
require_relative 'transport'
|
3
|
+
|
4
|
+
module GemFootprintAnalyzer
|
5
|
+
# A class that is loaded by the child process to faciliate require tracing.
|
6
|
+
# When `start_child_context` env is passed, it is instantiated and start is called automatically
|
7
|
+
# on require.
|
8
|
+
class ChildContext
|
9
|
+
PARENT_FIFO = '/tmp/parent'.freeze
|
10
|
+
CHILD_FIFO = '/tmp/child'.freeze
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
output Process.pid
|
14
|
+
init_transport
|
15
|
+
end
|
16
|
+
|
17
|
+
# Installs the require-spying code and starts requiring
|
18
|
+
def start
|
19
|
+
RequireSpy.spy_require(transport)
|
20
|
+
begin
|
21
|
+
require(require_string)
|
22
|
+
rescue LoadError => e
|
23
|
+
transport.exit_with_error(e)
|
24
|
+
end
|
25
|
+
transport.done_and_wait_for_ack
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :transport
|
31
|
+
|
32
|
+
def child_fifo
|
33
|
+
ENV['child_fifo']
|
34
|
+
end
|
35
|
+
|
36
|
+
def parent_fifo
|
37
|
+
ENV['parent_fifo']
|
38
|
+
end
|
39
|
+
|
40
|
+
def require_string
|
41
|
+
ENV['require_string']
|
42
|
+
end
|
43
|
+
|
44
|
+
def init_transport
|
45
|
+
write_stream = File.open(child_fifo, 'w')
|
46
|
+
read_stream = File.open(parent_fifo, 'r')
|
47
|
+
|
48
|
+
@transport = Transport.new(read_stream: read_stream, write_stream: write_stream)
|
49
|
+
end
|
50
|
+
|
51
|
+
def output(message)
|
52
|
+
STDOUT.puts(message)
|
53
|
+
STDOUT.flush
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
GemFootprintAnalyzer::ChildContext.new.start if ENV['start_child_context']
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'rbconfig'
|
3
|
+
|
4
|
+
module GemFootprintAnalyzer
|
5
|
+
# A class for starting the child process that does actual requires.
|
6
|
+
class ChildProcess
|
7
|
+
RUBY_CMD = [RbConfig.ruby, '--disable=did_you_mean', '--disable=gem'].freeze
|
8
|
+
|
9
|
+
def initialize(library, require_string, fifos)
|
10
|
+
@library = library
|
11
|
+
@require_string = require_string || library
|
12
|
+
@fifos = fifos
|
13
|
+
@pid = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_child
|
17
|
+
@child_thread ||= Thread.new do # rubocop:disable Naming/MemoizedInstanceVariableName
|
18
|
+
Open3.popen3(child_env_vars, *RUBY_CMD, context_file) do |_, stdout, stderr|
|
19
|
+
@pid = stdout.gets.strip.to_i
|
20
|
+
|
21
|
+
while (line = stderr.gets)
|
22
|
+
puts "!! #{line.strip}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def pid
|
29
|
+
return unless child_thread
|
30
|
+
|
31
|
+
sleep 0.01 while @pid.nil?
|
32
|
+
|
33
|
+
@pid
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :require_string, :child_thread, :fifos
|
39
|
+
|
40
|
+
def child_env_vars
|
41
|
+
{
|
42
|
+
'require_string' => require_string,
|
43
|
+
'start_child_context' => 'true',
|
44
|
+
'child_fifo' => fifos[:child],
|
45
|
+
'parent_fifo' => fifos[:parent],
|
46
|
+
'RUBYOPT' => '', # Stop bundler from requiring bundler/setup
|
47
|
+
'RUBYLIB' => $LOAD_PATH.join(':') # Include bundler-provided paths and paths passed by user
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def context_file
|
52
|
+
File.join(__dir__, 'child_context.rb')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'optparse'
|
2
|
+
require 'tmpdir'
|
2
3
|
|
3
4
|
module GemFootprintAnalyzer
|
4
5
|
# A command line interface class for the gem.
|
@@ -9,7 +10,8 @@ module GemFootprintAnalyzer
|
|
9
10
|
@options[:runs] = 10
|
10
11
|
@options[:debug] = false
|
11
12
|
@options[:formatter] = 'tree'
|
12
|
-
|
13
|
+
|
14
|
+
try_require_bundler
|
13
15
|
end
|
14
16
|
|
15
17
|
# @param args [Array<String>] runs the analyzer with parsed args taken as options
|
@@ -21,7 +23,6 @@ module GemFootprintAnalyzer
|
|
21
23
|
puts opts_parser
|
22
24
|
exit 1
|
23
25
|
end
|
24
|
-
require 'rubygems' unless options[:skip_rubygems]
|
25
26
|
|
26
27
|
print_requires(options, args)
|
27
28
|
end
|
@@ -39,15 +40,31 @@ module GemFootprintAnalyzer
|
|
39
40
|
|
40
41
|
def capture_requires(options, args)
|
41
42
|
GemFootprintAnalyzer::AverageRunner.new(options[:runs]) do
|
42
|
-
|
43
|
+
fifos = init_fifos
|
44
|
+
|
45
|
+
GemFootprintAnalyzer::Analyzer.new(fifos).test_library(*args).tap do
|
46
|
+
clean_up_fifos(fifos)
|
47
|
+
end
|
43
48
|
end.run
|
44
49
|
end
|
45
50
|
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
|
51
|
+
def init_fifos
|
52
|
+
dir = Dir.mktmpdir
|
53
|
+
parent_name = File.join(dir, 'parent.fifo')
|
54
|
+
child_name = File.join(dir, 'child.fifo')
|
55
|
+
|
56
|
+
File.mkfifo(parent_name)
|
57
|
+
File.mkfifo(child_name)
|
58
|
+
|
59
|
+
{parent: parent_name, child: child_name}
|
60
|
+
end
|
61
|
+
|
62
|
+
def clean_up_fifos(fifos)
|
63
|
+
fifos.each { |_, name| File.unlink(name) if File.exist?(name) }
|
64
|
+
Dir.unlink(File.dirname(fifos[:parent]))
|
65
|
+
end
|
50
66
|
|
67
|
+
def formatter_instance(options)
|
51
68
|
GemFootprintAnalyzer::Formatters.const_get(options[:formatter].capitalize)
|
52
69
|
end
|
53
70
|
|
@@ -72,14 +89,6 @@ module GemFootprintAnalyzer
|
|
72
89
|
options[:debug] = debug
|
73
90
|
end
|
74
91
|
|
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
92
|
opts.on_tail('-h', '--help', 'Show this message') do
|
84
93
|
puts opts
|
85
94
|
exit
|
@@ -100,7 +109,19 @@ module GemFootprintAnalyzer
|
|
100
109
|
|
101
110
|
def clean_up
|
102
111
|
fork_waiters = Thread.list.select { |th| th.is_a?(Process::Waiter) }
|
103
|
-
fork_waiters.each
|
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
|
104
125
|
end
|
105
126
|
end
|
106
127
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module GemFootprintAnalyzer
|
2
|
+
module CoreExt
|
3
|
+
# Provides Array#sum, missing in Ruby 2.2.0
|
4
|
+
module Array
|
5
|
+
def sum(init = 0, &block)
|
6
|
+
if block
|
7
|
+
reduce(init) { |acc, el| acc + yield(el) }
|
8
|
+
else
|
9
|
+
reduce(init) { |acc, el| acc + el }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Array.include(GemFootprintAnalyzer::CoreExt::Array) unless [].respond_to?(:sum)
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module GemFootprintAnalyzer
|
2
|
+
module CoreExt
|
3
|
+
# Provides File#mkfifo, missing in Ruby 2.2.0
|
4
|
+
module File
|
5
|
+
def mkfifo(name)
|
6
|
+
system("mkfifo #{name}")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
File.extend(GemFootprintAnalyzer::CoreExt::File) unless File.respond_to?(:mkfifo)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module GemFootprintAnalyzer
|
2
|
+
module CoreExt
|
3
|
+
# Provides Hash#dig, missing in Ruby 2.2.0
|
4
|
+
module Hash
|
5
|
+
def dig(*keys)
|
6
|
+
value = self
|
7
|
+
keys.each do |key|
|
8
|
+
return nil if !value.respond_to?(:key) || !value.key?(key)
|
9
|
+
|
10
|
+
value = value[key]
|
11
|
+
end
|
12
|
+
value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Hash.include(GemFootprintAnalyzer::CoreExt::Hash) unless {}.respond_to?(:dig)
|
@@ -4,7 +4,7 @@ module GemFootprintAnalyzer
|
|
4
4
|
class Transport
|
5
5
|
# @param read_stream [IO] stream that will be used to read from by this {Transport} instance
|
6
6
|
# @param write_stream [IO] stream that will be used to write to by this {Transport} instance
|
7
|
-
def initialize(read_stream
|
7
|
+
def initialize(read_stream:, write_stream:)
|
8
8
|
@read_stream = read_stream
|
9
9
|
@write_stream = write_stream
|
10
10
|
end
|
@@ -33,7 +33,7 @@ module GemFootprintAnalyzer
|
|
33
33
|
|
34
34
|
# Sends a done command and blocks until ack command is received
|
35
35
|
def done_and_wait_for_ack
|
36
|
-
|
36
|
+
write_raw_command 'done'
|
37
37
|
while (cmd = read_one_command)
|
38
38
|
msg, = cmd
|
39
39
|
break if msg == :ack
|
@@ -42,38 +42,43 @@ module GemFootprintAnalyzer
|
|
42
42
|
|
43
43
|
# Sends a ready command
|
44
44
|
def ready
|
45
|
-
|
45
|
+
write_raw_command 'ready'
|
46
46
|
end
|
47
47
|
|
48
48
|
# Sends a start command
|
49
49
|
def start
|
50
|
-
|
50
|
+
write_raw_command 'start'
|
51
51
|
end
|
52
52
|
|
53
53
|
# Sends an ack command
|
54
54
|
def ack
|
55
|
-
|
55
|
+
write_raw_command 'ack'
|
56
56
|
end
|
57
57
|
|
58
58
|
# @param library [String] Name of the library that was required
|
59
59
|
# @param source [String] Name of the source file that required the library
|
60
60
|
# @param duration [Float] Time which it took to complete the require
|
61
61
|
def report_require(library, source, duration)
|
62
|
-
|
62
|
+
write_raw_command "rq: #{library.inspect},#{source.inspect},#{duration.inspect}"
|
63
63
|
end
|
64
64
|
|
65
65
|
# @param library [String] Name of the library that was required, but was already required before
|
66
66
|
def report_already_required(library)
|
67
|
-
|
67
|
+
write_raw_command "arq: #{library.inspect}"
|
68
68
|
end
|
69
69
|
|
70
70
|
# @param error [Exception] Exception object that should halt the program
|
71
71
|
def exit_with_error(error)
|
72
|
-
|
72
|
+
write_raw_command "exit: #{error.to_s.inspect}"
|
73
73
|
end
|
74
74
|
|
75
75
|
private
|
76
76
|
|
77
|
+
def write_raw_command(command)
|
78
|
+
@write_stream.puts(command)
|
79
|
+
@write_stream.flush
|
80
|
+
end
|
81
|
+
|
77
82
|
def read_raw_command
|
78
83
|
@read_stream.gets.strip
|
79
84
|
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.5
|
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
|
+
date: 2018-11-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -104,7 +104,12 @@ files:
|
|
104
104
|
- lib/gem_footprint_analyzer.rb
|
105
105
|
- lib/gem_footprint_analyzer/analyzer.rb
|
106
106
|
- lib/gem_footprint_analyzer/average_runner.rb
|
107
|
+
- lib/gem_footprint_analyzer/child_context.rb
|
108
|
+
- lib/gem_footprint_analyzer/child_process.rb
|
107
109
|
- lib/gem_footprint_analyzer/cli.rb
|
110
|
+
- lib/gem_footprint_analyzer/core_ext/array.rb
|
111
|
+
- lib/gem_footprint_analyzer/core_ext/file.rb
|
112
|
+
- lib/gem_footprint_analyzer/core_ext/hash.rb
|
108
113
|
- lib/gem_footprint_analyzer/formatters/json.rb
|
109
114
|
- lib/gem_footprint_analyzer/formatters/text_base.rb
|
110
115
|
- lib/gem_footprint_analyzer/formatters/tree.rb
|