rails-dev-boost 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +240 -39
- data/VERSION +1 -1
- data/lib/rails-dev-boost.rb +1 -0
- data/lib/rails_development_boost.rb +44 -15
- data/lib/rails_development_boost/async.rb +73 -0
- data/lib/rails_development_boost/dependencies_patch.rb +245 -123
- data/lib/rails_development_boost/dependencies_patch/instrumentation_patch.rb +171 -0
- data/lib/rails_development_boost/descendants_tracker_patch.rb +2 -0
- data/lib/rails_development_boost/loadable_patch.rb +18 -0
- data/lib/rails_development_boost/loaded_file.rb +207 -30
- data/lib/rails_development_boost/observable_patch.rb +38 -0
- data/lib/rails_development_boost/reference_cleanup_patch.rb +14 -0
- data/lib/rails_development_boost/reference_patch.rb +4 -0
- data/lib/rails_development_boost/reloader.rb +51 -0
- data/lib/rails_development_boost/required_dependency.rb +36 -0
- data/lib/rails_development_boost/view_helpers_patch.rb +5 -1
- metadata +28 -26
- data/.gitignore +0 -1
- data/Rakefile +0 -52
- data/TODO.txt +0 -2
- data/init.rb +0 -3
- data/rails-dev-boost.gemspec +0 -116
@@ -0,0 +1,171 @@
|
|
1
|
+
module RailsDevelopmentBoost
|
2
|
+
module DependenciesPatch
|
3
|
+
module InstrumentationPatch
|
4
|
+
module Instrumenter
|
5
|
+
delegate :boost_log, :to => 'ActiveSupport::Dependencies'
|
6
|
+
|
7
|
+
def self.included(mod)
|
8
|
+
mod.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def included(klass)
|
13
|
+
(public_instance_methods(false) + private_instance_methods(false) + protected_instance_methods(false)).each do |method|
|
14
|
+
if m = method.to_s.match(/\A(.+)_with_(.+)\Z/)
|
15
|
+
meth_name, extension = m[1], m[2]
|
16
|
+
extension.sub!(/[?!=]\Z/) do |modifier|
|
17
|
+
meth_name << modifier
|
18
|
+
''
|
19
|
+
end
|
20
|
+
klass.alias_method_chain meth_name, extension
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module LoadedFile
|
30
|
+
include Instrumenter
|
31
|
+
|
32
|
+
def boost_inspect
|
33
|
+
"\#<LoadedFile #{relative_path} #{inspect_constants(@constants)}>"
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_constants_with_instrumentation(new_constants)
|
37
|
+
boost_log('ADD_CONSTANTS', "#{boost_inspect} <- #{inspect_constants(new_constants)}")
|
38
|
+
add_constants_without_instrumentation(new_constants)
|
39
|
+
end
|
40
|
+
|
41
|
+
def unload_dependent_file_with_instrumentation(dependent_file)
|
42
|
+
boost_log('UNLOAD_DEPENDENT', "#{boost_inspect}: #{dependent_file.boost_inspect}")
|
43
|
+
unload_dependent_file_without_instrumentation(dependent_file)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
RAILS_ROOT = /\A#{Rails.root.to_s}/
|
48
|
+
|
49
|
+
def inspect_constants(constants_arr)
|
50
|
+
"[#{constants_arr.join(', ')}]"
|
51
|
+
end
|
52
|
+
|
53
|
+
def relative_path
|
54
|
+
@path.sub(RAILS_ROOT, '')
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.included(klass)
|
58
|
+
klass.singleton_class.send :include, ClassMethods
|
59
|
+
super
|
60
|
+
end
|
61
|
+
|
62
|
+
module ClassMethods
|
63
|
+
include Instrumenter
|
64
|
+
|
65
|
+
def unload_modified_with_instrumentation!
|
66
|
+
boost_log('--- START ---')
|
67
|
+
unload_modified_without_instrumentation!.tap do
|
68
|
+
boost_log('--- END ---')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def unload_containing_file_with_instrumentation(const_name, file)
|
73
|
+
boost_log('UNLOAD_CONTAINING_FILE', "#{const_name} -> #{file.boost_inspect}")
|
74
|
+
unload_containing_file_without_instrumentation(const_name, file)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
module Files
|
80
|
+
include Instrumenter
|
81
|
+
|
82
|
+
def unload_modified_file_with_instrumentation(file)
|
83
|
+
boost_log('CHANGED', "#{file.boost_inspect}")
|
84
|
+
unload_modified_file_without_instrumentation(file)
|
85
|
+
end
|
86
|
+
|
87
|
+
def unload_decorator_file_with_instrumentation(file)
|
88
|
+
boost_log('UNLOAD_DECORATOR_FILE', "#{file.boost_inspect}")
|
89
|
+
unload_decorator_file_without_instrumentation(file)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.apply!
|
94
|
+
unless applied?
|
95
|
+
ActiveSupport::Dependencies.extend self
|
96
|
+
RailsDevelopmentBoost::LoadedFile.send :include, LoadedFile
|
97
|
+
RailsDevelopmentBoost::LoadedFile::Files.send :include, Files
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.applied?
|
102
|
+
ActiveSupport::Dependencies.singleton_class.include?(self)
|
103
|
+
end
|
104
|
+
|
105
|
+
def load_file_without_constant_tracking(path, *args)
|
106
|
+
other_args = ", #{args.map(&:inspect).join(', ')}" if args.any?
|
107
|
+
boost_log('LOAD', "load_file(#{path.inspect}#{other_args})")
|
108
|
+
super
|
109
|
+
end
|
110
|
+
|
111
|
+
def remove_constant_without_handling_of_connections(const_name)
|
112
|
+
boost_log('REMOVE_CONST', const_name)
|
113
|
+
super
|
114
|
+
end
|
115
|
+
|
116
|
+
def load_file_from_explicit_load(expanded_path)
|
117
|
+
boost_log('EXPLICIT_LOAD_REQUEST', expanded_path)
|
118
|
+
super
|
119
|
+
end
|
120
|
+
|
121
|
+
def boost_log(action, msg = nil)
|
122
|
+
action, msg = msg, action unless msg
|
123
|
+
raw_boost_log("#{ "[#{action}] " if action}#{msg}")
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def unprotected_remove_constant(const_name)
|
128
|
+
boost_log('REMOVING', const_name)
|
129
|
+
@removal_nesting = (@removal_nesting || 0) + 1
|
130
|
+
super
|
131
|
+
ensure
|
132
|
+
@removal_nesting -= 1
|
133
|
+
end
|
134
|
+
|
135
|
+
def error_loading_file(file_path, e)
|
136
|
+
description = RailsDevelopmentBoost::LoadedFile.loaded?(file_path) ? RailsDevelopmentBoost::LoadedFile.for(file_path).boost_inspect : file_path
|
137
|
+
boost_log('ERROR_WHILE_LOADING', "#{description}: #{e.inspect}")
|
138
|
+
super
|
139
|
+
end
|
140
|
+
|
141
|
+
def remove_explicit_dependency(const_name, depending_const)
|
142
|
+
boost_log('EXPLICIT_DEPENDENCY', "#{const_name} -> #{depending_const}")
|
143
|
+
super
|
144
|
+
end
|
145
|
+
|
146
|
+
def remove_dependent_constant(original_module, dependent_module)
|
147
|
+
boost_log('DEPENDENT_MODULE', "#{original_module._mod_name} -> #{dependent_module._mod_name}")
|
148
|
+
super
|
149
|
+
end
|
150
|
+
|
151
|
+
def remove_autoloaded_parent_module(initial_object, parent_object)
|
152
|
+
boost_log('REMOVE_PARENT', "#{initial_object._mod_name} -> #{parent_object._mod_name}")
|
153
|
+
super
|
154
|
+
end
|
155
|
+
|
156
|
+
def remove_child_module_constant(parent_object, full_child_const_name)
|
157
|
+
boost_log('REMOVE_CHILD', "#{parent_object._mod_name} -> #{full_child_const_name}")
|
158
|
+
super
|
159
|
+
end
|
160
|
+
|
161
|
+
def remove_nested_constant(parent_const, child_const)
|
162
|
+
boost_log('REMOVE_NESTED', "#{parent_const} :: #{child_const.sub(/\A#{parent_const}::/, '')}")
|
163
|
+
super
|
164
|
+
end
|
165
|
+
|
166
|
+
def raw_boost_log(msg)
|
167
|
+
Rails.logger.info("[DEV-BOOST] #{"\t" * (@removal_nesting || 0)}#{msg}")
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module RailsDevelopmentBoost
|
2
|
+
module LoadablePatch
|
3
|
+
def self.apply!
|
4
|
+
Object.send :include, LoadablePatch
|
5
|
+
end
|
6
|
+
|
7
|
+
def load(file, wrap = false)
|
8
|
+
expanded_path = File.expand_path(file)
|
9
|
+
# force the manual #load calls for autoloadable files to go through the AS::Dep stack
|
10
|
+
if ActiveSupport::Dependencies.in_autoload_path?(expanded_path)
|
11
|
+
expanded_path << '.rb' unless expanded_path =~ /\.(rb|rake)\Z/
|
12
|
+
ActiveSupport::Dependencies.load_file_from_explicit_load(expanded_path)
|
13
|
+
else
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,13 +1,98 @@
|
|
1
1
|
module RailsDevelopmentBoost
|
2
2
|
class LoadedFile
|
3
|
-
|
3
|
+
class Files < Hash
|
4
|
+
def initialize(*args)
|
5
|
+
super {|hash, file_path| hash[file_path] = LoadedFile.new(file_path)}
|
6
|
+
end
|
7
|
+
|
8
|
+
def unload_modified!
|
9
|
+
each_file_unload_if_changed {|file| file.changed?}
|
10
|
+
end
|
11
|
+
|
12
|
+
def each_file_unload_if_changed
|
13
|
+
unloaded_something = false
|
14
|
+
values.each do |file|
|
15
|
+
if yield(file)
|
16
|
+
unload_modified_file(file)
|
17
|
+
unloaded_something = true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
if unloaded_something
|
21
|
+
values.each do |file|
|
22
|
+
unload_decorator_file(file) if file.decorator_like?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
unloaded_something
|
26
|
+
end
|
27
|
+
|
28
|
+
def unload_modified_file(file)
|
29
|
+
file.unload!
|
30
|
+
end
|
31
|
+
|
32
|
+
def unload_decorator_file(file)
|
33
|
+
file.unload!
|
34
|
+
end
|
35
|
+
|
36
|
+
def constants
|
37
|
+
values.map(&:constants).flatten
|
38
|
+
end
|
39
|
+
|
40
|
+
def stored?(file)
|
41
|
+
key?(file.path) && self[file.path] == file
|
42
|
+
end
|
43
|
+
|
44
|
+
alias_method :loaded?, :key?
|
45
|
+
end
|
4
46
|
|
5
|
-
class
|
6
|
-
|
47
|
+
class ConstantsToFiles < Hash
|
48
|
+
def associate(const_name, file)
|
49
|
+
(self[const_name] ||= []) << file
|
50
|
+
end
|
51
|
+
|
52
|
+
def deassociate(const_name, file)
|
53
|
+
if files = self[const_name]
|
54
|
+
files.delete(file)
|
55
|
+
delete(const_name) if files.empty?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def each_file_with_const(const_name, &block)
|
60
|
+
if files = self[const_name]
|
61
|
+
files.dup.each(&block)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Interdependencies < Hash
|
67
|
+
def associate(file_a, file_b)
|
68
|
+
(self[file_a] ||= Set.new) << file_b
|
69
|
+
(self[file_b] ||= Set.new) << file_a
|
70
|
+
end
|
71
|
+
|
72
|
+
def each_dependent_on(file)
|
73
|
+
if deps = delete(file)
|
74
|
+
deps.each do |dep|
|
75
|
+
deassociate(dep, file)
|
76
|
+
yield dep
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
def deassociate(file_a, file_b)
|
83
|
+
if deps = self[file_a]
|
84
|
+
deps.delete(file_b)
|
85
|
+
delete(file_a) if deps.empty?
|
86
|
+
end
|
87
|
+
end
|
7
88
|
end
|
8
89
|
|
90
|
+
LOADED = Files.new
|
91
|
+
CONSTANTS_TO_FILES = ConstantsToFiles.new
|
92
|
+
INTERDEPENDENCIES = Interdependencies.new
|
93
|
+
NOW_UNLOADING = Set.new
|
94
|
+
|
9
95
|
attr_accessor :path, :constants
|
10
|
-
delegate :constants_to_files, :to => 'self.class'
|
11
96
|
|
12
97
|
def initialize(path, constants=[])
|
13
98
|
@path = path
|
@@ -21,53 +106,145 @@ module RailsDevelopmentBoost
|
|
21
106
|
end
|
22
107
|
|
23
108
|
def add_constants(new_constants)
|
24
|
-
new_constants.each
|
25
|
-
(constants_to_files[new_constant] ||= []) << self
|
26
|
-
end
|
109
|
+
new_constants.each {|new_constant| CONSTANTS_TO_FILES.associate(new_constant, self)}
|
27
110
|
@constants |= new_constants
|
28
|
-
retrieve_associated_files.each {|file| file.add_constants(@constants)} if @associated_files
|
29
111
|
end
|
30
112
|
|
31
|
-
|
32
|
-
|
33
|
-
|
113
|
+
# "decorator" files are popular with certain Rails frameworks (spree/refinerycms etc.) they don't define their own constants, instead
|
114
|
+
# they are usually used for adding methods to other classes via Model.class_eval { def meth; end }
|
115
|
+
def decorator_like?
|
116
|
+
@constants.empty? && !INTERDEPENDENCIES[self]
|
34
117
|
end
|
35
118
|
|
36
|
-
def
|
37
|
-
|
119
|
+
def unload!
|
120
|
+
guard_double_unloading do
|
121
|
+
INTERDEPENDENCIES.each_dependent_on(self) {|dependent_file| unload_dependent_file(dependent_file)}
|
122
|
+
@constants.dup.each {|const| ActiveSupport::Dependencies.remove_constant(const)}
|
123
|
+
clean_up_if_necessary
|
124
|
+
end
|
38
125
|
end
|
39
126
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
127
|
+
def associate_to_greppable_constants # brute-force approach
|
128
|
+
# we don't know anything about the constants contained in the files up the currently_loading stack
|
129
|
+
ActiveSupport::Dependencies.currently_loading.each {|path| self.class.relate_files(path, self)}
|
130
|
+
add_constants(greppable_constants)
|
43
131
|
end
|
44
132
|
|
45
|
-
|
46
|
-
|
133
|
+
# It is important to catch all the intermediate constants as they might be "nested" constants, that are generally not tracked by AS::Dependencies.
|
134
|
+
# Pathological example is as follows:
|
135
|
+
#
|
136
|
+
# File `a.rb` contains `class A; X = :x; end`. AS::Dependencies only associates the 'A' const to the `a.rb` file (ignoring the nested 'A::X'), while
|
137
|
+
# a decorator file `b_decorator.rb` containing `B.class_eval {A::X}` would grep and find the `A::X` const, check that indeed it is in autoloaded
|
138
|
+
# namespace and associate it to itself. When `b_decorator.rb` is then being unloaded it simply does `remove_constant('A::X')` while failing to trigger
|
139
|
+
# the unloading of `a.rb`.
|
140
|
+
def greppable_constants
|
141
|
+
constants = []
|
142
|
+
read_greppable_constants.each do |const_name|
|
143
|
+
intermediates = nil
|
144
|
+
begin
|
145
|
+
if self.class.loaded_constant?(const_name)
|
146
|
+
constants << const_name
|
147
|
+
constants.concat(intermediates) if intermediates
|
148
|
+
break
|
149
|
+
end
|
150
|
+
(intermediates ||= []) << const_name.dup
|
151
|
+
end while const_name.sub!(/::[^:]+\Z/, '')
|
152
|
+
end
|
153
|
+
constants.uniq
|
154
|
+
end
|
155
|
+
|
156
|
+
def read_greppable_constants
|
157
|
+
File.read(@path).scan(/[A-Z][_A-Za-z0-9]*(?:::[A-Z][_A-Za-z0-9]*)*/).uniq
|
47
158
|
end
|
48
159
|
|
49
|
-
|
50
|
-
|
51
|
-
|
160
|
+
# consistent hashing
|
161
|
+
def hash
|
162
|
+
@path.hash
|
163
|
+
end
|
164
|
+
|
165
|
+
def eql?(other)
|
166
|
+
@path.eql?(other)
|
167
|
+
end
|
168
|
+
|
169
|
+
def unload_dependent_file(dependent_file)
|
170
|
+
dependent_file.unload!
|
171
|
+
end
|
172
|
+
|
173
|
+
def guard_double_unloading
|
174
|
+
if NOW_UNLOADING.add?(self)
|
175
|
+
begin
|
176
|
+
yield
|
177
|
+
ensure
|
178
|
+
NOW_UNLOADING.delete(self)
|
179
|
+
end
|
52
180
|
end
|
53
181
|
end
|
54
182
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
183
|
+
def delete_constant(const_name)
|
184
|
+
CONSTANTS_TO_FILES.deassociate(const_name, self)
|
185
|
+
@constants.delete(const_name)
|
186
|
+
clean_up_if_necessary
|
187
|
+
end
|
188
|
+
|
189
|
+
def clean_up_if_necessary
|
190
|
+
if @constants.empty? && LOADED.stored?(self)
|
191
|
+
LOADED.delete(@path)
|
192
|
+
ActiveSupport::Dependencies.loaded.delete(require_path)
|
59
193
|
end
|
60
194
|
end
|
61
195
|
|
62
|
-
|
196
|
+
def associate_with(other_loaded_file)
|
197
|
+
INTERDEPENDENCIES.associate(self, other_loaded_file)
|
198
|
+
end
|
199
|
+
|
200
|
+
def require_path
|
201
|
+
File.expand_path(@path.sub(/\.rb\Z/, '')) # be sure to do the same thing as Dependencies#require_or_load and use the expanded path
|
202
|
+
end
|
63
203
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
67
|
-
|
204
|
+
def stale!
|
205
|
+
@mtime = 0
|
206
|
+
INTERDEPENDENCIES.each_dependent_on(self, &:stale!)
|
207
|
+
end
|
208
|
+
|
209
|
+
class << self
|
210
|
+
def unload_modified!
|
211
|
+
LOADED.unload_modified!
|
212
|
+
end
|
213
|
+
|
214
|
+
def for(file_path)
|
215
|
+
LOADED[file_path]
|
216
|
+
end
|
217
|
+
|
218
|
+
def loaded?(file_path)
|
219
|
+
LOADED.loaded?(file_path)
|
220
|
+
end
|
221
|
+
|
222
|
+
def loaded_constants
|
223
|
+
LOADED.constants
|
224
|
+
end
|
225
|
+
|
226
|
+
def loaded_constant?(const_name)
|
227
|
+
CONSTANTS_TO_FILES[const_name]
|
228
|
+
end
|
229
|
+
|
230
|
+
def unload_files_with_const!(const_name)
|
231
|
+
CONSTANTS_TO_FILES.each_file_with_const(const_name) {|file| unload_containing_file(const_name, file)}
|
232
|
+
end
|
233
|
+
|
234
|
+
def unload_containing_file(const_name, file)
|
235
|
+
file.unload!
|
236
|
+
end
|
237
|
+
|
238
|
+
def const_unloaded(const_name)
|
239
|
+
CONSTANTS_TO_FILES.each_file_with_const(const_name) {|file| file.delete_constant(const_name)}
|
240
|
+
end
|
241
|
+
|
242
|
+
def relate_files(base_file, related_file)
|
243
|
+
LOADED[base_file].associate_with(LOADED[related_file])
|
68
244
|
end
|
69
245
|
end
|
70
246
|
|
247
|
+
private
|
71
248
|
def current_mtime
|
72
249
|
# trying to be more efficient: there is no need for a full-fledged Time instance, just grab the timestamp
|
73
250
|
File.mtime(@path).to_i rescue nil
|