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