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.
@@ -1,9 +1,15 @@
1
1
  require 'gem_footprint_analyzer/version'
2
2
  require 'gem_footprint_analyzer/transport'
3
- require 'gem_footprint_analyzer/require_spy'
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 forks the original process and runs the first require in that fork explicitly.
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 fork and passed along to the parent.
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 test_library(library, require_string = nil)
15
- try_activate_gem(library)
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
- detach_process(process_id)
23
- requires = collect_requires(parent_transport, process_id)
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
- def fork_and_require(require_string, child_transport)
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 init_transports
109
- child_reader, parent_writer = IO.pipe
110
- parent_reader, child_writer = IO.pipe
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
- [child_transport, parent_transport]
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
- @options[:skip_rubygems] = false
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
- GemFootprintAnalyzer::Analyzer.new.test_library(*args)
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 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'
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 { |waiter| Process.kill('TERM', waiter.pid) }
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, write_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
- @write_stream.puts 'done'
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
- @write_stream.puts 'ready'
45
+ write_raw_command 'ready'
46
46
  end
47
47
 
48
48
  # Sends a start command
49
49
  def start
50
- @write_stream.puts 'start'
50
+ write_raw_command 'start'
51
51
  end
52
52
 
53
53
  # Sends an ack command
54
54
  def ack
55
- @write_stream.puts 'ack'
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
- @write_stream.puts "rq: #{library.inspect},#{source.inspect},#{duration.inspect}"
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
- @write_stream.puts "arq: #{library.inspect}"
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
- @write_stream.puts "exit: #{error.to_s.inspect}"
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
@@ -1,3 +1,3 @@
1
1
  module GemFootprintAnalyzer
2
- VERSION = '0.1.4'.freeze
2
+ VERSION = '0.1.5'.freeze
3
3
  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
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-10-28 00:00:00.000000000 Z
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