rails-dev-boost 0.1.1 → 0.2.0

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.
data/README.markdown CHANGED
@@ -1,9 +1,11 @@
1
- # RailsDevelopmentBoost
1
+ # Fix slow Rails development mode via `rails-dev-boost`
2
2
 
3
3
  Make your Rails app 10 times faster in development mode (see FAQ below for more details).
4
4
 
5
5
  Alternative to Josh Goebel's [`rails_dev_mode_performance`](https://github.com/yyyc514/rails_dev_mode_performance) plugin.
6
6
 
7
+ Alternative to Robert Pankowecki's [`active_reload`](https://github.com/paneq/active_reload) gem.
8
+
7
9
  ## Branches
8
10
 
9
11
  If you are using **Rails 3**: [`rails-dev-boost/master`](http://github.com/thedarkone/rails-dev-boost/tree/master) branch.
@@ -14,6 +16,23 @@ If you are using **Rails 2.2**: [`rails-dev-boost/rails-2-2`](http://github.com/
14
16
 
15
17
  If you are using **Rails 2.1** or **Rails 2.0** or **anything older**: you are out of luck.
16
18
 
19
+ ## Problems
20
+
21
+ If your app doesn't work with `rails-dev-boost`:
22
+
23
+ * make sure you are not keeping "class-level" references to reloadable constants (see "Known limitations" section below)
24
+ * otherwise **please open an [issue](https://github.com/thedarkone/rails-dev-boost/issues)**!
25
+
26
+ I'm very interested in making the plugin as robust as possible and will work with you on fixing any issues.
27
+
28
+ ### Debug mode
29
+
30
+ There is built-in debug mode in `rails-dev-boost` that can be enabled by putting this line a Rails initializer file:
31
+
32
+ RailsDevelopmentBoost.debug!
33
+
34
+ After restarting your server `rails-dev-boost` will start to spewing detailed tracing information about its actions into your `development.log` file.
35
+
17
36
  ## Background
18
37
 
19
38
  Why create a similar plugin? Because I couldn't get Josh Goebel's to work in my projects. His attempts to keep templates cached in a way that fails with recent versions of Rails. Also, removing the faulty chunk of code revealed another issue: it stats source files that may not exist, without trying to find their real path beforehand. That would be fixable is the code wasn't such a mess (no offense).
@@ -26,9 +45,11 @@ I needed better performance in development mode right away, so here is an altern
26
45
 
27
46
  Usage through `Gemfile`:
28
47
 
29
- group :development do
30
- gem 'rails-dev-boost', :git => 'git://github.com/thedarkone/rails-dev-boost.git', :require => 'rails_development_boost'
31
- end
48
+ ```ruby
49
+ group :development do
50
+ gem 'rails-dev-boost', :git => 'git://github.com/thedarkone/rails-dev-boost.git', :require => 'rails_development_boost'
51
+ end
52
+ ```
32
53
 
33
54
  Installing as a plugin:
34
55
 
@@ -42,7 +63,221 @@ When the server is started in *development* mode, the special unloading mechanis
42
63
 
43
64
  It can also be used in combination with [RailsTestServing](https://github.com/Roman2K/rails-test-serving) for even faster test runs by forcefully enabling it in test mode. To do so, add the following in `config/environments/test.rb`:
44
65
 
45
- def config.soft_reload() true end if RailsTestServing.active?
66
+ ```ruby
67
+ def config.soft_reload() true end if RailsTestServing.active?
68
+ ```
69
+
70
+ ## Known limitations
71
+
72
+ The only code `rails-dev-boost` is unable to handle are "class-level" reloadable constant inter-references ("reloadable" constants are classes/modules that are automatically reloaded in development mode: models, helpers, controllers etc.).
73
+
74
+ ### Class-level reference examples
75
+
76
+ ```ruby
77
+ # app/models/article.rb
78
+ class Article
79
+ end
80
+
81
+ # app/models/blog.rb
82
+ class Blog
83
+ ARTICLE_CLASS = Article # <- stores class-level reference
84
+ @article = Article # <- stores class-level reference
85
+ @@article = Article # <- stores class-level reference
86
+
87
+ MODELS_ARRAY = []
88
+ MODELS_ARRAY << Article # <- stores class-level reference
89
+
90
+ MODELS_CACHE = {}
91
+ MODELS_CACHE['Article'] ||= Article # <- stores class-level reference
92
+
93
+ class << self
94
+ attr_accessor :article_klass
95
+ end
96
+
97
+ self.article_klass = Article # <- stores class-level reference
98
+
99
+ def self.article_klass
100
+ @article_klass ||= Article # <- stores class-level reference
101
+ end
102
+
103
+ def self.article_klass2
104
+ @article_klass ||= 'Article'.constantize # <- stores class-level reference
105
+ end
106
+
107
+ def self.find_article_klass
108
+ const_set(:ARTICLE_CLASS, Article) # <- stores class-level reference
109
+ end
110
+
111
+ def self.all_articles
112
+ # caching object instances is as bad, because each object references its own class
113
+ @all_articles ||= [Article.new, Article.new] # <- stores class-level reference
114
+ end
115
+
116
+ article_kls_ref = Article
117
+ GET_ARTICLE_PROC = Proc.new { article_kls_ref } # <- stores class-level reference via closure
118
+ end
119
+ ```
120
+
121
+ ### What goes wrong
122
+
123
+ Using the example files from above, here's the output from a Rails console:
124
+
125
+ irb(main):001:0> Article
126
+ => Article
127
+ irb(main):002:0> Blog
128
+ => Blog
129
+ irb(main):003:0> Blog.object_id
130
+ => 2182137540
131
+ irb(main):004:0> Article.object_id
132
+ => 2182186060
133
+ irb(main):005:0> Blog::ARTICLE_CLASS.object_id
134
+ => 2182186060
135
+ irb(main):006:0> Blog.all_articles.first.class.object_id
136
+ => 2182186060
137
+
138
+ Now imagine that we change the `app/models/article.rb` and add a new method:
139
+
140
+ ```ruby
141
+ # app/models/article.rb
142
+ class Article
143
+ def say_hello
144
+ puts "Hello world!"
145
+ end
146
+ end
147
+ ```
148
+
149
+ Back in console, trigger an app reload:
150
+
151
+ irb(main):007:0> reload!
152
+ Reloading...
153
+ => true
154
+
155
+ When `app/models/article.rb` file is saved `rails-dev-boost` detects the change and calls `ActiveSupport::Dependencies.remove_constant('Article')` this unloads the `Article` constant. At this point `Article` becomes undefined and `Object.const_defined?('Article')` returns `false`.
156
+
157
+ irb(main):008:0> Object.const_defined?('Article')
158
+ => false
159
+
160
+ However all of the `Blog`'s references to the `Article` class are still valid, so doing something like `Blog::ARTICLE_CLASS.new` will not result into an error:
161
+
162
+ irb(main):009:0> Blog::ARTICLE_CLASS.new
163
+ => #<Article:0x10415b3a0>
164
+ irb(main):010:0> Blog::ARTICLE_CLASS.object_id
165
+ => 2182186060
166
+ irb(main):011:0> Object.const_defined?('Article')
167
+ => false
168
+
169
+ Now lets try calling the newly added method:
170
+
171
+ irb(main):012:0> Blog::ARTICLE_CLASS.new.say_hello
172
+ NoMethodError: undefined method `say_hello' for #<Article:0x104143430>
173
+ from (irb):12
174
+
175
+ As can be seen the new method is nowhere to be found. Lets see if this can be fixed by using the `Article` const directly:
176
+
177
+ irb(main):013:0> Article.new.say_hello
178
+ Hello world!
179
+ => nil
180
+
181
+ Yay, it works! Lets try `Blog::ARTICLE_CLASS` again:
182
+
183
+ irb(main):014:0> Blog::ARTICLE_CLASS.new.say_hello
184
+ NoMethodError: undefined method `say_hello' for #<Article:0x1040b77f0>
185
+ from (irb):14
186
+
187
+ What is happening? When we use the `Article` const directly, since it is undefined Rails does its magic - intercepts the exception and `load`s the `app/models/article.rb`. This creates a brand new `Article` class with the new `object_id` and stuff.
188
+
189
+ irb(main):015:0> Article.object_id
190
+ => 2181443620
191
+ irb(main):016:0> Blog::ARTICLE_CLASS.object_id
192
+ => 2182186060
193
+ irb(main):017:0> Article != Blog::ARTICLE_CLASS
194
+ => true
195
+ irb(main):018:0> Article.public_method_defined?(:say_hello)
196
+ => true
197
+ irb(main):019:0> Blog::ARTICLE_CLASS.public_method_defined?(:say_hello)
198
+ => false
199
+
200
+ Now we've ended up with 2 distinct `Article` classes. To fix the situation we can force `blog.rb` to be reloaded:
201
+
202
+ irb(main):020:0> FileUtils.touch(Rails.root.join('app/models/blog.rb'))
203
+ => ["mongo-boost/app/models/blog.rb"]
204
+ irb(main):021:0> reload!
205
+ Reloading...
206
+ => true
207
+ irb(main):022:0> Blog.object_id
208
+ => 2180872580
209
+ irb(main):023:0> Article.object_id
210
+ => 2181443620
211
+ irb(main):024:0> Blog::ARTICLE_CLASS.object_id
212
+ => 2181443620
213
+ irb(main):025:0> Article == Blog::ARTICLE_CLASS
214
+ => true
215
+ irb(main):026:0> Blog::ARTICLE_CLASS.public_method_defined?(:say_hello)
216
+ => true
217
+ irb(main):027:0> Blog::ARTICLE_CLASS.new.say_hello
218
+ Hello world!
219
+ => nil
220
+
221
+ ### The fix
222
+
223
+ #### Code refactor
224
+
225
+ The best solution is to avoid class-level references at all. A typical bad code looking like this:
226
+
227
+ ```ruby
228
+ # app/models/article.rb
229
+ class Article < ActiveRecord::Base
230
+ end
231
+
232
+ # app/models/blog.rb
233
+ class Blog < ActiveRecord::Base
234
+ def self.all_articles
235
+ @all_articles ||= Article.all
236
+ end
237
+ end
238
+ ```
239
+
240
+ can easily be rewritten like this:
241
+
242
+ ```ruby
243
+ # app/models/article.rb
244
+ class Article < ActiveRecord::Base
245
+ def self.all_articles
246
+ @all_articles ||= all
247
+ end
248
+ end
249
+
250
+ # app/models/blog.rb
251
+ class Blog < ActiveRecord::Base
252
+ def self.all_articles
253
+ Article.all_articles
254
+ end
255
+ end
256
+ ```
257
+
258
+ This way saving `arcticle.rb` will trigger the reload of `@all_articles`.
259
+
260
+ #### require_dependency
261
+
262
+ If the code refactor isn't possible, make use of the `ActiveSupport`'s `require_dependency`:
263
+
264
+ ```ruby
265
+ #app/models/blog.rb
266
+ require_dependency 'article'
267
+
268
+ class Blog < ActiveRecord::Base
269
+ def self.all_articles
270
+ @all_articles ||= Article.all
271
+ end
272
+
273
+ def self.authors
274
+ @all_authors ||= begin
275
+ require_dependency 'author' # dynamic require_dependency is also fine
276
+ Author.all
277
+ end
278
+ end
279
+ end
280
+ ```
46
281
 
47
282
  ## FAQ
48
283
 
@@ -61,40 +296,6 @@ A: You need to force it to be reloaded (just hit the save button in your editor
61
296
  ### Q: I used `require 'article'` and the `Article` model is not being reloaded.
62
297
  A: You really shouldn't be using `require` to load your files in the Rails app (if you want them to be automatically reloaded) and let automatic constant loading handle the require for you. You can also use `require_dependency 'article'`, as it goes through the Rails stack.
63
298
 
64
- ### Q: I'm using class variables (by class variables I mean "metaclass instance variables") and they are not being reloaded.
65
- A: Class level instance variables are not thread safe and you shouldn't be really using them :). There is generally only one case where they might pose a problem for `rails-dev-boost`:
66
-
67
- #app/models/article.rb
68
- class Article < ActiveRecord::Base
69
- end
70
-
71
- #app/models/blog.rb
72
- class Blog < ActiveRecord::Base
73
- def self.all_articles
74
- @all_articles ||= Article.all
75
- end
76
- end
77
-
78
- Modifying `article.rb` will not reload `@all_articles` (you would always need to re-save `blog.rb` as well).
79
-
80
- The solution is to move class instance variable to its class like this:
81
-
82
- #app/models/article.rb
83
- class Article < ActiveRecord::Base
84
- def self.all_articles
85
- @all_articles ||= all
86
- end
87
- end
88
-
89
- #app/models/blog.rb
90
- class Blog < ActiveRecord::Base
91
- def self.all_articles
92
- Article.all_articles
93
- end
94
- end
95
-
96
- This way saving `arcticle.rb` will trigger the reload of `@all_articles`.
97
-
98
299
  ### Q: I'm using JRuby, is it going to work?
99
300
  A: I haven't tested the plugin with JRuby, but the plugin does use `ObjectSpace` to do its magic. `ObjectSpace` is AFAIK disabled by default on JRuby.
100
301
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.0
@@ -0,0 +1 @@
1
+ require 'rails_development_boost' # require the proper module in case somebody forgets to tell bundler to :require => 'rails_development_boost'
@@ -1,32 +1,61 @@
1
1
  module RailsDevelopmentBoost
2
- ActiveSupport.on_load(:after_initialize) do
3
- ReferencePatch.apply!
4
- DependenciesPatch.apply!
5
- DescendantsTrackerPatch.apply!
6
- ObservablePatch.apply!
2
+ class Railtie < ::Rails::Railtie
3
+ config.dev_boost = RailsDevelopmentBoost
7
4
 
8
- # this should go into ActiveSupport.on_load(:action_pack), alas Rails doesn't provide it
9
- if defined?(ActionDispatch::Reloader) # post 0f7c970
10
- ActionDispatch::Reloader.to_prepare { ActiveSupport::Dependencies.unload_modified_files! }
11
- else
12
- ActionDispatch::Callbacks.before { ActiveSupport::Dependencies.unload_modified_files! }
5
+ config.after_initialize do
6
+ if boost_enabled?
7
+ # this should go into ActiveSupport.on_load(:action_pack), alas Rails doesn't provide it
8
+ if supports_reload_classes_only_on_change? # post fa1d9a
9
+ Rails.application.config.reload_classes_only_on_change = true
10
+ Reloader.hook_in!
11
+ elsif defined?(ActionDispatch::Reloader) # post 0f7c970
12
+ ActionDispatch::Reloader.to_prepare(:prepend => true) { ActiveSupport::Dependencies.unload_modified_files! }
13
+ else
14
+ ActionDispatch::Callbacks.before(:prepend => true) { ActiveSupport::Dependencies.unload_modified_files! }
15
+ end
16
+ end
13
17
  end
14
- end
15
-
16
- ActiveSupport.on_load(:action_controller) do
17
- ActiveSupport.on_load(:after_initialize) do
18
- ViewHelpersPatch.apply!
18
+
19
+ delegate :boost_enabled?, :supports_reload_classes_only_on_change?, :to => 'self.class'
20
+
21
+ def self.boost_enabled?
22
+ !$rails_rake_task && (Rails.env.development? || (config.respond_to?(:soft_reload) && config.soft_reload))
23
+ end
24
+
25
+ def self.supports_reload_classes_only_on_change?
26
+ Rails.application.config.respond_to?(:reload_classes_only_on_change)
27
+ end
28
+
29
+ initializer 'dev_boost.setup', :after => :load_active_support do |app|
30
+ if boost_enabled?
31
+ [DependenciesPatch, ReferencePatch, DescendantsTrackerPatch, ObservablePatch, ReferenceCleanupPatch, LoadablePatch].each(&:apply!)
32
+
33
+ if defined?(AbstractController::Helpers)
34
+ ViewHelpersPatch.apply!
35
+ else
36
+ ActiveSupport.on_load(:action_controller) { ViewHelpersPatch.apply! }
37
+ end
38
+ end
19
39
  end
20
40
  end
21
41
 
42
+ autoload :Async, 'rails_development_boost/async'
22
43
  autoload :DependenciesPatch, 'rails_development_boost/dependencies_patch'
23
44
  autoload :DescendantsTrackerPatch, 'rails_development_boost/descendants_tracker_patch'
24
45
  autoload :LoadedFile, 'rails_development_boost/loaded_file'
46
+ autoload :LoadablePatch, 'rails_development_boost/loadable_patch'
25
47
  autoload :ObservablePatch, 'rails_development_boost/observable_patch'
26
48
  autoload :ReferencePatch, 'rails_development_boost/reference_patch'
49
+ autoload :ReferenceCleanupPatch, 'rails_development_boost/reference_cleanup_patch'
50
+ autoload :Reloader, 'rails_development_boost/reloader'
51
+ autoload :RequiredDependency, 'rails_development_boost/required_dependency'
27
52
  autoload :ViewHelpersPatch, 'rails_development_boost/view_helpers_patch'
28
53
 
29
54
  def self.debug!
30
55
  DependenciesPatch.debug!
31
56
  end
57
+
58
+ def self.async!
59
+ DependenciesPatch.async!
60
+ end
32
61
  end
@@ -0,0 +1,73 @@
1
+ require 'rb-fsevent'
2
+ require 'monitor'
3
+
4
+ module RailsDevelopmentBoost
5
+ module Async
6
+ extend self
7
+
8
+ MONITOR = Monitor.new
9
+
10
+ def heartbeat_check!
11
+ if @reactor
12
+ unless @reactor.alive?
13
+ @reactor.stop
14
+ @reactor = nil
15
+ start!
16
+ end
17
+ re_raise_unload_error_if_any
18
+ else
19
+ start!
20
+ end
21
+ @unloaded_something.tap { @unloaded_something = false }
22
+ end
23
+
24
+ def synchronize
25
+ MONITOR.synchronize { yield }
26
+ end
27
+
28
+ private
29
+
30
+ def start!
31
+ @reactor = Reactor.new
32
+ @reactor.watch(ActiveSupport::Dependencies.autoload_paths) {|changed_dirs| unload_affected(changed_dirs)}
33
+ @reactor.start!
34
+ self.unloaded_something = LoadedFile.unload_modified! # don't miss-out on any of the file changes as the async thread hasn't been started as of yet
35
+ end
36
+
37
+ def re_raise_unload_error_if_any
38
+ if e = @unload_error
39
+ @unload_error = nil
40
+ raise e, e.message, e.backtrace
41
+ end
42
+ end
43
+
44
+ def unload_affected(changed_dirs)
45
+ changed_dirs = changed_dirs.map {|changed_dir| File.expand_path(changed_dir).chomp(File::SEPARATOR)}
46
+
47
+ synchronize do
48
+ self.unloaded_something = LoadedFile::LOADED.each_file_unload_if_changed do |file|
49
+ changed_dirs.any? {|changed_dir| file.path.starts_with?(changed_dir)} && file.changed?
50
+ end
51
+ end
52
+ rescue Exception => e
53
+ @unload_error ||= e
54
+ end
55
+
56
+ def unloaded_something=(value)
57
+ @unloaded_something ||= value
58
+ end
59
+
60
+ class Reactor
61
+ delegate :alive?, :to => '@thread'
62
+ delegate :watch, :stop, :to => '@watcher'
63
+
64
+ def initialize
65
+ @watcher = FSEvent.new
66
+ end
67
+
68
+ def start!
69
+ @thread = Thread.new { @watcher.run }
70
+ end
71
+ end
72
+ end
73
+ end