Some Elgg performance/caching ideas

I just wanted to dump some ideas here lest I forget. I think the key to improving Elgg's performance is going to be maximizing re-use of views across users and across time. Some ideas I think could be pursued:

1. A caching mechanism that can be dynamically enabled (or disabled) to arbitrary views with flexible critera for reuse/invalidation. This would be a scalpel, allowing Elgg developers to optimize particular cases on a site. E.g. a way to turn this into code: "On URLs matching pattern A, cache the foo/bar view across all non-admin users and invalidate it every 30 seconds"

2. A mechanism for rendering views that can be reused across the most common user cases. E.g., say an entity summary view has 3 basic cases: [logged out users, logged in users, users who can edit it]. Instead of rendering for each as necessary, render all 3 views to a single string with a unique boundary (like in MIME) and cache it. Before display to particular user, cut out the unneeded sections with PHP's blazingly fast binary string functions (str_replace, strpos, substr, etc.).

3. Embed particularly useful metadata into cached representations, like if an entity is public or viewable by all logged in users. E.g. In an entity list, it may be more efficient to cache the first several pages of all applicable entities. Then when a user needs page 1, look up access visibility only for the more restricted items and just cut out invisible items from the cached string. Later pages could use the standard list_entities queries: most users won't get to these pages anyway.

4. Explore new pagination models: Ideally, no separate COUNT queries would be needed at all: always select one more row than you need to display the current page, which tells you if the next page exists. For situations where full page counts are necessary, cache the COUNT queries, and give larger count values longer TTLs. (Imagine if Google had to give accurate page counts for search results on every page. No one needs that). The endless scroll type of item viewing also allows for nice optimizations, like not having to worry about displaying fixed-size chunks of entities, and leaning a bit on HTTP caching.

5. Somehow allow particular view sequences to be "compiled" into flatter code while stripping away unnecessary code paths. Even with the view cache PHP has to load lots of files and make very many and deeply-nested function calls, which I bet have a real cost.

6. Slowly transition the API towards passing entities directly into functions rather than GUIDs. I see many instances where $entity->guid is passed into a function, which immediately turns around and calls get_entity($guid). Step through this process and watch the call stack... What I've always seen is that function calls are expensive when they start to add up.

  • Great stuff, Steve. What I experienced as a bottleneck was the Elgg engine startup time (using high number of plugins). This only becomes an issue when a lot of code uses background ajax calls (periodic poll-based refresh stuff and alike). Starting up the whole Elgg engine to serve a lightweight call is an overkill - think about calls only needing access to the database (or to the server session).

    I'm not sure if this is something that should be addressed at core level - maybe it's a very specific problem to some installations. Some php frameworks use lazy loading, maybe it's worth considering for Elgg as well.

    At the end, I improved the mentioned site's performance by implementing a custom, configurable boot process, and I defined for most actions what core parts and what plugins should be loaded and initialized. Not very nice, but was certainly effective in that particular case.

  • Yes, polling is killer (ignore the Elgg rant, though some of it still applies :)). There's a lot of lazy loading optimization possible, but Elgg has to move towards an OO API to really start seeing the gain, and that doesn't have a lot of traction.

    I think the plugin problem can be solved in a similar way, but it requires work in each plugin: move almost all functionality to class files (make start.php lightweight) and provide mechanisms to allow not registering things like hooks/page handlers when they shouldn't be needed. Some plugins don't even need to init() on every page.

    And of course, we need a global autoloader on which plugins can register namespaces/prefixes. I've made a plugin to provide ZF 2.0's StandardAutoloader for this purpose, but haven't released it yet.

  • We need to decide when to move to PHP 5.3 only.

    We need to have better caching. We need to make the engine slimmer. Like Steve said, moving from lots of functions to classes will help. Right now we load 1,400 lines of web services code on every page load regardless of whether it is needed.

  • @Steve: totally agree abt the plugins code loads - the only thing most plugins need is to declare their menu-item, nothing else. all other pieces of code can/should be loaded on-demand. exceptions might be themes where css needs to be loaded for every page. mandated/regimented classes ? i 'Morger' (Carlos Tealdi, Argentina) developed an o-o version of messages plugin -- http://community.elgg.org/pg/plugins/project/391040/developer/morgar/modified-messages-plugin-using-an-extensible-object-oriented-controller & http://community.elgg.org/mod/groups/topicposts.php?topic=391104&group_guid=212846 -- as an 'experiment' abt 18 mnths back - still sitting there @ repo if you wanna read code/study.

  • 7. Adopt plugin standards that would allow precompilation of plugin start-ups into a single PHP file on disk. The file would include a function to call all the plugin init() functions (no loops, no argument passing). In the short term, precompile the code that calls all the init functions (does it really need to use the dynamic trigger event infrastructure for each init call?).

  • Hey, Steve,

    Some good discussions here. Sorry I'm just now noticing them. Seems like the lowest hanging fruit is in

    1. Reducing queries to the database
    2. Reducing amount of code loaded/executed per page
    You and I are already working on #1 with metadata. I feel pretty confident that work will turn out to have a big effect. In order to really win at #2, though, Elgg is going to need some serious refactoring, as we all realize.
    Here's the real problem: In order to do refactoring safely, we need tests. In order to write good tests, we need to refactor.
    This is why we can't have nice things.
  • A little sidenote comment - it would be nice if plugin was represented by an object. Registering new actions, new CSS, new pagehandlers etc. could be operations on an object, or could be done in specific object methods. This won't increase performance a lot, but should eg. allow to test pagehandlers. It would also organize code structure in plugins. In later stages, it could be used to not to process pieces of code that are not being used (although this is not the most important gain - those reported by Evan are most important).

  • @Mike, if I understand you, you want an API kinda like this?

    function myPlugin_init(ElggPlugin $plugin) {
        $plugin->registerSomething(...);
    }

    Can you expand on this idea in a new topic?

     

  • Steve, I will :)

    btw, comments to your remarks:

    ad 4. It's very important. It's a must for sites with larger amount of data.

     

    ad 6, comment of our developer:

    It seems as rather bad idea
    -  it may create overhead in situations where we don't have to instantiate ELggEntity
    - it's not necessary, as optimizing elgg_get_entity to serve instantiated entity from cache for repeated calls removes current overhead
    - it may create probems while separating parts of page (so important while optimizing). It's harder to compare view's parameters that are objects than compare single guid. Also views for /ajax/ calls won't be compatible with object specific ones
    What we actually need here is to cache entity with the same guid and reuse it instead of fetching again. This actually solves our problem. Of course we could track situations where there's no gain from falling back to guid from object, but there are much more important stuff to fix.

     

    btw, I added our report regarding possible optimizations of Elgg's core as an article here.

  • Re: 6, the cases I'm mostly referring to are funcs/meths that always call get_entity() as their first step. Here the overhead is already being paid, it's just out of sight. Functions/methods/classes should declare their dependencies and these are cases of hidden service locator. Type hinting with ElggEntity would clean up the API, improve static analysis by IDEs, and force the developer to consider the case that the entity can't be loaded; he/she should probably not be calling the function without a usable entity.

    The recent improvements made in the entity caching (still in pull requests) will mean this is no longer much of a performance need, but I still think it would lead to (c)leaner code both in the API and its usage.

Performance and Scalability

Performance and Scalability

If you've got a need for speed, this group is for you.