Skip to content

don't call stale detection on each call and add flag to disable it#139

Open
kubosuke wants to merge 1 commit into
curiosum-dev:mainfrom
kubosuke:fix/performance-stale-detector
Open

don't call stale detection on each call and add flag to disable it#139
kubosuke wants to merge 1 commit into
curiosum-dev:mainfrom
kubosuke:fix/performance-stale-detector

Conversation

@kubosuke

@kubosuke kubosuke commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Description

in short, performance fix around stale detector

Fixes severe performance on the translations list page (/kanta/locales/:id/translations) when many messages exist (~22k+). The page was replying in ~45 seconds because full stale detection (load all messages, PO parsing, partitioning, fuzzy Jaro matching) ran synchronously on every mount. This PR uses the cached stale-detection result from MessagesExtractorAgent on mount instead of calling StaleDetection.call() each time, and adds a config flag to disable stale detection entirely (no work at boot or on translations/dashboard, 0 counts in UI).

example:

[info] GET /kanta/locales/3/translations
[debug] Processing with KantaWeb.Translations.TranslationsLive.__live__/0
  Parameters: %{"locale_id" => "3"}
  Pipelines: [:browser, :domain_basic_auth, :custom_basic_auth]
[debug] QUERY OK db=0.2ms queue=0.2ms idle=1045.2ms
SELECT data, expires_at FROM sessions ...
...
[debug] QUERY OK source="kanta_messages" db=12.4ms decode=0.9ms idle=72.1ms
SELECT k0."id", k0."msgid", ... FROM "kanta_messages" AS k0 []
↳ Kanta.PoFiles.Services.StaleDetection.call/1, at: lib/kanta/po_files/services/stale_detection.ex:97
...
[debug] Replied in 45059ms 👈 45sec to load /locales/foo/translations page!
[debug] HANDLE PARAMS in KantaWeb.Translations.TranslationsLive
...
[debug] Replied in 17ms

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Code refactoring
  • Test improvements
  • CI/CD improvements

Related Issues

Fixes #
Closes #
Related to #

Changes Made

  • TranslationsLive.mount: Use MessagesExtractorAgent.get_stale_detection_result() (cached) instead of Kanta.PoFiles.Services.StaleDetection.call(). Handle nil (e.g. before agent has run) as empty stale_message_ids and fuzzy_matches.
  • TranslationsLive: Add stale_detection_assigns/0 helper; when Kanta.config().disable_stale_detection is true, assign empty MapSet and empty map and skip the agent.
  • TranslationsLive.handle_info(:refresh_messages): When stale detection is disabled, skip recomputing and keep current assigns; otherwise keep using get_stale_detection_result(true) for refresh-after-merge/delete.
  • Kanta.Config: New option disable_stale_detection: boolean() (default false) with validation.
  • MessagesExtractorAgent.init: When conf.disable_stale_detection is true (pattern-matched), skip MessagesExtractor and StaleDetection.call() and set stale_detection_result: nil. When get_stale_detection_result(true) is called and disabled, return current state without running StaleDetection.
  • DashboardLive: When disable_stale_detection is true, get_stale_messages_count and get_mergeable_messages_count return 0 without calling the agent; "delete stale" and "restore mergeable" handlers no-op.

Testing

Test Environment

  • Elixir version:
  • OTP version:
  • Phoenix version:
  • Database:
  • Gettext version:

Test Cases

  • All existing tests pass
  • New tests added for new functionality at appropriate levels
  • Manual testing performed

Test Commands Run

mix test
# All 99 tests pass

Documentation

  • Updated README.md (if applicable)
  • Updated documentation comments (with examples for new features)
  • Updated CHANGELOG.md (if applicable)

Code Quality

  • Code follows the existing style conventions
  • Self-review of the code has been performed
  • Code has been commented, particularly in hard-to-understand areas
  • No new linting warnings introduced
  • No new Dialyzer warnings introduced

Backward Compatibility

  • This change is backward compatible
  • This change includes breaking changes (please describe below)
  • Migration guide provided for breaking changes

Breaking Changes

None.

Performance Impact

  • Performance improvement

Performance Notes

Before this change, loading /kanta/locales/3/translations with ~22,868 translations showed Replied in 45059ms while DB queries were fast. Example logs:

[info] GET /kanta/locales/3/translations
[debug] Processing with KantaWeb.Translations.TranslationsLive.__live__/0
  Parameters: %{"locale_id" => "3"}
  Pipelines: [:browser, :domain_basic_auth, :custom_basic_auth]
[debug] QUERY OK db=0.2ms queue=0.2ms idle=1045.2ms
SELECT data, expires_at FROM sessions ...
...
[debug] QUERY OK source="kanta_messages" db=12.4ms decode=0.9ms idle=72.1ms
SELECT k0."id", k0."msgid", ... FROM "kanta_messages" AS k0 []
↳ Kanta.PoFiles.Services.StaleDetection.call/1, at: lib/kanta/po_files/services/stale_detection.ex:97
...
[debug] Replied in 45059ms
[debug] HANDLE PARAMS in KantaWeb.Translations.TranslationsLive
...
[debug] Replied in 17ms

The bottleneck was CPU work in StaleDetection (loading all messages, partitioning, fuzzy Jaro matching), not DB or cache. After the fix, the translations page uses the agent's cached result on mount, so response time should align with the ~17ms observed for handle_params after the first reply. With disable_stale_detection: true, no StaleDetection runs at boot or on translations/dashboard requests.

Translation Management Impact

  • Affects Kanta UI/dashboard

Translation Impact Notes

Translations list and dashboard use stale detection for "stale" badges and merge suggestions. With the flag enabled (default), behavior is unchanged except that the list page loads quickly. With disable_stale_detection: true, stale/mergeable counts show as 0 and related actions are no-ops.

Security Considerations

  • No security impact

Additional Notes

Host apps can disable stale detection via Kanta config, e.g. in config/config.exs:

config :my_app, Kanta,
  endpoint: MyAppWeb.Endpoint,
  repo: MyApp.Repo,
  otp_name: :my_app,
  disable_stale_detection: true

Ensure this config is passed to Kanta.start_link/1 (e.g. via Application.get_env(:my_app, Kanta, [])).

Screenshots/Examples

# Disable stale detection (no DB/PO/fuzzy work for stale detection)
config :nexus, Kanta,
  endpoint: NexusWeb.Endpoint,
  repo: Nexus.Repo.Local,
  otp_name: :nexus,
  disable_stale_detection: true

Checklist

  • I have read the Contributing Guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Reviewer Notes

  • TranslationsLive mount now uses cached stale result + optional disable flag; handle_info(:refresh_messages) still recomputes when stale detection is enabled.
  • MessagesExtractorAgent init uses pattern match init(conf: %Kanta.Config{disable_stale_detection: true}) for the disabled path.

@kubosuke kubosuke marked this pull request as ready for review March 5, 2026 12:08
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