activerecord-minimizer 0.1.3 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +21 -11
- data/lib/ar_minimizer/census.rb +4 -14
- data/lib/ar_minimizer/loader.rb +8 -1
- data/lib/ar_minimizer/railtie.rb +1 -2
- data/lib/ar_minimizer/version.rb +1 -1
- data/lib/ar_minimizer.rb +0 -1
- metadata +2 -4
- data/lib/ar_minimizer/enums.rb +0 -61
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b96e595f334bcacd836276d8ff508f885079a41d045ef4ffdcc743689ae6469b
|
|
4
|
+
data.tar.gz: 7ecfa22211db6dc240d50dbc20cdcf99f6e133e8e4ba416e54a477c2b3a6a817
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cd2f5496af0696ee69d1eea8e49573c5cd3df9668d34fae090746f8b9f86311c7f3c83caca487eecf639dc6d2e33e49d107f41f7b9c73aa216cc5886cf07f0b
|
|
7
|
+
data.tar.gz: 55114753dad4d0ffad49945183424379059bca3637f0d24517e7bcc6d86cf475646bec3d4e2cc499c76b79b3d3ec900555d41d1a2e267237f3b05695d9472428
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 (2026-11-05)
|
|
4
|
+
|
|
5
|
+
- Removed `config.ar_minimizer.enums` (`ArMinimizer::Enums`). It made enum predicates faster in a benchmark but changed nothing measurable in production, and it no longer worked on Rails main. Setting it does nothing now. `ar_minimizer:census` no longer counts enum methods.
|
|
6
|
+
|
|
7
|
+
## 0.2.0 (2026-10-20)
|
|
8
|
+
|
|
9
|
+
- With `load_time`, the manifest's index (10-20 MB on a large app) is let go once an eager-loading boot is done, before a preloading server forks; a file loaded later indexes the manifest again.
|
|
10
|
+
- README: strict mode and enums are the core; compiling call sites (`load_time`, `views`, `ar_minimizer:build`, `guards`) and small records are experimental, with the production results. The manifest is only needed by the experimental parts.
|
|
11
|
+
|
|
3
12
|
## 0.1.3 (2026-10-18)
|
|
4
13
|
|
|
5
14
|
- `ar_minimizer:manifest` stops, saying why, when no model's table exists (a database not set up yet), instead of writing a manifest that describes no models.
|
data/README.md
CHANGED
|
@@ -4,8 +4,7 @@ Active Record generates a reader, a writer and one method per attribute
|
|
|
4
4
|
method pattern for every column of every model: `name?`, `name_changed?`,
|
|
5
5
|
`name_was`, `saved_change_to_name?`, `name_before_type_cast`,
|
|
6
6
|
`restore_name!`, ... 21 patterns in Rails 8.1, so 21 methods per column.
|
|
7
|
-
activerecord-minimizer generates the readers and writers only
|
|
8
|
-
the call sites that used the rest into the calls they stood for. Part of
|
|
7
|
+
activerecord-minimizer generates the readers and writers only. Part of
|
|
9
8
|
[rails-minify](../rails-minify).
|
|
10
9
|
|
|
11
10
|
```ruby
|
|
@@ -57,7 +56,13 @@ first call to one generates it on that model.
|
|
|
57
56
|
Turn `slim` on in development and tests as well as production: the
|
|
58
57
|
errors then show up while you work and in CI. Slim follows code reloads.
|
|
59
58
|
|
|
60
|
-
##
|
|
59
|
+
## Experimental
|
|
60
|
+
|
|
61
|
+
Each is off unless set. They are faster per call in benchmarks, but made no
|
|
62
|
+
measurable difference on a production app (see [Numbers](#numbers)), and the
|
|
63
|
+
compiling parts cost memory in every worker.
|
|
64
|
+
|
|
65
|
+
### Compiling call sites
|
|
61
66
|
|
|
62
67
|
Calls whose receiver is known to be a record of a model with that column are
|
|
63
68
|
compiled to what the generated method would have done:
|
|
@@ -92,7 +97,7 @@ Where it compiles:
|
|
|
92
97
|
`app/` and `lib/` in place instead, like explode_sqlite. Production only (or
|
|
93
98
|
`FORCE=1`); `DRY_RUN=1` previews.
|
|
94
99
|
|
|
95
|
-
All three read the **manifest
|
|
100
|
+
All three read the **manifest** (strict mode doesn't need one), `config/ar_minimizer.json`, written by
|
|
96
101
|
`bin/rails ar_minimizer:manifest`: per model its columns and their query kind,
|
|
97
102
|
the attribute method patterns, hand-written methods, superclass, app modules,
|
|
98
103
|
associations, scopes, and names it defines another way (an enum predicate
|
|
@@ -103,19 +108,14 @@ stale, and an eager-loading boot logs an error if it is.
|
|
|
103
108
|
Never compiled: methods any model defines by hand, names used as columns,
|
|
104
109
|
primary key methods, `defined?(...)`, calls with blocks, heredoc arguments.
|
|
105
110
|
|
|
106
|
-
|
|
111
|
+
### More
|
|
107
112
|
|
|
108
|
-
* **Enums** (`config.ar_minimizer.enums = true`) replaces the closures Rails
|
|
109
|
-
defines for enum predicates and bang methods, which build and intern the
|
|
110
|
-
`<name>_for_database` symbol on every call, with ordinary methods carrying
|
|
111
|
-
the enum's values: `def published? = public_send(:status_for_database) == 1`.
|
|
112
|
-
1.3–1.6× faster predicates; non-finite float values keep Rails' closures.
|
|
113
113
|
* **Small records** (`config.ar_minimizer.small_records = true`). A loaded
|
|
114
114
|
record owns two Hashes most records never put anything in, 160 bytes each:
|
|
115
115
|
the association cache and the attribute set's Attribute objects. Both start
|
|
116
116
|
as one shared frozen empty Hash and get their own on the first insert. A
|
|
117
117
|
read-only record goes from 1,480 to 1,160 bytes (23 columns), at the same
|
|
118
|
-
speed. Destroyed and frozen records still raise on writes.
|
|
118
|
+
speed. Only worth it on pages that load thousands of records. Destroyed and frozen records still raise on writes.
|
|
119
119
|
* **Census** (`ArMinimizer::Census`, run by rails-minify's
|
|
120
120
|
`bin/rails minify:census`): how many call sites compile, and why the others
|
|
121
121
|
don't.
|
|
@@ -140,6 +140,16 @@ On [Japan Voyage](https://japanvoyage.jp) (191 models, 2009 columns):
|
|
|
140
140
|
* Test suite (4317 runs) in strict mode, after rubocop-minify's rewrites and
|
|
141
141
|
with a 41-line keep list: the same results as the untouched app.
|
|
142
142
|
|
|
143
|
+
In production there (Falcon, 6 workers, half the workers with a part and half
|
|
144
|
+
without, the same traffic for an hour, October 2026):
|
|
145
|
+
|
|
146
|
+
| part | result |
|
|
147
|
+
|---|---|
|
|
148
|
+
| strict mode | no call to a removed method; the same latency and memory |
|
|
149
|
+
| small records | no difference: tens of records per request |
|
|
150
|
+
| compiling in views | no faster, ~22 MB more per worker |
|
|
151
|
+
| compiling at load time | ~12% more CPU per request, not reproduced locally; measured before the app was preloaded, so worth measuring again |
|
|
152
|
+
|
|
143
153
|
## Requirements and caveats
|
|
144
154
|
|
|
145
155
|
* Ruby 3.4+, Rails 8.0, 8.1 and main (`rake test:rails`).
|
data/lib/ar_minimizer/census.rb
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ArMinimizer
|
|
4
|
-
# What the attribute-method
|
|
4
|
+
# What the attribute-method compilers cover in an app.
|
|
5
5
|
module Census
|
|
6
|
-
Result = Data.define(:models, :columns, :stock_methods, :generated_methods, :
|
|
7
|
-
:sites, :files, :manifest) do
|
|
6
|
+
Result = Data.define(:models, :columns, :stock_methods, :generated_methods, :sites, :files, :manifest) do
|
|
8
7
|
def lines(verbose: false)
|
|
9
8
|
[
|
|
10
9
|
"models: #{models}, #{columns} columns",
|
|
11
10
|
"attribute methods: #{stock_methods} in stock Rails, #{generated_methods || "n/a (slim off)"} generated with slim",
|
|
12
|
-
"enum methods: #{enum_compiled}/#{enum_methods} compiled",
|
|
13
11
|
"attribute call sites: #{sites} compilable in #{files} files under app/ and lib/",
|
|
14
12
|
"manifest: #{manifest}",
|
|
15
13
|
]
|
|
@@ -24,22 +22,14 @@ module ArMinimizer
|
|
|
24
22
|
models.each(&:define_attribute_methods)
|
|
25
23
|
models.sum { it.send(:generated_attribute_methods).instance_methods(false).size }
|
|
26
24
|
end
|
|
27
|
-
enum_methods = enum_compiled = 0
|
|
28
|
-
models.each do |model|
|
|
29
|
-
mod = model.instance_variable_get(:@_enum_methods_module) or next
|
|
30
|
-
mod.instance_methods(false).each do |name|
|
|
31
|
-
enum_methods += 1
|
|
32
|
-
enum_compiled += 1 if mod.instance_method(name).source_location&.first.to_s.start_with?("enum ")
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
25
|
sites = files = 0
|
|
36
26
|
Dir.glob(File.join(root.to_s, "{app,lib}", "**", "*.rb")).each do |file|
|
|
37
27
|
n = Rewriter.new(index: index, path: file, guards: guards).rewrite(File.read(file)).sites
|
|
38
28
|
sites += n
|
|
39
29
|
files += 1 if n.positive?
|
|
40
30
|
end
|
|
41
|
-
Result.new(models.size, models.sum { it.attribute_names.size }, stock, generated,
|
|
42
|
-
|
|
31
|
+
Result.new(models.size, models.sum { it.attribute_names.size }, stock, generated, sites, files,
|
|
32
|
+
manifest_state(root, index))
|
|
43
33
|
end
|
|
44
34
|
|
|
45
35
|
def self.manifest_state(root, index)
|
data/lib/ar_minimizer/loader.rb
CHANGED
|
@@ -15,7 +15,7 @@ module ArMinimizer
|
|
|
15
15
|
# version: changes when the index does (Manifest.digest of the file).
|
|
16
16
|
def install!(index:, patterns:, exclude_patterns: nil, version: nil, guards: false)
|
|
17
17
|
require "require-hooks/setup"
|
|
18
|
-
@index = index
|
|
18
|
+
@index = @index_source = index
|
|
19
19
|
@guards = guards
|
|
20
20
|
@mutex = Mutex.new
|
|
21
21
|
if defined?(RequireHooks::Bootsnap) && RequireHooks::Bootsnap.respond_to?(:add_version_hash)
|
|
@@ -33,6 +33,13 @@ module ArMinimizer
|
|
|
33
33
|
@index
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# Lets go of the index (10-20 MB on a large app) once app/ and lib/ are
|
|
37
|
+
# loaded, before a preloading server forks: a file loaded later indexes
|
|
38
|
+
# the manifest again.
|
|
39
|
+
def release_index!
|
|
40
|
+
@mutex.synchronize { @index = @index_source } if @index_source.respond_to?(:call)
|
|
41
|
+
end
|
|
42
|
+
|
|
36
43
|
# nil leaves the file as it is.
|
|
37
44
|
def transform(path, source)
|
|
38
45
|
result = Rewriter.new(index: index, path: path, marker: false, guards: @guards).rewrite(source || File.read(path))
|
data/lib/ar_minimizer/railtie.rb
CHANGED
|
@@ -10,7 +10,6 @@ module ArMinimizer
|
|
|
10
10
|
# config.ar_minimizer.strict = false # ... and generate the rest when first called, instead of removing them
|
|
11
11
|
# config.ar_minimizer.load_time = true # compile app/ and lib/ as they load (needs the manifest)
|
|
12
12
|
# config.ar_minimizer.views = true # compile templates as ActionView loads them
|
|
13
|
-
# config.ar_minimizer.enums = true # enum predicates/bang methods as plain methods
|
|
14
13
|
# config.ar_minimizer.small_records = true # no Hashes for records that never fill them
|
|
15
14
|
# config.ar_minimizer.guards = true # also compile calls on receivers of unknown class, behind a class check
|
|
16
15
|
#
|
|
@@ -30,7 +29,6 @@ module ArMinimizer
|
|
|
30
29
|
manifest = -> { ArMinimizer::Manifest.parse(manifest_json) } if manifest_json
|
|
31
30
|
|
|
32
31
|
ActiveSupport.on_load(:active_record) do
|
|
33
|
-
ArMinimizer::Enums.install! if options.enums
|
|
34
32
|
ArMinimizer::SmallRecords.install! if options.small_records
|
|
35
33
|
ArMinimizer::Slim.enable!(keep_methods: keep_methods, strict: options.strict != false) if options.slim
|
|
36
34
|
ArMinimizer::Profile.start!(ENV["AR_MINIMIZER_PROFILE"]) if ENV["AR_MINIMIZER_PROFILE"]
|
|
@@ -44,6 +42,7 @@ module ArMinimizer
|
|
|
44
42
|
# File.fnmatch? without FNM_PATHNAME: `*` crosses directories.
|
|
45
43
|
ArMinimizer::Loader.install!(index: manifest, patterns: roots.map { "#{it}/*.rb" }, guards: !!options.guards,
|
|
46
44
|
version: ArMinimizer::Manifest.digest(manifest_json))
|
|
45
|
+
app.config.after_initialize { ArMinimizer::Loader.release_index! if app.config.eager_load }
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
# Every model is loaded after eager loading: say so if the manifest no
|
data/lib/ar_minimizer/version.rb
CHANGED
data/lib/ar_minimizer.rb
CHANGED
|
@@ -25,7 +25,6 @@ module ArMinimizer
|
|
|
25
25
|
autoload :Views, "ar_minimizer/views"
|
|
26
26
|
autoload :Profile, "ar_minimizer/profile"
|
|
27
27
|
autoload :Manifest, "ar_minimizer/manifest"
|
|
28
|
-
autoload :Enums, "ar_minimizer/enums"
|
|
29
28
|
autoload :Census, "ar_minimizer/census"
|
|
30
29
|
autoload :SmallRecords, "ar_minimizer/small_records"
|
|
31
30
|
autoload :Loader, "ar_minimizer/loader"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-minimizer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sampo Kuokkanen
|
|
@@ -59,8 +59,7 @@ dependencies:
|
|
|
59
59
|
version: '0.4'
|
|
60
60
|
description: Generates only the reader and writer per Active Record column (strict
|
|
61
61
|
mode removes the other 19 attribute methods), compiles call sites to the calls those
|
|
62
|
-
methods stood for
|
|
63
|
-
Hashes they never fill.
|
|
62
|
+
methods stood for and stops records allocating Hashes they never fill.
|
|
64
63
|
executables: []
|
|
65
64
|
extensions: []
|
|
66
65
|
extra_rdoc_files: []
|
|
@@ -72,7 +71,6 @@ files:
|
|
|
72
71
|
- lib/ar_minimizer.rb
|
|
73
72
|
- lib/ar_minimizer/build.rb
|
|
74
73
|
- lib/ar_minimizer/census.rb
|
|
75
|
-
- lib/ar_minimizer/enums.rb
|
|
76
74
|
- lib/ar_minimizer/index.rb
|
|
77
75
|
- lib/ar_minimizer/loader.rb
|
|
78
76
|
- lib/ar_minimizer/manifest.rb
|
data/lib/ar_minimizer/enums.rb
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ArMinimizer
|
|
4
|
-
# Enum predicates and bang methods as ordinary methods.
|
|
5
|
-
#
|
|
6
|
-
# Rails defines them with closures, and each predicate call builds and
|
|
7
|
-
# interns the `<name>_for_database` symbol before calling it:
|
|
8
|
-
#
|
|
9
|
-
# # def active?() status_for_database == 0 end (Rails' own comment)
|
|
10
|
-
# define_method("#{value_method_name}?") { public_send(:"#{name}_for_database") == value }
|
|
11
|
-
#
|
|
12
|
-
# Here the enum's literal data goes into the method source instead, as
|
|
13
|
-
# Roundhouse does for macros with literal arguments:
|
|
14
|
-
#
|
|
15
|
-
# def active? = public_send(:status_for_database) == 0
|
|
16
|
-
# def active! = update!("status" => 0)
|
|
17
|
-
#
|
|
18
|
-
# Rails still runs first (conflict detection, scopes); only the two methods
|
|
19
|
-
# are replaced. Rails allows booleans, integers, floats, symbols and strings
|
|
20
|
-
# as values; non-finite floats, and method names that aren't plain
|
|
21
|
-
# identifiers, keep Rails' closures.
|
|
22
|
-
module Enums
|
|
23
|
-
IDENTIFIER = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/
|
|
24
|
-
|
|
25
|
-
class << self
|
|
26
|
-
# Before models load (their class bodies declare the enums).
|
|
27
|
-
def install!
|
|
28
|
-
@enabled = true
|
|
29
|
-
mod = ActiveRecord::Enum.const_get(:EnumMethods)
|
|
30
|
-
mod.prepend(Definer) unless mod < Definer
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def disable! = @enabled = false
|
|
34
|
-
|
|
35
|
-
def enabled? = @enabled == true
|
|
36
|
-
|
|
37
|
-
def literal(value)
|
|
38
|
-
case value
|
|
39
|
-
when Integer, String, Symbol, true, false, nil then value.inspect
|
|
40
|
-
when Float then value.inspect if value.finite?
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
module Definer
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def define_enum_methods(name, value_method_name, value, scopes, instance_methods)
|
|
49
|
-
super
|
|
50
|
-
return unless instance_methods && Enums.enabled?
|
|
51
|
-
return unless value_method_name.match?(IDENTIFIER) && (literal = Enums.literal(value))
|
|
52
|
-
|
|
53
|
-
remove_method(:"#{value_method_name}?", :"#{value_method_name}!")
|
|
54
|
-
module_eval(<<~RUBY, "enum #{klass.name}.#{name}", 1)
|
|
55
|
-
def #{value_method_name}? = public_send(#{:"#{name}_for_database".inspect}) == #{literal}
|
|
56
|
-
def #{value_method_name}! = update!(#{name.inspect} => #{literal})
|
|
57
|
-
RUBY
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|