Skip to content

[TEST] attribute-based extraction — CI matrix run (do not merge)#1

Closed
oliver-kriska wants to merge 2 commits into
mainfrom
attribute-based-extraction
Closed

[TEST] attribute-based extraction — CI matrix run (do not merge)#1
oliver-kriska wants to merge 2 commits into
mainfrom
attribute-based-extraction

Conversation

@oliver-kriska

@oliver-kriska oliver-kriska commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Fork-internal draft PR. Not for merging into fork main (fork main tracks upstream). Two purposes:

  1. Trigger the CI matrix (incl. the Elixir 1.16 / OTP 24 floor) on the attribute-based-extraction branch — green on both legs (1.16/OTP 24.2 and 1.18/OTP 27.2).
  2. Document the real-world validation results below ahead of the upstream PR.

Status: in active production-app trial

This branch is currently pinned (immutable ref) as the gettext dependency of our larger commercial Phoenix app, where it is being used day-to-day for all extraction work — local gettext.extract --merge, the LLM translation pipeline, and the per-PR CI up-to-date check — ahead of the upstream submission.

Real-world benchmarks (the trial app)

Testbed: 1,875 source files; 4,713 msgids across 11 gettext domains × 16 locales (176 PO files), incl. macro-generated gettext calls and HEEx templates. Elixir 1.20.1 / OTP 29.0.2, Apple Silicon. Branch @ 2e8808e.

measurement force-recompile path --from-attributes
mix gettext.extract --merge 70.3s 5.7–5.9s (12×)
mix gettext.extract --check-up-to-date (local) ~60s 3.2s
same check on CI (4-core runner, the per-PR job) 94s 5s (~19×)
LLM translation pipeline (2× extract+merge per run) 2m49s 41–53s (3.4×)
full project compile with attribute persistence on 129.8s user CPU (off) 129.6s user CPU → no measurable overhead
dev beam size delta +2.7% (+2.3 MiB); prod beams clean (env gate)

Correctness: equivalence under mutation

Beyond steady-state runs (which stay byte-clean and idempotent), six mutation classes were each applied to source and run through both paths (new path first, so its incremental-compile detection is what finds the change), with the resulting PO/POT diffs compared byte-for-byte:

mutation result
new gettext() call (.ex) byte-identical
reworded existing msgid byte-identical
deleted gettext call byte-identical
macro-generated message change (gettext injected into modules that never literally mention it) byte-identical
string change in .heex template byte-identical
hand-edited msgstr preserved through extract+merge byte-identical

The batched mutations produced 1,322 changed lines across the 176-PO catalog — identical from both paths (new path: 9.1s incl. incremental compile of the changed files; old path: 65.3s).

Also caught only by real-app testing: Extractor.extracting?() returns nil (not false) when the agent isn't running, which crashed a dependency compile under strict boolean ops in the first iteration — fixed on this branch.


Note: the sibling branch parallel-locale-merge was merged upstream as elixir-gettext#436. This branch predates that merge; it will be rebased onto upstream main before the upstream PR is opened.

mix gettext.extract can only discover messages by force-recompiling the
whole project, because extraction happens during macro expansion and the
results only exist in the extractor agent for the duration of that
compilation. On large projects this dominates extraction time.

Implement the design proposed by @maennchen in elixir-gettext#373: during normal
compilation, when the current Mix environment is included in the new
:extraction_environments gettext configuration (default [:dev]), the
gettext macros persist each message into an accumulated, persisted
@__gettext_messages__ attribute of the calling module, and backends
persist a @__gettext_backend_module__ marker. A new experimental
--from-attributes flag on mix gettext.extract then runs a normal
incremental compile, reads the persisted entries back from the compiled
BEAM files with :beam_lib (without loading modules), feeds them into the
existing extractor agent, and reuses the unchanged
Gettext.Extractor.pot_files/2 merge logic.

Staleness needs no bookkeeping: changed files are recompiled by the
incremental compile and get fresh attributes, and deleted files' beams
are pruned by the compiler. --check-up-to-date composes with the new
flag and gets the same speedup. Environments not listed in
:extraction_environments (such as :prod) persist nothing, so release
artifacts are unaffected. If the scan finds no persisted messages or
backends at all, the task raises with instructions instead of silently
producing empty output.

The force-recompile path is untouched and remains the default. Old-path
and new-path POT output is byte-identical, covered by tests including
macro-generated messages (gettext calls injected into modules that do
not literally mention them) and plural/context/comment/noop variants.
@stage-review

stage-review Bot commented Jun 10, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 4 individual chapters for you:

Title
1 Define attribute-based extraction logic in Extractor
2 Instrument macros and compiler to persist attributes
3 Add --from-attributes flag to extraction task
4 Verify attribute extraction with tests and changelog
Open in Stage

Chapters generated by Stage for commit d538cb4 on Jun 10, 2026 1:49pm UTC.

Address review findings on the --from-attributes prototype:

  * Validate the shape of persisted message entries when scanning BEAM
    files, skipping malformed ones instead of crashing the scan with a
    CaseClauseError (an unrelated module could carry an attribute with
    the same name, and future format changes need a safe path).

  * Reenable "compile" and friends in the incremental compile, so that
    extraction still picks up source changes when the compile tasks
    already ran in the current VM (for example, through a task alias).

  * Guard the backend marker registration with Module.has_attribute?/2,
    matching persist_message/6.

Test improvements: assert fixed content on the rebuilt POT (not only
equality with the recompilation path), reset the extractor agent's full
initial state between tests, make reference line assertions robust to
fixture edits, pin the stale-POT filename in the --check-up-to-date
assertion, and restore Code.compiler_options/1 after the suite.

New coverage: _with_backend macro variants, reference merging when two
modules share a msgid, --from-attributes combined with --merge, custom
:priv backends, and extraction across umbrella apps (each app gets its
own POT through the task's recursion).
@oliver-kriska

Copy link
Copy Markdown
Owner Author

Superseded by the upstream PR elixir-gettext#437, whose CI runs the full matrix including the Elixir 1.16/OTP 24 floor. No longer needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant