Skip to content

Translation set new/edit page is O(spaces x constraints) and times out on large organizations #135

Description

@fblupi

Summary

TranslationSetsController#edit (and #new) renders one hidden <select> of components for every participatory space in the organization, for every constraint in the set. On an organization with ~530 participatory spaces this makes the page take 16-22s when it succeeds, and time out with a 500 the rest of the time.

Impact

Production APM data for a single translation set with 2 constraints:

Metric Value
Successful loads 16.0s, 17.0s, 18.1s, 20.1s, 21.6s
Failed loads (HTTP 500) 27.7s, 28.1s, 28.4s, 28.6s, 34.7s
Exception on failure Rack::Timeout::RequestTimeoutException (SIGTERM at 25000ms)
Time spent in view rendering 19.87s out of 20.07s total
Time spent in SQL 3.2s
Decidim::Component Load queries in one request 889
Object allocations in one request 4,814,003

The _constraint_fields partial was rendered 3 times (1 hidden <template> + 2 constraints), taking 6.5s, 5.9s and 7.1s respectively. The resulting HTML contains roughly 1,600 <select> elements.

The page is therefore unusable in practice: adding a third constraint to the set puts every single load past the timeout.

Root cause

app/views/decidim/term_customizer/admin/translation_sets/_constraint_fields.html.erb#L34-L55:

<% subject_manifests.each do |manifest| %>
  <% models = manifest.model_class_name.constantize.where(organization: current_organization) %>
  ...
    <% models.each do |model| %>
      ...
        <% if model.respond_to?(:components) %>
          <%= component_fields.select :component_id, model.components.map { |c| [translated_attribute(c.name), c.id] }, selected: selected_component, include_blank: true %>

Three separate problems:

  1. Every space gets a pre-rendered component select. Only the selected space's select is ever visible; constraint_fields.js shows/hides .component-container by data-components. The markup for the other ~529 spaces is dead weight in the response.
  2. model.components is an N+1, one query per space, per constraint block. Hence the 889 Decidim::Component Load queries in a single request.
  3. The space lists are unscoped, so past, unpublished and archived spaces are all loaded and rendered.

The stack trace of the timed-out requests ends exactly at line 49 of that partial.

Secondarily, TranslationSetsController#subject_manifests (app/controllers/decidim/term_customizer/admin/translation_sets_controller.rb#L119-L126) loads every space of every manifest and maps all their translated titles, only to discard the array and test count.positive?:

models = manifest.model_class_name.constantize.where(organization: current_organization).map { |p| [translated_attribute(p.title), p.id] }
next unless models.count.positive?

An exists? would do the same job.

Steps to reproduce

  1. Have an organization with several hundred participatory spaces.
  2. Create a translation set with 2 or more constraints.
  3. Open /admin/term_customizer/sets/:id/edit.

Response time scales as (number of constraints + 1) × (number of spaces in the organization), so it degrades every time a new space or initiative is created.

Suggested fix

Load the component options on demand instead of pre-rendering them:

  1. Render only the space selects, and fetch the components of the chosen space over AJAX (a small admin endpoint returning the components for a given participatory space). This makes the page O(1) in the number of spaces.
  2. If a full rewrite is too much for now, an interim mitigation would be to hoist models out of the per-constraint loop and replace the per-space model.components with a single grouped query. That removes the ~889 queries, but roughly 1,600 select elements still have to be built, so it is a partial mitigation only.
  3. Scope the space lists (published, not archived) and replace the subject_manifests full load with exists?.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions