Skip to content

Store font usage instead of walking every form and site #2018

Closed

Description

@gravitypdf-bot

The Font Manager's "what's using this font" lookup (GET /fonts/?scope=usage) reads every active form on every request, and for a Multisite super admin, every form on every site. #2015 makes that scan about 2.5x faster and stops subsite admins walking the network, but the cost still grows with the number of forms and sites. Storing the result would remove the walk from the normal path.

Measurements

1,411 forms, 35 MB of display_meta, 602 PDFs, MariaDB 12.3 in Docker, single site. Multisite figures are projected from timing get_site_font_keys() on that site, not from a real network.

Time
Lookup before #2015 281 ms
Lookup with the byte-wise LIKE in #2015 about 105 ms
Each other site of this size, before #2015 about 270 ms
Each other site without Gravity Forms 0.2 ms
Decoding a stored usage map 0.3 ms
One query reading a key list per site, 10,000 simulated sites, plus totalling under 10 ms

Proposal

Per site: keep the font-to-PDF map that Model_Required_Fonts::get_font_pdfs() returns in a non-autoloaded option.

  • Clear it on gform_post_update_form_meta (display_meta only), gform_post_form_activated, gform_post_form_deactivated, gform_post_form_trashed, gform_post_form_restored, gform_post_form_duplicated and gform_after_delete_form.
  • Rebuild it the next time something needs it.
  • Add a daily expiry as a backstop for direct database edits. gf_form.date_updated can't serve as a fingerprint, because update_form_meta() doesn't touch it.

Network: each site writes its font keys (default font, locale fonts, PDF fonts) to its own sitemeta row, e.g. gfpdf_site_fonts_{blog_id}.

  • One query on meta_key reads every site, with no per-site tables and no switch_to_blog().
  • One row per site means two sites saving at once can't overwrite each other.
  • Also refresh a site's row when gfpdf_settings or WPLANG changes.

Response size: the PDF list per font has no cap, so GET /fonts/?scope=usage grows with every PDF: 111 KB for 602 PDFs, about 185 bytes each, so around 1.8 MB before compression at 10,000 PDFs (projected).

  • Once usage is stored, that endpoint can return only reasons and pdfs_label for each font, and GET /fonts/{id}?scope=usage on the existing route can return pdfs when a font is opened.
  • Don't truncate the list. A font can't be deleted until every PDF using it has changed, so each one needs to stay reachable.
  • This depends on the stored map. Without it, each font click would walk the forms again.

Open points

  • Backfill: existing sites have no row until something writes one. Fill them in through the existing sync cron (Model_Font_Downloader::SYNC_EVENT), a bounded number of sites per run.
  • Sites without a row: count their core fonts as in use, so Delete stays locked until the backfill finishes. The Font Manager may need to say the count is still being worked out.
  • Accuracy of the delete check: delete_downloaded_font() and the custom font delete should still check the current site live. A stale "unused" from another site is low risk for core fonts, as a deleted font downloads again when a PDF needs it, but with downloads turned off it would stay missing.
  • Clean-up: remove a site's row when the site is deleted, and add the rows and the option to Model_Uninstall.
  • Form title changes: the stored map includes form titles, so confirm every path that renames a form clears it.

7.0

7.0 adds its own font tables, so a usage table keyed by font may be a better home there. This issue is about what 6.x can do without a new table.

Activity

  1. added 3 commits that reference this issue on Nov 10, 2026
    7ea4d77
    9e7e7b5
    062a7fe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions