-
Notifications
You must be signed in to change notification settings - Fork 79
/
gem_tracker.rb
162 lines (125 loc) · 3.38 KB
/
gem_tracker.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# frozen_string_literal: true
require "memory_profiler"
require "set"
# Tracks gems that are used by the application.
#
# Compares this set with the gems specified in Gemfile and
# report which gems haven't left any trace during the execution.
class GemTracker
# Based on https://github.com/SamSaffron/memory_profiler/blob/master/lib/memory_profiler/reporter.rb
module ObjectAllocation
module_function
def start
# Die young
3.times { GC.start }
GC.disable
@generation = GC.count
ObjectSpace.trace_object_allocations_start
end
def collect(gems:)
ObjectSpace.trace_object_allocations_stop
ObjectSpace.each_object do |obj|
next unless ObjectSpace.allocation_generation(obj) == @generation
file = ObjectSpace.allocation_sourcefile(obj)
next unless file
gem = GemTracker.helper.guess_gem(file)
next unless gem
gems << gem
end
gems
end
end
# Using TracePoint API https://ruby-doc.org/core-2.6/TracePoint.html
#
# NOTE: This approach is much slower.
module TP
module_function
def start
@gems = Set.new
tp.enable
end
def collect(gems:)
tp.disable
@gems.each { |gem| gems << gem }
end
def tp
TracePoint.trace(:call) do |tp|
name = GemTracker.helper.guess_gem(tp.path)
@gems << name if name
end
end
end
class << self
attr_reader :instance
delegate :report_unused, :stop, :start, :flush, :maybe_flush, to: :instance
def start
collector = ENV["GEM_TRACK"] == "tp" ? TP : ObjectAllocation
@instance = new(collector)
instance.start
end
def helper
@helper ||= MemoryProfiler::Helpers.new
end
end
attr_reader :collector, :gems, :deps
def initialize(collector)
@gems = Set.new
@deps = build_deps
@collector = collector
end
def start
collector.start
end
def maybe_flush
return if rand > 0.4
flush
end
def flush
stop
report_unused(false)
start
end
def stop
collector.collect(gems: gems)
end
def report_unused(print_list = true)
maybe_unused = deps.each_with_object([]) do |name, acc|
acc << name unless gems.include?(name)
end
if print_list
$stdout.puts "\e[33m\nMaybe unused gems:\n\n"
$stdout.puts maybe_unused.sort.join("\n")
$stdout.puts "\e[0m"
end
$stdout.puts "\e[33mTotal maybe unused gems: #{maybe_unused.size} (of #{deps.size})\e[0m"
end
def build_deps
groups = Rails.groups.map(&:to_sym)
Bundler.setup.dependencies.select do |dep|
# Only take into account current env
!(dep.groups & groups).empty?
end.map do |dep|
loaded_spec = Gem.loaded_specs[dep.name]
# skip local gems (loaded from path)
next unless loaded_spec.is_a?(Bundler::RemoteSpecification)
# we need to known the actual load path for the gem (to handle non-RubyGems sources, e.g. Git)
Regexp.last_match[1] if loaded_spec.load_paths.first =~ /\/gems\/([^\/]+)\/lib$/
end.compact
end
end
if ENV["GEM_TRACK"]
RSpec.configure do |config|
config.before(:suite) do
GemTracker.start
end
config.after(:all) do
# flush object space to avoid high memory usage
GemTracker.maybe_flush
end
# Cleanup files
config.after(:suite) do
GemTracker.stop
GemTracker.report_unused
end
end
end