gem_footprint_analyzer 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7545b14e023e211b25a3e178e81fea39d7875f5e3cc440e3e2c343fa3e601cd
4
- data.tar.gz: a7c6721d973008c2c5496c93fbecb9584e040d7bc523be3bed704100dfe4eee2
3
+ metadata.gz: 69c4c34c88102817c70c7d4a69e1716152213337d19edcb9139437c8c4ce80f0
4
+ data.tar.gz: 98e61556aa5bf8fdc6c8498f2157e16abb8f45294ed0e5345958652604d0c7a8
5
5
  SHA512:
6
- metadata.gz: b72e58bb39f7e9f9fb886c1e3639b6a38bc8332b4cb7a272a273f132d08d495eda783d858286451562840b7d27c795a352ff3d39e87a044313dc4fcb41787c35
7
- data.tar.gz: 6d75e8a5a5c7502ebc1d19b02654a49376458feba7f1d6c2e07986234ffc614c7638d9a31844b32c0500dc769a20d0ec0c165a1c670ddc5d2fc94450dfc47110
6
+ metadata.gz: b582135703728eaaa2ca71217ed4ad796e7c4add2e78c503887017a9f0666dad5bf09022b6dbc83a63cab5d60102524220ffc7e9b14797cb715dec8e16c64e37
7
+ data.tar.gz: 3c0b25663dd56f72928c0b3e7319e0f2c8c9dffcbefd7d5e6dcac3fa64636b5e10eec91673853cc2e2d2f739cff31851f35d8cc5fe7b9bf6444db95141611e3e
File without changes
@@ -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 "https://rubygems.org"
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
@@ -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
@@ -1,6 +1,6 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
6
+ task default: :spec
@@ -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
- opts.on('-n', '--runs-num NUMBER', OptParse::DecimalInteger, 'Number of runs for avergae') do |runs|
20
- if runs < 1
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
- opts.on('-d', '--debug', 'Show debug information') do |debug|
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
- require 'gem_footprint_analyzer/formatters/text'
48
- require 'gem_footprint_analyzer/formatters/tree'
49
- require 'gem_footprint_analyzer/formatters/json'
8
+ cli = GemFootprintAnalyzer::CLI.new
50
9
 
51
- formatter = GemFootprintAnalyzer::Formatters.const_get(options[:formatter].capitalize)
52
- puts formatter.new(options).format(requires_list_average)
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/pipe_transport'
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
- def test_library(library, require_string=nil)
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
- GC.start
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.done
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 collect_requires(parent_transport, process_id)
36
- base_rss = nil
37
- requires = []
38
- while (msg, payload = parent_transport.read_one_command)
39
- if msg == :require
40
- curr_rss = rss(process_id) - base_rss
41
- name, parent_name, time = payload
42
-
43
- requires << {name: name, parent_name: parent_name, time: Float(time) * 1000, rss: curr_rss}
44
- elsif msg == :already_required
45
- elsif msg == :ready
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
- requires
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::PipeTransport.new(child_reader, child_writer)
82
- parent_transport = GemFootprintAnalyzer::PipeTransport.new(parent_reader, parent_writer)
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
- def initialize(runs=RUNS, &run_block)
7
- fail ArgumentError, 'runs must be > 0' if runs < 1
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
- average_results = []
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
- average_results << average
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) ** 2 } / num)
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
- def format(requires_list)
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
- def initialize(options={})
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, options={})
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)&.round
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)&.round
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 format(requires_list)
46
+ def format_list(requires_list)
42
47
  return if requires_list.size == 1
43
48
 
44
- entries = requires_list.last(requires_list.size - 1).map { |entry_hash| Entry.new(entry_hash, @options) }
49
+ entries = init_entries(requires_list)
50
+ info + formatted_entries(entries)
51
+ end
45
52
 
46
- root = entries.last
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
- (entries - [root]).reverse.each do |entry|
51
- indent_levels[entry.name] ||= indent_levels.fetch(entry.parent, 0) + 1
60
+ lines = entries.reverse.map do |entry|
61
+ format_entry(entry, indent_levels, ljust_value)
52
62
  end
53
63
 
54
- max_name_length = entries.map { |e| e.formatted_name.length }.max
55
- max_indent = indent_levels.values.max
64
+ (legend(ljust_value) + lines).join("\n")
65
+ end
56
66
 
57
- ljust_value = max_name_length + (max_indent * INDENT.size) + 1
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
- lines = entries.reverse.map do |entry|
61
- indent = INDENT * indent_levels[entry.name]
62
- time = "%5dms" % entry.time
63
- rss = "%7dKB" % entry.rss
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
- "#{indent}#{entry.formatted_name}".ljust(ljust_value) + time + rss
92
+ (entries - [root]).reverse_each do |entry|
93
+ indent_levels[entry.name] ||= indent_levels.fetch(entry.parent, 0) + 1
66
94
  end
67
- lines.unshift(dash(ljust_value + 16))
68
- lines.unshift('name'.ljust(ljust_value + 2) + 'time' + ' RSS after')
69
- info + lines.join("\n")
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
- def self.relative_path(caller_entry, require_name=nil)
4
- caller_file = caller_entry.split(':')[0]
5
- if require_name
6
- caller_dir = File.dirname(caller_file)
7
- full_path = File.join(caller_dir, require_name)
8
- else
9
- full_path = caller_file
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
- def self.first_foreign_caller(caller)
16
- ffc = caller.find { |c| GemFootprintAnalyzer::RequireSpy.relative_path(c) !~ /gem_footprint_analyzer/ }
17
- if ffc
18
- GemFootprintAnalyzer::RequireSpy.relative_path(ffc).sub(/\.rb\z/, '')
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
- def self.spy_require(interactor)
23
- Kernel.send :alias_method, :regular_require, :require
24
- Kernel.send :alias_method, :regular_require_relative, :require_relative
25
+ def spy_require(transport)
26
+ alias_require_methods
27
+ define_timed_exec
25
28
 
26
- Kernel.send :define_method, :timed_exec do |&block|
27
- start_time = Time.now.to_f
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
- Kernel.send :define_method, :require do |name|
33
- result = nil
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
- interactor.ready
36
- interactor.wait_for_start
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
- t = timed_exec do
39
- result = regular_require(name)
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
- # As of Ruby 2.5.1, both :require and :require_relative use an unexposed
47
- # native method rb_safe_require, however it's challenging to plug into it
48
- # and using original :require_relative is not really possible (it does path calculation magic)
49
- # so instead we're redirecting :require_relative to the regular :require
50
- Kernel.send :define_method, :require_relative do |name|
51
- relative_path = GemFootprintAnalyzer::RequireSpy.relative_path(caller[0], name)
52
- return require(relative_path)
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
@@ -1,3 +1,3 @@
1
1
  module GemFootprintAnalyzer
2
- VERSION = "0.1.2"
2
+ VERSION = '0.1.3'.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.2
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-23 00:00:00.000000000 Z
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