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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd5b6b67995d6c1cbf665637d1c06d7bba3a481906a2dfab3478c1f7e9e303b0
4
- data.tar.gz: f9a99422b743b0942a7e07d8de0fb43273f4ec2fdc9e9f1e34c0021e196a6aee
3
+ metadata.gz: b96e595f334bcacd836276d8ff508f885079a41d045ef4ffdcc743689ae6469b
4
+ data.tar.gz: 7ecfa22211db6dc240d50dbc20cdcf99f6e133e8e4ba416e54a477c2b3a6a817
5
5
  SHA512:
6
- metadata.gz: 1bc117e578665279e838e5ce2ea05d915c271a435224b21824e4ce51af8ac01a53248c2c7c3532f9fca3c46c2927927ff81d5b65a1db5dc93aa4dd267836fbb2
7
- data.tar.gz: 8895e58f378594e9b92df76e98c72d6d377f6b6c2e1f9a77441677efc1ecdef23a15d2640cac6584940c0d7c5128ac8f563d225bbd471d5d1e5f8d58aa6652c2
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, and compiles
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
- ## Compiling call sites
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**, `config/ar_minimizer.json`, written by
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
- ## More
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`).
@@ -1,15 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ArMinimizer
4
- # What the attribute-method and enum compilers cover in an app.
4
+ # What the attribute-method compilers cover in an app.
5
5
  module Census
6
- Result = Data.define(:models, :columns, :stock_methods, :generated_methods, :enum_methods, :enum_compiled,
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, enum_methods, enum_compiled,
42
- sites, files, manifest_state(root, index))
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)
@@ -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))
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ArMinimizer
4
- VERSION = "0.1.3"
4
+ VERSION = "0.3.0"
5
5
  end
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.1.3
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, turns enum closures into plain methods and stops records allocating
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
@@ -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