gem_footprint_analyzer 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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