Problems, need help? Have a tip or advice? Post it here.
8 posts Page 1 of 1
I've been playing around with some of the code and noticed that the sample tutorial pages (portfolio showcase) usually take around 250ms up to 750ms without caching enabled. I'm trying to get that down to 100ms and have been exploring ways to get there since tthe pages are really simple, and with caching enabled it's about 20ms or less.

My goal is to find ways to tweak the couch performance, before caching or any other features, and see how far I can get. It's not that I'm terribly concerned with the performance, but I like to push it to the extreme, before I build out complex websites, and then apply caching and service workers on top of that. Starting with the foundation seemed like a good idea.

I definitely tried playing with the existing tags and code, reducing the required files that are getting imported, the opcodes and cache settings queries, as well as the performance tips posted here in the forum,

By looking closer at xdebug traces and profiling it seems a lot of it has to do with the parsing approach (examining character by character). Not knowing anything about building such a marvelous parser that couch has, I wonder two things:
  1. Has it been considered, or is work in progress, to take a different approach to gain a massive speed increase? I don't know what it would look like, but I wonder if there are more efficient ways to do such complex template parsing?
  2. Has it been considered, or is work in progress, to do a little bit more with the caching functionality?

On the caching, more specifically,
  1. only invalidate the pages belonging to the template that received an update instead of all cached pages, and
  2. generating the cache after the update happened to a page in the back-end, and
  3. a special tag that is excluded to allow some dynamic php code in the template to remain after caching (I understand that this is not how the cache works current)
  4. some kind of warm caching script that crawls the links on a page to create the cached pages

I'm exploring to play with some of these things on the caching side, and see what I can put together, but wanted to check on the parser side first.
Thanks for the suggestions.
I do have a few plans for improving the cache system but, to be frank, those are not at the top of the agenda at the moment.
I'll keep the points you brought up in mind when work starts on that.

For improving the performance of what we have in the current state, I'd like to suggest two things -
1. Move over to PHP 7.x - that is a huge performance boost from the word go.
2. The parsed tree of snippets are cached (by default). We can use this to improve performance by placing the bulk of a template in snippets leaving the main template with just calls to <cms:embed .. />

Hope it helps.
@KK, thanks for your blazing fast response!

I'm using PHP Version 7.0.30-0 (FPM/FastCGI server API) and MariaDB 10.1.26, hosted on my OLinuXino A20-Lime2 (armhf) box. I'm playing around with opcache, redis, service worker, and other mechanism to get to the lowest possible response speed possible, constantly comparing between a pure HTML file (the outcome of the couch caching) of the page and the non-cached version.

Snippets is something I use for about everything (head,body,navigation,sidebar,tail,foot) except the main content of the page, and I'll play around with it more to see what I can achieve.

If helpful, as I mess around with performance improvements, I will suggest code or query improvements where I have positive results. I'm an enthusiastic hobby coder that feels very excited about CouchCMS and admire the work you do!
Thanks @den0x :)
Do keep me posted with your findings.
First and foremost, it's impressive how streamlined the codebase already is. After messing around with several things, it's clear that the parser is the big elephant in the room, and improving it's performance will have the biggest impact. That is a daunting task for me and so I've stayed away from it for now.

I'll go through some of the things that I've been playing with, as well as my performance results at the end (measured through a simple locust.io scipt). I'd love to hear feedback why something is or isn't a good idea of the things I'n experimenting with, beyond the logical backwards compatibility and supporting older software installations. For your reference, in the spirit of starting small, I have a small data set (13 tables, 974 rows, 2.7MiB), and have found that the performance gain is small and not really noticeable to user (except for my geeking pleasure I'm happy to shave of a few tens of ms here and there).
See for yourself.

What I've been playing with or have been pondering about...

  1. Small improvements, not a large difference on my small dataset, but I imagine this may have some speed improvements on larger data sets (worth considering taking these approaches going forward):
    • Moved functions, such as count() and strlen() outside of the for loop and, where possible, replaced it with a while statement.
    • Replaced count() and strlen() with empty() or isset(), or the negation of course, where no count is needed.
    • Replaced str_replace() with strtr() when only replacing one character for another.
    • Replaced foreach (hash as key => val) if the hash is getting modified inside the foreach loop (TODO: still got to clean this up in some places).
    • To avoid function _init_settings_cache(), which is a select * from 'couch_settings' without limitations, I got rid of all entries in that table for template files (based on get_cached_HTML(), get_setting() and set_setting()) and base64 encoded template code and turned on K_CACHE_OPCODES and K_CACHE_SETTINGS again. It greatly reduced the table size and the speed of the queries to that table (mostly for secret key and mosaic gc); prior to this table clean-up I had set K_CACHE_OPCODES and K_CACHE_SETTINGS to 0 to get away from this slow sql query.

  2. Small potatoes, mostly only relevant for my own use case, although it probably can become a config setting instead to apply to broader userbase and save speed for everyone:
    • Removed the date_default_timezone_set() in header.php (line 44 to 47) as I've done that through php.ini and this function isn't super fast.
    • Removed file_exists() checks on core config, kfunctions, themes and language files in header.php as this function isn't super fast (thinking about creating one admin consistency checker instead).
    • Removed check on couch version to trigger upgrade in header.php (line 317 to 331) as I watch the git repository for any uodates myself, reducing another query.
    • Commented out the recaptcha file inclusion in header.php for now as I'm not using this functionality (TODO: still want to play with this more to decide whether I'll use this over the older style captcha as I personally get annoyed the crap out of recaptcha on other sites).
    • Commented out an expensive routine in page.php (line 309 to 347) that was rearranging fields according to their groups (if in any), with or without it I don't notice any impact, and this code dates back several years ago to v1.3.5.
    • Playing around with replacing the strtoupper() in tags.php (line 1425) with "===", so far no impact noticed although I know that I'm not executing the same type of check, I only use names in lowercase.
    • Playing around with not executing the file_exists() on the cache lock file in tags.php (line 1472) that checks for failure in the past run of this routine.
    • Playing around with not executing the strtolower() and trim() in the register_udf() function (in functions.php, line 3509).

  3. What I'm playing with next:
    • See which uses of *, both in sql queries and regular expressions, can be replaced as that will also save some calculation time.
    • See if there's any performance increase by getting rid of the fallback on mysql, and only use mysqli functions (it seems to me to having your own mysql functions that invokve the mysqli class and then call the actual functions are all tiny calculations and steps that may have a tiny impact if I don't care about supporting anything other than mysqli.

  4. What I'm daunted by and staying away from right now (parser.php):
    • Wanted to make the is_valid_for_label() function use a regular expression, as it's called often, and time consuming in the big picture, but since this is called one character at a time the regex approach has no use, and calling it character by character is the bigger (daunting) challenge to figure out other ways for; same thing with the strpos() in set(), substr() in get_DOM(), and count() in $this->add_child().
    • Is there a faster way to set the page header() in COUCH::invoke (in cms.php, line 334), as it's a time consuming function.
    • Is there a faster way to trim() in gen_sql() (in functions.php, line 3333), as it's a time consuming function.
    • Is there a faster way to is_callable() in register_tag() (in functions.php, line 3459), as it's a time consuming function.


The results for just a single user and about 280 requests in total, with up a couple of seconds between each request (keep in mind that I run other services such as DNS and EMAIL on my server too, which may impact the performance for some requests)

I see a small improvement with the modified codebase for the minimum response time (between 20ms and 70ms lower), but the median and averages are about the same. The max response time is lower, but that could be because of outliers and due to other competing traffic or server load.
I don't despair and may look at it again with a bigger dataset and more concurrent users, see what that gives. First I'll try my two remaining ideas (use of widlcards and mysql support) in point #3 and then I'll probably do some more pondering about the daunting parser, as that really is where the most time is spent (in terms of processing cycles), or hot caching/template cache invalidation.

Attachments

After getting rid of the backward compatibility for mysql functions, replacing them with the mysqli functions and no longer checking for the availability of this module and include the mysqli class/functionI ran another test. This time with 10 users concurrently, 50ms between each request, at about 3 requests per second, up to 1500 requests during a small 10 minute window.

The minimum response time on the modified codebase is on average 30ms faster (up to 90ms) for the minimum response time, and is on average 135ms faster (up to 850ms) for the maximum response time (keep in mind, this could be stating more about my server choking up than anything else). I see some outliers for about.php, products.php (listing) and news.php (listing) that indicate that some of my modifications may not have the desired impact.

In general though, I'm happy that my modifications have not been for nothing thus far, but clearly nothing thus far is providing a dramatic difference

Attachments

The last thing that I'll add today is some stats from xdebug traces, as it helps me focus my attention and look at the bigger picture. "occurs" refers to how many times the function exists in the file, while "called" refers to how many times this function was called after visiting 10-15 pages of my couch powered website (not the admin back-end).

Code: Select all
<h1> 55.7% of processing time for /parser/parser.php --- looping over all characters is definitely the biggest drain
    <h2> 11.9% of which 9.3% to all KParser->is_valid_for_label() (occurs 5 times) and 2.6% to all KParser->is_white_space() (occurs 6 times) --- called 30k and 10k times respectively...
    <h2> 9.8% goes to all count() (occurs 13 times)
    <h2> 6.5% goes to all strpos() (occurs 5 times) --- noticed that .7% goes to KContext->_get() and .7% goes to strpos() in the same context
    <h2> 4.3% goes to all substr() (occurs 16 times)
    <h2> 2.8% goes to all in_array() (occurs 5 times)
    <h2> 4.3% of which 2.6% to KContext->_set() and 1.7% to KContext->set() --- noticed perhaps something that may need fixing related to the use of _set() vs/and set()
    <h2> 2.4% goes to all KFuncs->dispatch_event() (occurs 7 times) --- called 8k times
    <h2> 2.2% goes to all KContext->push() (occurs 4 times) -- and .6% goes to KNode->get_HTML()
    <h2> 1.9% goes to KContext->pop()
    <h2> 1.8% goes to KNode->__construct()
    <h2> 1.2% goes to all KParser->add_child() (occurs 4 times)
    <h2> 2% of which 1.1% to method_exists() and .9% to KFuncs->resolve_parameters() -- then 1.1% for KFuncs->dispatch_event()
        <h3> KParser->get_DOM() has 17.4% overall weight according to xdebug profiling -- called 191 times from parser.php
        <h3> KNode->get_HTML() has 12.3% overall weight according to xdebug profiling -- called 7.5k times, mainly from tags.php and parser.php
        <h3> KContext->set() has 4.3% overall weight according to xdebug profiling -- called 8k times, mainly from parser.php and tags.php
        <h3> KContext->_set() has 2% overall weight according to xdebug profiling -- called 8k times from parser.php
        <h3> KContext->push() has 3.8% overall weight according to xdebug profiling -- called 7.5k times from parser.php
        <h3> KParser->get_HTML() has 3.4% overall weight according to xdebug profiling -- called 145 times from parser.php (121), cms.php (22 x invoke) and tags.php (2)
        <h3> KParser->is_white_space() has 2.2% overall weight according to xdebug profiling -- called 10k times from parser.php

<h1> 21.6% of processing time for /functions.php --- beyond the query on 'couch_settings', nothing stands out
    <h2> 3.4% of which 2% goes to mysqli_query() and 1.3% goes to mysqli_fetch_row() and .1% goes to mysqli_free_result()
    <h2> 3.2% goes to EventDispatcher->dispatch()
    <h2> 3.1% goes to all trim() (occurs 26 times) -- called 15k times
    <h2> 2% goes to all array_key_exists() (occurs 9 times) -- called 6k times
    <h2> 1.1% goes to all is_callable() (occurs 2 times) -- called 3k times
    <h2> .6% goes to all KFuncs->is_callable() (occurs 4 times)
    <h2> .9% goes to KFuncs->_register_render() (occurs 2 times)
    <h2> .5% goes to eval()
    <h2> .3% goes to EventDispatcher->add_listener()
    <h2> .3% goes to method_exists() -- called 4k times
    <h2> other mentions go out to sizeof(), strpos() -- called 22k times, strtolower() -- called 3k times, strtotime()
        <h3> KFuncs->dispatch_event() has 2.3% overall weight according to xdebug profiling -- called 8k times, mainly from parser.php and page.php

<h1> 7.5% of processing time for /tags.php --- looks like small potatoes, nothing stands out
    <h2> 1.6% goes to all trim() and strtolower() (occurs 116 and 28 times respectively)
    <h2> 1.3% goes to all KNode->get_HTML() (occurs 22 times)
    <h2> 1% goes to all extract() and KFuncs->get_named_vars() (occurs 28 times)
    <h2> .9% goes to all KContext->set() (occurs 93 times)
    <h2> .3% goes to KFuncs->resolve_condition()
        <h3> KTags->k_if () has 2% overall weight according to xdebug profiling -- called 931 times from parser.php

<h1> 5.4% of processing time /db.php --- looks like small potatoes, nothing stands out
    <h2> 2.6% goes to mysqli_query() -- called .7k times
    <h2> 1.2% goes to mysqli_fetch_assoc() -- called 2.3k times
    <h2> .4% goes to mysqli_free_result() -- called .6k times
    <h2> .3% goes to mysqli_real_escape_string() -- called .7k times
        <h3> mysqli_query() has 4.1% overall weight according to xdebug profiling -- called 758 times, mainly from db.php


I'm running out of places to look and tweak so learning more about the parser is going to be my next challenge.
Wow!. I can only say - thank you, @den0x, for making such an effort.
8 posts Page 1 of 1
cron