gem_footprint_analyzer 0.1.2 → 0.1.3

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 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