Bug: Helix Ultimate reloads the template data 4 times for every overridden layout render (~150 wasted cache lookups per page) - Forum | JoomShaper
Staff replied Helix Framework Joomla 5 v2.2.10

Bug: Helix Ultimate reloads the template data 4 times for every overridden layout render (~150 wasted cache lookups per page)

Asked by HalTurner 1 day ago Last activity 1 day ago

Environment

  • Helix Ultimate framework 2.2.10 (plugin plg_system_helixultimate), template Gazette 2.0.5
  • Joomla 5.4.9, PHP 8.3 (PHP-FPM), MySQL 8.4
  • Joomla caching: conservative, cache handler memcached (local to each web server)
  • News site with a main menu of ~50 items and article lists on most pages

The problem

HTMLOverride::loadTemplate() (src/Platform/HTMLOverride.php) runs every time an overridden layout is rendered, for example once per menu item (the Gazette html/mod_menu/default_url.php calls it), and once per article for the intro image, info block and similar layouts. Each call:

  1. Calls parsePath() 4 times ($htmlPath, $overridePath, $tmplOverridePath and joomla.asset.json). The last result, $webAssetUri, is never used.
  2. Each parsePath() calls Helper::loadTemplateData() only to read $template->template (the template folder name, which never changes during a request).
  3. Helper::loadTemplateData() (src/Platform/Helper.php) has no per-request memoisation. Every call creates a new HelixCache (and with it a new Cache::getInstance() controller) and then:
    • checks the draft key with $cache->contains() before checking $requestFromIframe, so the cache is queried on every frontend request even though the result is only used in the editor iframe;
    • checks the init key with $cache->contains(), and on a hit calls loadData(), which reads the same entry a second time.
  4. It also calls \debug_backtrace() without DEBUG_BACKTRACE_IGNORE_ARGS or a limit, which copies the whole call stack with its arguments just to read [0]['file'].

On the frontend the init key is almost never present: it is only stored when an administrator opens the Helix settings editor (Settings.php), with a 24-hour lifetime, and only in the cache of the server that handled that request. So in practice each loadTemplateData() call is 2 cache misses followed by the (memoised) database read in getTemplateStyle(), and each overridden layout render is 8 cache misses.

Measured on our site (one production web server, normal daytime traffic, memcached watch fetchers for 20 seconds):

  • 2,946 cache reads during 15 page views
  • 2,344 of them (80%) were in the helixultimate cache group, and all 2,344 were misses (not_found)
  • That is ~156 Helix cache misses per page view; a separate 60-second sample gave ~166 cache reads per page view in total, 87% of them misses
  • The PHP-FPM slow log shows request stacks stuck in this path whenever the cache is slow, e.g. HelixCache->__call() → Helper::loadTemplateData() → HTMLOverride::parsePath() → HTMLOverride::loadTemplate() → templates/shaper_gazette/html/mod_menu/default_url.php (39 of the slow traces in our log go through HelixCache.php:145)

A raw memcached miss costs ~15 µs from PHP on our server, so the network part alone is ~2.5 ms per page, plus the PHP overhead of creating a cache controller, hashing the key and the full debug_backtrace() each time. The bigger cost is with a remote or file cache handler, where every one of these ~150 misses becomes a network round trip or a filesystem lookup.

Suggested fix

All backwards-compatible, no behaviour change:

  1. Memoise Helper::loadTemplateData() per request, e.g. a static $cache = [] keyed by $templateId plus whether this is an editor iframe request (the same pattern getTemplateStyle() already uses). This alone removes almost all of the lookups.
  2. In HTMLOverride::loadTemplate(), read the template name once and pass it to parsePath() (or memoise parsePath()), and drop the unused $webAssetUri line.
  3. Check $requestFromIframe before $cache->contains() in loadTemplateData(), so normal frontend requests never query the draft key.
  4. Use one read instead of contains() + loadData(): call loadData() and test for null.
  5. Use debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1) in loadTemplate(), since only the caller's file is needed.

Impact

Every Helix Ultimate site renders overridden menu items and article layouts on almost every page, so this affects every Helix site that uses Joomla caching, and the waste grows with the menu size and the number of articles listed per page. The fix is a few lines, mostly a static memo, with no change in output. I'm happy to test a build on our site and share the before/after cache counts.

1 reply

Anne Staff

Hi Halturner,

Thank you for taking the time to investigate this issue so thoroughly and for sharing your findings and suggested solutions with us. We really appreciate the detailed explanation, performance measurements, and the effort you’ve put into identifying potential improvements.

We’ll pass your findings along to our development team for further review. We also appreciate your willingness to test a build and compare the before-and-after results. Your insights and contribution are genuinely valuable to us!

Log in to reply.