Kirby3 Boost - makes loading a lot of pages in one request fast + unique ID

I am creating posts for my plugins to make it easier to find them using the forum search and not just the docs search.

Boost the speed of Kirby by having content files of pages cached, with automatic unique ID, fast lookup and Tiny-URL.

  • up to 3x faster content loading
  • fastest page lookup and resolution of relations

Usecase

If you have to process within a single request a lot of page objects (1000+) or if you have a lot of relations between page objects to resolve then consider using this plugin. With less page objects you will propably not gain enough to justify the overhead.

How does this plugin work?

  • It caches all content files and keeps the cache up to date when you add or modify content. This cache will be used when constructing page objects making everything that involves page objects faster (even the Panel).
  • It provides a benchmark to help you decide which cachedriver to use.
  • It can add an unique ID for page objects that can be used create relations that do not break even if the slug or directory of a page object changes.
  • It provides a very fast lookup for page objects via id, diruri or the unique id.
  • It provides you with a tiny-url for page objects that have an unique id.

site/blueprints/pages/default.yml

preset: page

fields:
  # visible field
  boostid:
    type: boostid
  
  # hidden field
  #boostid:
  #  extends: fields/boostid

  one_relation:
    extends: fields/boostidpage

  many_related:
    extends: fields/boostidpages

site/models/default.php

class DefaultPage extends \Kirby\Cms\Page
{
    use \Bnomei\PageHasBoost;
}

// or

class DefaultPage extends \Bnomei\BoostPage
{
    
}

PHP

// page lookup
$page = page($somePageId); // slower
$page = boost($somePageId); // faster
$page = boost($somePageDirUri); // fastest
$page = boost($boostId); // will use fastest internally

// resolving one relation
$pageOrNull = $page->one_relation()->fromBoostID();

// resolving many relations
$pagesCollectionOrNull = $page->many_related()->fromBoostIDs();

Cache Drivers

A cache driver is a piece of code that defines where get/set commands for the key/value store of the cache are directed to. Kirby has built in support for File, Apcu, Memcached and Memory. I have created additional cache drivers for MySQL , Redis and SQLite.

site/config/config.php

<?php

return [
    // other options

    // example apcu
    'bnomei.boost.cache' => [
        'type'     => 'apcu',
    ],
];

Interactive Demo

I created an interactive demo to compare various cache drivers and prove how much your website can be boosted. It kind of ended up as a love-letter to the KQL Plugin as well. You can find the benchmark and interactive demos running on server sponsored by Kirbyzone here:

3 Likes

Hi @bnomei, thanks for another great plugin. I wanted to use the included Benchmark on a production server to decide which driver to use, but was unable to perform the test. Maybe I forgot some configs, but I couldn’t figure it out.

My php file looks like this:

<?php

$caches = [
    \Bnomei\BoostCache::sqlite(),
    \Bnomei\BoostCache::redis(),
    \Bnomei\BoostCache::memcached()
];

$results = \Bnomei\CacheBenchmark::run($caches, 1, site()->index()->count());
print_r($results['results']);

But it returns an empty array.

My models/default.php like this:

<?php
class DefaultPage extends \Kirby\Cms\Page
{
    use \Bnomei\PageHasBoost;
}

Do i need pages with the boostid-field for the benchmark test?

you do not need real pages to run the CacheBenchmark. but that is only a quick way to compare some caches. like the readme says you should verify real performance lateron using the boostmark() pages method as well. for example on my demo server the redis driver ranks 4th place in the generic benchmark but 2nd place when using the boostmark with real page objects.

the only situation for the array to come up emtpy would be that the drivers are not available as far as i can tell right now. so maybe var_dump the $caches array and see if there are objects in there or just null values.

ah yeah, i hadn’t configured the caches correctly. thanks for the hint.

unfortunately, i am unable to activate APCu and Memcached on the production server due to limitations (it’s a managed server). will there be support for the OPcache with the boost plugin? that would be great.

afaik opache does not have a user cache. it just caches the compiled php files.
but my sqlite driver is a pretty fast solution if you have sqlite3 support.

2 Likes

yes, sqlite does work. thanks for your help!

Hi again :slight_smile:

A few days later, i wanted to setup everything using the SQL-Lite server, but i get this Error:

Fatal error: Uncaught Error: The SQLite3 object has not been correctly initialised or is already
 closed in .../site/plugins/kirby3-sqlite-
cachedriver/classes/SQLiteCache.php:175 Stack trace: #0
.../site/plugins/kirby3-sqlite-cachedriver/classes/SQLiteCache.php(175): SQLite3Stmt-
>bindValue(':id', '...',
 3) #1 ...//site/plugins/kirby3-sqlite-cachedriver/classes/SQLiteCache.php(107): 
Bnomei\SQLiteCache->remove(...')
 #2 .../site/plugins/kirby3-sqlite-cachedriver/classes/SQLiteCache.php(102): Bnomei\SQLiteCache->removeAndSet('index', Array, 0) 
#3 /.../site/plugins/kirby3-boost/classes/BoostIndex.php(54): Bnomei\SQLiteCache->set('index', Array, 0) 
#4 /.../site/plugins/kirby3-boost/classes/BoostIndex.php(37): Bnomei\BoostIndex->write() #5 [internal function]: 
Bnomei\BoostIndex->__destruct() #6 {main} thrown in 
/.../site/plugins/kirby3-sqlite-cachedriver/classes/SQLiteCache.php on line 175

When running

site()->boost();

or

var_dump(site()->boostmark());

Do i need to initialize the SQLite Cachedriver plugin somehow?

Thank you!

i created an issue SQLite3 object has not been correctly initialised or is already closed · Issue #1 · bnomei/kirby3-sqlite-cachedriver · GitHub
lets continue discussing the sqlite driver issue there.

1 Like

Thanks for the fix!

One more question: does the Boost-plugin also speed up the Kirby query-language inside a blueprint? Like when used for my many-to-many plugin:

foreignkey:
        label: Project
        type: multiselect
        min: 1
        max: 1
        options: query
        query:
          fetch: site.find('projects').childrenAndDrafts
          text: "{{ page.title }}" #<-- will this be quicker with boost?
          value: "{{ page.boostid }}"

Will this speed up the fetching of the titles?

Thanks :slight_smile:

yes. once you have a page boosted accessing any content field will be faster since the initial load was made from cache.
in your example the title will be loaded the same time as the boostid. but that load will be from cache and thus faster than from disk.

it does not however reduce computational load like from using site->find or site->index. you would need to create a statically cached collection/sitemethod for these. static because it might be called by kirby more than once in a single request. i will publish a short example later.

1 Like

with boost its quiet easy to measure/see the performance gain. just toggle the debug mode. boost will always write to the cache but only load in non-debug mode.

1 Like

Thanks, thats very helpful. I will make a new release of my plugin using boost then soon.

i explained a bit how to create static cached collections here:

New version 1.8.0 adds a site index crawler with lower memory footprint than core kirby. This makes it possible to run a callback on a huge amount of pages.

Using site()->index() in Kirby will load all Pages into memory at the same time. This plugin provides a way to iterate over the index with having only one page loaded at a time.

$boostedCount = 0;
$indexCount = \Bnomei\Bolt::index(function ($page) use (&$boostedCount) {
    // do something with that $page like...
    $boostedCount += $page->boost() ? 1 : 0;
});
// or just
$boostedCount = site()->boost();

Within some limitations you could use \Bnomei\Bolt::index($callback) for your own code as well like when counting or updating pages.

The new version 1.9.0 add support for content caching of files and users. So if you have to read the content files of many of them then do consider using the Boost plugin to speed up the load time up to 3-4x times.

I update my boost plugin demo server to most recent version of kirby and kql. also moved it to a “slow” 5 euro/month hetzner instance to better show how much the plugins speeds up content loading.

the main demo page shows a general read/write benchmark for caches but that needs to be taken with a grain of salt since when using them with boost there are lots of initial writes (using transactions to batch them) when indexing and later on there are just a massive amount of read calls. some caches like sqlite and redis really excel at read speeds.
https://kirby3-boost.bnomei.com/

the subdomains each show a cache driver like apcu, redis, sqlite, mysql and php. the null cache driver is kind of special and at the same time not… it simply is running no cache at all. use this to compare how much faster things get using boost and a cache driver.

read 5500 pages in one request => earthlings
apcu: 1300ms
sqlite: 1500ms
redis: 1700ms
php: 2500ms (that loads all 38k pages of the demo since its a mono cache file)
mysql: 3300ms
null: 18.000ms

kirby actually has to load more than 5500 with all parent pages in the tree.

read 20.000 pages in one request => karma of humans
apcu: 14.000ms
sqlite: 14.000ms
redis: 17.000ms
php: 22.000ms (not sure why thats slow actually, will check it out, should be around 14-17sec)
mysql: > 30.000ms (thats 20k or more queries, no optimization yet)
null: 22.000ms

with lots of pages the performed computations of the demo take up a major part of the response time. the null driver performs well enough here but thats partly due to boost resolving the page uuids to page objects and not the core logic (see toPagesBoosted).

summary: as long as the read speed of your cache is faster than reading from files you gain some performance boost.

quick question:

lets say i have product pages which update their price every 30 minutes (e.g. page->price())

the cart itself collects the uuid and qty to calculate the cart array/session with the latest price

in the calculation if i were to use boost, it seems as though also the price is being cached. might that be the case? i thought boost is mostly speeding up the page lookup but not the contents?

because in my local demo i could see the prices do not add up and a certain price seems to be stuck/cached even after a price update.

within kirby’s methods for caching (tried apcu and sqlite) i make sure to output everything via post requests which should somewhat avoid caching - the same for disabling cache. if i were to remove boost from the calculation functions, it seems to calculate just fine again.

 // EXAMPLE
function cartTotal(){
    $total = 0;
    $cart = getCartSession();
    if(!is_array($cart)) return $total;
    foreach($cart as $item){
        if($page = page('page://'.$item['uuid'])){ // here using boost($item['uuid'])
            $total += $page->price()->value() * $item['qty']; 
        }
    }
    return ($total);
}

Edit:
Seems from the first post, it caches the content as well.
To explain further,

There’s a price countdown which updates the page()->price() every half an hour. The update itself obviously seems to work, but it doesn’t seem to flush boost cached / or update boost content of each certain page

adding page()->boost() after the actual price update, does not seem to make it work in the calculations…

boost is about content caching first and also resolving relations between pages. it kind of was a successor to autoid until the uuids became core.
see readme:

Kirby3 Boost
⏱️ up to 3x faster content loading
🎣 fastest page lookup and resolution of relations

what boost does is taking the modified timestamp of the page into consideration and maybe load the content from a cache.
it will write on any content change. so calling page->update() to save the new price should force boost to write a new cache for that models content as well.

it should not make any difference if you use the page or boost helper in your example on what content gets loaded. the boost helper just resolves the object in a different way. it does not alter how it gets loaded. the later is based on the model (extending the ModelHasBoost trait).

you can check if the ModelHasBoost is the issue by setting the config values to false. 'bnomei.boost.read' => false and 'bnomei.boost.write' => false

is the cartTotal function within a cached call - not related to boost?

i assume you are using k4? its a bit hard to debug this wihtout seeing the whole setup. you can invite me via github or PM me some more code? I will sign a NDA if you need one.

  • kirby 3, latest
  • the cartTotal function is within the plugin index

Basically using fetch JavaScript API, i’m pulling /cart.json via a route, which outputs html (via snippet) which will be loaded from the fetch and added to the cart div with innerHTML…

the cart contents are output correctly, e.g. going though the array, correct qty, but the prices seem not always be uptodate with what’s saved in the page (if i check via panel)

The fetch request includes no-store
The route shouldn’t be caching as it’s including a session (kirby docs), i have also changed to get the cart.json via post

That’s whats saved in the product-page:
price

That’s what’s loaded via fetch>route>snippet which uses the cartTotal function
cart

boost read/write “true” (default)
on

boost read/write “false” via config

off