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 +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
data/README.markdown
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
#
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
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
|
+
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
|
-
|
3
|
-
|
4
|
-
DependenciesPatch.apply!
|
5
|
-
DescendantsTrackerPatch.apply!
|
6
|
-
ObservablePatch.apply!
|
2
|
+
class Railtie < ::Rails::Railtie
|
3
|
+
config.dev_boost = RailsDevelopmentBoost
|
7
4
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|