The Taxonomy Vocabulary Performance Issue I Fell Into: loadTree().

2020-09-07

Taxonomy term management in Drupal 8 has an Achilles heel: \Drupal\taxonomy\TermStorageInterface::loadTree(). Taxonomies that grow too large can WSOD a site with a fatal Out of Memory Error (OOME) when this method is called.

I first ran into it when I attempted to ‘List Terms’ for a taxonomy with only 12k terms in it although my other vocabularies worked fine. I didn’t realize at the time it was due to size. My logs indicated it was an OOME but repeated instances gave different sources for the error; but they were all red herrings.

I began to search the web for answers. One post I found indicated you could change the number of terms listed at a time. Surely that would fix the error; right? Unfortunately, no. drush config:set taxonomy.settings terms_per_page_admin 25 didn’t help although I verified the command did change the number of terms displayed per page for other vocabularies. Dropping it down to five terms per page didn’t help either!

Something else was going on so I continued my hunt. I finally stumbled upon the root of the problem, loadTree(). This function loads the entire vocabulary into memory and, because it is called by the term list admin form (\Drupal\taxonomy\Form\OverviewTerms::buildForm()), it can WSOD your page even if try to limit the page to a single term. I found one issue ticket indicating it can happen with as few as 6k terms in the vocabulary! 😱

There are workarounds, but this is a pretty ugly hole that Drupal has simply laid a rug over and unwitting site developers and administrators keep falling into.

The first workaround is the classic OOME brute-force fix: increase your PHP’s max memory. PHP’s max memory limit serves as a not-so-subtle that developers have written inefficient code. If you have to keep increasing this limit you are likely doing something wrong although there are exceptions. Most of the time my site is quite happy living within the 256MB limit and many sites run on much less. Unfortunately, loadTree() will still consume all that memory and complain. Giving it more memory can work, to a point. Eventually, memory won’t be enough; the time it takes to load all that memory will eventually lead to time-out issues. This is a losing battle as the vocabulary gets larger.

The next workaround is to turn off this feature altogether by setting taxonomy.settings:override_selector to false and using a view that doesn’t use loadTree() at all but displays a flat list without hierarchy. This setting is here because Drupal knows they have a scalability problem:

\Drupal\taxonomy\TermStorageInterface::loadTree() and \Drupal\taxonomy\TermStorageInterface::loadParents() may contain large numbers of items so we check for taxonomy.settings:override_selector before loading the full vocabulary. Contrib modules can then intercept before hook_form_alter to provide scalable alternatives.

Really, Drupal? Kicking performance issues on a core feature like taxonomy to contrib??

In any case, yes, you can turn off this setting and create a term form (with pagination!) that also includes an edit button. I’ve included one below based on an Islandora 8.x-1.1.0.

Fortunately, there is a better solution that preserves the existing interface! 🎉 Unfortunately, it is only available as a patch. ☹️

In the aptly named ticket “Do not use \Drupal\taxonomy\TermStorageInterface::loadTree() in \Drupal\taxonomy\Form\OverviewTerms::buildForm()”, legolasbo submitted a patch in May 2018 which was picked up by others but then never merged. There have been a few re-rolls since and the July 8th, 2019 patch works perfectly with Drupal 8.9.5.

Applying patches is relatively easy using composer.

Update your composer.json to include the following in your ‘extra’ section:

"patches": {
         "drupal/core": {
                 "Fix large taxonomy admin scaling issue": "https://www.drupal.org/files/issues/2019-07-08/763380-61.patch"
         }
 },

Then run composer update and clear cache.

I tested this with the Islandora 8.x-1.1.0 release. After the initial provision, I updated all my module with composer update and then generated 10k terms in the subject vocabulary. Trying to view that vocabulary resulted in a WSOD (as we would now expect). Applying the patch allowed the subjects vocabulary page to render as it should!

The trouble with patches is they keep needing re-rolls as core is updated. The sooner we can get this patch in core the better, as far as I’m concerned.

Appendix: Manage Taxonomy Terms View

Config file: views.view.manage_taxonomies.yml:

langcode: en
status: true
dependencies:
  config:
    - taxonomy.vocabulary.corporate_body
    - taxonomy.vocabulary.family
    - taxonomy.vocabulary.genre
    - taxonomy.vocabulary.geo_location
    - taxonomy.vocabulary.islandora_access
    - taxonomy.vocabulary.islandora_display
    - taxonomy.vocabulary.islandora_media_use
    - taxonomy.vocabulary.islandora_models
    - taxonomy.vocabulary.language
    - taxonomy.vocabulary.person
    - taxonomy.vocabulary.physical_form
    - taxonomy.vocabulary.resource_types
    - taxonomy.vocabulary.subject
    - taxonomy.vocabulary.tags
    - taxonomy.vocabulary.temporal
  module:
    - taxonomy
    - user
id: manage_taxonomies
label: 'Manage taxonomies'
module: views
description: ''
tag: ''
base_table: taxonomy_term_field_data
base_field: tid
display:
  default:
    display_plugin: default
    id: default
    display_title: Master
    position: 0
    display_options:
      access:
        type: perm
        options:
          perm: 'administer taxonomy'
      cache:
        type: tag
        options: {  }
      query:
        type: views_query
        options:
          disable_sql_rewrite: false
          distinct: false
          replica: false
          query_comment: ''
          query_tags: {  }
      exposed_form:
        type: basic
        options:
          submit_button: Apply
          reset_button: false
          reset_button_label: Reset
          exposed_sorts_label: 'Sort by'
          expose_sort_order: true
          sort_asc_label: Asc
          sort_desc_label: Desc
      pager:
        type: full
        options:
          items_per_page: 10
          offset: 0
          id: 0
          total_pages: null
          tags:
            previous: ‹‹
            next: ››
            first: '« First'
            last: 'Last »'
          expose:
            items_per_page: true
            items_per_page_label: 'Items per page'
            items_per_page_options: '5, 10, 25, 50, 100, 200'
            items_per_page_options_all: false
            items_per_page_options_all_label: '- All -'
            offset: false
            offset_label: Offset
          quantity: 9
      style:
        type: table
        options:
          grouping: {  }
          row_class: ''
          default_row_class: true
          override: true
          sticky: false
          caption: ''
          summary: ''
          description: ''
          columns:
            name: name
            edit_taxonomy_term: edit_taxonomy_term
            status: status
            vid: vid
          info:
            name:
              sortable: true
              default_sort_order: asc
              align: ''
              separator: ''
              empty_column: false
              responsive: ''
            edit_taxonomy_term:
              sortable: true
              default_sort_order: asc
              align: ''
              separator: ''
              empty_column: false
              responsive: ''
            status:
              sortable: true
              default_sort_order: asc
              align: ''
              separator: ''
              empty_column: false
              responsive: ''
            vid:
              sortable: true
              default_sort_order: asc
              align: ''
              separator: ''
              empty_column: false
              responsive: ''
          default: '-1'
          empty_table: false
      row:
        type: fields
      fields:
        name:
          id: name
          table: taxonomy_term_field_data
          field: name
          relationship: none
          group_type: group
          admin_label: ''
          label: Name
          exclude: false
          alter:
            alter_text: false
            text: ''
            make_link: false
            path: ''
            absolute: false
            external: false
            replace_spaces: false
            path_case: none
            trim_whitespace: false
            alt: ''
            rel: ''
            link_class: ''
            prefix: ''
            suffix: ''
            target: ''
            nl2br: false
            max_length: 0
            word_boundary: false
            ellipsis: false
            more_link: false
            more_link_text: ''
            more_link_path: ''
            strip_tags: false
            trim: false
            preserve_tags: ''
            html: false
          element_type: ''
          element_class: ''
          element_label_type: ''
          element_label_class: ''
          element_label_colon: true
          element_wrapper_type: ''
          element_wrapper_class: ''
          element_default_classes: true
          empty: ''
          hide_empty: false
          empty_zero: false
          hide_alter_empty: true
          click_sort_column: value
          type: string
          settings:
            link_to_entity: true
          group_column: value
          group_columns: {  }
          group_rows: true
          delta_limit: 0
          delta_offset: 0
          delta_reversed: false
          delta_first_last: false
          multi_type: separator
          separator: ', '
          field_api_classes: false
          convert_spaces: false
          entity_type: taxonomy_term
          entity_field: name
          plugin_id: term_name
        vid:
          id: vid
          table: taxonomy_term_field_data
          field: vid
          relationship: none
          group_type: group
          admin_label: ''
          label: Vocabulary
          exclude: false
          alter:
            alter_text: false
            text: ''
            make_link: false
            path: ''
            absolute: false
            external: false
            replace_spaces: false
            path_case: none
            trim_whitespace: false
            alt: ''
            rel: ''
            link_class: ''
            prefix: ''
            suffix: ''
            target: ''
            nl2br: false
            max_length: 0
            word_boundary: true
            ellipsis: true
            more_link: false
            more_link_text: ''
            more_link_path: ''
            strip_tags: false
            trim: false
            preserve_tags: ''
            html: false
          element_type: ''
          element_class: ''
          element_label_type: ''
          element_label_class: ''
          element_label_colon: false
          element_wrapper_type: ''
          element_wrapper_class: ''
          element_default_classes: true
          empty: ''
          hide_empty: false
          empty_zero: false
          hide_alter_empty: true
          click_sort_column: target_id
          type: entity_reference_label
          settings:
            link: true
          group_column: target_id
          group_columns: {  }
          group_rows: true
          delta_limit: 0
          delta_offset: 0
          delta_reversed: false
          delta_first_last: false
          multi_type: separator
          separator: ', '
          field_api_classes: false
          entity_type: taxonomy_term
          entity_field: vid
          plugin_id: field
        status:
          id: status
          table: taxonomy_term_field_data
          field: status
          relationship: none
          group_type: group
          admin_label: ''
          label: 'Published?'
          exclude: false
          alter:
            alter_text: false
            text: ''
            make_link: false
            path: ''
            absolute: false
            external: false
            replace_spaces: false
            path_case: none
            trim_whitespace: false
            alt: ''
            rel: ''
            link_class: ''
            prefix: ''
            suffix: ''
            target: ''
            nl2br: false
            max_length: 0
            word_boundary: true
            ellipsis: true
            more_link: false
            more_link_text: ''
            more_link_path: ''
            strip_tags: false
            trim: false
            preserve_tags: ''
            html: false
          element_type: ''
          element_class: ''
          element_label_type: ''
          element_label_class: ''
          element_label_colon: false
          element_wrapper_type: ''
          element_wrapper_class: ''
          element_default_classes: true
          empty: ''
          hide_empty: false
          empty_zero: false
          hide_alter_empty: true
          click_sort_column: value
          type: boolean
          settings:
            format: unicode-yes-no
            format_custom_true: ''
            format_custom_false: ''
          group_column: value
          group_columns: {  }
          group_rows: true
          delta_limit: 0
          delta_offset: 0
          delta_reversed: false
          delta_first_last: false
          multi_type: separator
          separator: ', '
          field_api_classes: false
          entity_type: taxonomy_term
          entity_field: status
          plugin_id: field
        edit_taxonomy_term:
          id: edit_taxonomy_term
          table: taxonomy_term_data
          field: edit_taxonomy_term
          relationship: none
          group_type: group
          admin_label: ''
          label: ''
          exclude: false
          alter:
            alter_text: false
            text: ''
            make_link: false
            path: ''
            absolute: false
            external: false
            replace_spaces: false
            path_case: none
            trim_whitespace: false
            alt: ''
            rel: ''
            link_class: ''
            prefix: ''
            suffix: ''
            target: ''
            nl2br: false
            max_length: 0
            word_boundary: true
            ellipsis: true
            more_link: false
            more_link_text: ''
            more_link_path: ''
            strip_tags: false
            trim: false
            preserve_tags: ''
            html: false
          element_type: ''
          element_class: ''
          element_label_type: ''
          element_label_class: ''
          element_label_colon: false
          element_wrapper_type: ''
          element_wrapper_class: ''
          element_default_classes: true
          empty: ''
          hide_empty: false
          empty_zero: false
          hide_alter_empty: true
          text: Edit
          output_url_as_text: false
          absolute: false
          entity_type: taxonomy_term
          plugin_id: entity_link_edit
      filters:
        name:
          id: name
          table: taxonomy_term_field_data
          field: name
          relationship: none
          group_type: group
          admin_label: ''
          operator: '='
          value: ''
          group: 1
          exposed: true
          expose:
            operator_id: name_op
            label: Name
            description: ''
            use_operator: true
            operator: name_op
            operator_limit_selection: false
            operator_list: {  }
            identifier: name
            required: false
            remember: false
            multiple: false
            remember_roles:
              authenticated: authenticated
              anonymous: '0'
              administrator: '0'
              fedoraadmin: '0'
            placeholder: ''
          is_grouped: false
          group_info:
            label: ''
            description: ''
            identifier: ''
            optional: true
            widget: select
            multiple: false
            remember: false
            default_group: All
            default_group_multiple: {  }
            group_items: {  }
          entity_type: taxonomy_term
          entity_field: name
          plugin_id: string
        vid:
          id: vid
          table: taxonomy_term_field_data
          field: vid
          relationship: none
          group_type: group
          admin_label: ''
          operator: in
          value:
            all: all
            corporate_body: corporate_body
            family: family
            genre: genre
            geo_location: geo_location
            islandora_access: islandora_access
            islandora_display: islandora_display
            islandora_media_use: islandora_media_use
            islandora_models: islandora_models
            language: language
            person: person
            physical_form: physical_form
            resource_types: resource_types
            subject: subject
            tags: tags
            temporal: temporal
          group: 1
          exposed: true
          expose:
            operator_id: vid_op
            label: Vocabulary
            description: ''
            use_operator: false
            operator: vid_op
            operator_limit_selection: false
            operator_list: {  }
            identifier: vid
            required: false
            remember: false
            multiple: false
            remember_roles:
              authenticated: authenticated
              anonymous: '0'
              administrator: '0'
              fedoraadmin: '0'
            reduce: false
          is_grouped: false
          group_info:
            label: ''
            description: ''
            identifier: ''
            optional: true
            widget: select
            multiple: false
            remember: false
            default_group: All
            default_group_multiple: {  }
            group_items: {  }
          entity_type: taxonomy_term
          entity_field: vid
          plugin_id: bundle
        status:
          id: status
          table: taxonomy_term_field_data
          field: status
          relationship: none
          group_type: group
          admin_label: ''
          operator: '='
          value: All
          group: 1
          exposed: true
          expose:
            operator_id: ''
            label: Published
            description: ''
            use_operator: false
            operator: status_op
            operator_limit_selection: false
            operator_list: {  }
            identifier: status
            required: false
            remember: false
            multiple: false
            remember_roles:
              authenticated: authenticated
              anonymous: '0'
              administrator: '0'
              fedoraadmin: '0'
          is_grouped: false
          group_info:
            label: ''
            description: ''
            identifier: ''
            optional: true
            widget: select
            multiple: false
            remember: false
            default_group: All
            default_group_multiple: {  }
            group_items: {  }
          entity_type: taxonomy_term
          entity_field: status
          plugin_id: boolean
      sorts: {  }
      title: 'Manage taxonomies'
      header: {  }
      footer: {  }
      empty: {  }
      relationships: {  }
      arguments: {  }
      display_extenders: {  }
      filter_groups:
        operator: AND
        groups:
          1: AND
    cache_metadata:
      max-age: -1
      contexts:
        - 'languages:language_content'
        - 'languages:language_interface'
        - url
        - url.query_args
        - user.permissions
      tags: {  }
  page_1:
    display_plugin: page
    id: page_1
    display_title: Page
    position: 1
    display_options:
      display_extenders: {  }
      path: manage-taxonomies
    cache_metadata:
      max-age: -1
      contexts:
        - 'languages:language_content'
        - 'languages:language_interface'
        - url
        - url.query_args
        - user.permissions
      tags: {  }