Improving Plone 3 Product Performance

(For more resources on Plone, see here.)


CMS Plone provides:

  • A Means of adding, editing, and managing content
  • A database to store content
  • A mechanism to serve content in HTML or other formats

Fortunately, it also supplies the tools to do all these things in an incredibly easy and powerful way. For example, content producers can create a new article without worrying how it will look or what other information will be surrounding the main information.

To do this Plone must compose a single HTML output file (if we are talking from a web browser viewpoint) by joining and rendering several sources of data according to the place, importance, and target they are meant for.

As it is built upon the Zope application server, all these jobs are easy for Plone. However, they have a tremendous impact as far as work and performance goes. If enough care is not taken, then a whole website could be stuck due to a couple of user requests.

In this article, we'll look at the various performance improvements and how to measure these enhancements.

We are not going to make a comprehensive review of all the options to tweak or set up a Zope-based web application, like configuring a like configuring a proxy cache or a load balancer. There are lots of places, maybe too many, where you can find information about these topics. We invite you to read these articles and tutorials and subscribe or visit Zope and Plone mailing lists:

Installing CacheFu with a policy product

When a user requests HTML pages from a website, many things can be expressed about the downloading files by setting special headers in the HTTP response. If managed cautiously, the server can save lots of time and, consequently, work by telling the browser how to store and reuse many of the resources it has got.

CacheFu is the Plone add-on product that streamlines HTTP header handling in order to obtain the required performance.

We could add a couple of lines to the buildout.cfg file to download and install CacheFu. Then we could add some code in our end user content type products ( and Products.poxContentTypes) to configure CacheFu properly to deliver them in an efficient way.

However, if we do so, we would be forcing these products to automatically install CacheFu, even if we were testing them in a development environment.

To prevent this, we are going to create a policy product and add some code to install and configure CacheFu.

A policy product is a regular package that will take care of general customizations to meet customer requirements. For information on how to create a policy product see Creating a policy product.

Getting ready

To achieve this we'll use pox.policy, the policy product created in Creating a policy product.

How to do it...

    1. Automatically fetch dependencies of the policy product: Open in the root pox.policy folder and modify the install_requires variable of the setup call:

      # -*- Extra requirements: -*-

    2. Install dependencies during policy product installation. In the profiles/default folder, modify the metadata.xml file:

      <?xml version="1.0"?>

You could also add here all the other products you plan to install as dependencies, instead of adding them individually in the buildout.cfg file.

    1. Configure products during the policy product installation. Our policy product already has a <genericsetup:importStep /> directive in its main component configuration file (configure.zcml). This import step tells GenericSetup to process a method in the setuphandlers module (we could have several steps, each of them with a matching method). Then modify the setupVarious method to do what we want, that is, to apply some settings to CacheFu.

      from import getSite
      from Products.CMFCore.utils import getToolByName
      from config import *

      def setupVarious(context):

      if context.readDataFile('pox.policy_various.txt') is None:
      portal = getSite()

      # perform custom operations
      # Get portal_cache_settings (from CacheFu) and
      # update plone-content-types rule
      pcs = getToolByName(portal, 'portal_cache_settings')
      rules = pcs.getRules()
      rule = getattr(rules, 'plone-content-types')
      rule.setContentTypes(list(rule.getContentTypes()) +

The above code has been shortened for clarity's sake. Check the accompanying code bundle for the full version.

  1. Add or update a file in your package with all configuration options:

    # Content types that should be cached in plone-content-types
    # rule of CacheFu
    CACHED_CONTENT = ['XNewsItem', 'Video',]

  2. Build your instance up again and launch it:

    ./bin/instance fg

  3. After installing the pox.policy product (it's automatically installed during buildout as explained in Creating a policy product) we should see our content types—Video and XNewsItem—listed within the cached content types.

    The next screenshot corresponds to the following URL: http://localhost:8080/plone/portal_cache_settings/with-caching-proxy/rules/plone-content-types.

The with-caching-proxy part of the URL matches the Cache Policy field; and the plone-content-types part matches the Short Name field.

As we added Python code, we must test it.

  • Create this doctest in the README.txt file in the pox.policy package folder:

    Check that our content types are properly configured

    >>> pcs = getToolByName(self.portal, 'portal_cache_settings')
    >>> rules = pcs.getRules()
    >>> rule = getattr(rules, 'plone-content-types')
    >>> 'Video' in rule.getContentTypes()
    >>> 'XNewsItem' in rule.getContentTypes()

  • Modify the tests module by replacing the ptc.setupPloneSite() line with these ones:

    # We first tell Zope there's a CacheSetup product available

    # And then we install pox.policy product in Plone.
    # This should take care of installing CacheSetup in Plone also

  • And then uncomment the ZopeDocFileSuite:

    # Integration tests that use PloneTestCase
    'README.txt', package='pox.policy',

  • Run this test suite with the following command:

    ./bin/instance test -s pox.policy

How it works...

In the preceding steps, we have created a specific procedure to install and configure other products (CacheFu in our case). This will help us in the final production environment startup as well as on installation of other development environments we could need (when a new member joins the development team, for instance).

In Step 1 of the How to do it... section, we modified to download and install a dependency package during the installation process, which is done on instance buildout. Getting dependencies in this way is possible when products are delivered in egg format thanks to Python eggs repositories and distribution services.

If you need to get an old-style product, you'll have to add it to the [productdistros] part in buildout.cfg.

Products.CacheSetup is the package name for CacheFu and contains these dependencies: CMFSquidTool, PageCacheManager, and PolicyHTTPCacheManager.

There's more...

For more information about CacheFu visit the project home page at

You can also check for its latest version and release notes at Python Package Index (PyPI, a.k.a. The Cheese Shop):

The first link that we recommended in the Introduction is a great help in understanding how CacheFu works:

See also

  • Creating a policy product
  • Installing and configuring an egg repository

(For more resources on Plone, see here.)

Improving performance by tweaking expensive code

Not all performance problems will be solved by installing and configuring CacheFu. Sometimes there are serious issues in the very code because it could have been coded in a different and more efficient way, or just because of some job that takes too long.

Let's leave the inefficiency problem aside. Many times we perform expensive tasks like searching objects in the catalog, getting images, accessing external relational databases, or accessing web services, and we know in advance that they will be necessary again and again. How to deal with them? Or at least, how to make them more efficient?

In this section, we will see three alternative approaches to tackle this problem:

  1. @view.memoize decorators.
  2. @ram.cache decorators.
  3. Volatile variables.

Getting ready

The example below will perform a simple catalog query to get all banners located inside a specific section of our portal.

The final classes and methods won't be used anywhere else in our site. They are just dummy examples. However, we will reuse part of their code in Customizing a new portlet according to our requirements.

How to do it...

  1. Open the browser sub-package inside the pox.banner product and create this file:

    from Products.Five.browser import BrowserView
    from Products.CMFCore.utils import getToolByName
    from pox.banner.content.banner import IBanner
    from Acquisition import aq_inner
    import logging
    from plone.memoize import ram, view
    from time import time

    logger = logging.getLogger('pox.banner')

    class ISectionBanners(Interface):
    Retrieve section's contained banners
    def banners():
    Get contained banners
    def banners_count():
    Get banners count

    class SectionBanners( BrowserView ):
    Helper view to test code caching techniques
    def banners(self):
    The actual search
    logger.log(logging.DEBUG, 'calling banners')
    context = aq_inner(self.context)
    catalog = getToolByName(context, 'portal_catalog')
    query = dict(object_provides = IBanner.__identifier__)
    query['path'] = {
    'query': '/'.join(context.getPhysicalPath()),
    'depth': 1 }
    brains = catalog(query)

    return brains

    def banners_count(self):
    Tells how many banners we found
    logger.log(logging.DEBUG, 'calling banners_count')
    return len(self.banners())

    We'll use this method for the examples later.

  2. Add an adapted class based on the preceding SectionBanners but decorated with @view.memoize:

    class SectionBannersMem( SectionBanners ):
    Alternative implementation of the SectionBanners class
    def banners(self):
    Decorated. It just calls SectionBanners' banners().
    return super(SectionBannersMem, self).banners()

  3. Add another class based on SectionBanners but decorated with @ram.cache this time:


    def _banners_cachekey(method, self, **args):
    Returns key used by @ram.cache.
    the_key = list(self.context.getPhysicalPath())
    the_key.append(time() // RAM_CACHE_SECONDS)
    return the_key

    class SectionBannersRam( SectionBanners ):

    Alternative implementation of the SectionBanners class
    def banners(self):
    Decorated. It just calls SectionBanners' banners().

    return super(SectionBannersRam, self).banners()

  4. Add a third SectionBanners-based class, this time without any decorator but using volatile variables.

    class SectionBannersVol( SectionBanners ):
    Alternative implementation of the SectionBanners class
    def banners(self):
    It just calls SectionBanners' banners() when needed.
    the_banners = getattr(self.context, '_v_banners', None)
    if the_banners is None:
    the_banners = super(SectionBannersVol, self).banners()
    self._v_banners = the_banners
    return the_banners

  5. Modify configure.zcml in the browser sub-package to enable the above code as browser views:


How it works...

In Step 1, we created the sectionBanners module (with all the imports required for the following steps), the interface, and the base class (that we will use), where:

  • we first inform there's a call to the banners method by using logger.log(...).
  • then we perform the actual search. We are interested in objects providing the IBanner interface located right inside the section we are (denoted by context). The usage of 'depth': 1 tells portal_catalog not to search in the sub-sections.

logger is a handle to output debug information in the instance log (or console when running Zope in foreground mode).

In Step 2, we have only decorated the inherited banners method with @view.memoize. This decorator, from plone.memoize, caches the result of the decorated method. In this way, if we call it several times in the very same request, it will be executed just once. Current context, request, and method's arguments are used to key the values in this short-time cache.

Alternatively, you can use @instance.memoize (from the same package) which caches on the instance and is useful in adapters and other transient components.

Step 3 is similar to the previous one, but the banners method this time is decorated with @ram.cache. Note how, unlike in the @view.memoize decorator, we must state specifically the caching key when using @ram.cache. In the case above, we created a special _banners_cachekey module-level method to return an interesting caching key. By using time() // RAM_CACHE_SECONDS, we ensure that the returned value will be stored in cache for the specified time. We can also use other parameters like context, authenticated user, or any other value that might be helpful.

The next approach (Step 4) is the usage of volatile variables (they must be named with a v_ preffix). This method is the cheapest and a more direct way of caching values. There's no certainty on how long they will be kept in memory. That's why we must always check if the volatile variable is still there.

Before using any volatile variable inside a method's body, we must check if it is still there, as you can see in the highlighted code in the highlighted code of Step 4.

The last step is to register one Zope 3 view (<browser:page /> directive) for every SectionBanners alternative.

Notice how we used allowed_interface to state which attributes or methods will be available in the outside world. We could have used the allowed_attributes parameter instead, like this:

allowed_attributes="banners banners_count"

Attributes are separated with whitespaces.

Using allowed_interface is preferable when you are planning to add a new method or attribute or when all of them are protected by the same permission. You should just add it in the interface rather than changing every <browser:page /> directive. However, if you want to limit the access of attributes or methods (by changing the permission value) you might need to create different browser pages with different allowed_attributes values.

Caching always carries penalties as increasing memory usage, but it also brings obvious benefits. How, when, and where to use any of these options is up to you and your code.

As a rule of thumb, it's advisable to cache methods only when retrieving information, like in our example from the catalog, and not for operations with side effects, like modifying the database.

The shown code won't be used in any production environment, it was created just for learning purposes. We have also included a test inside the README.txt file in the accompanying code, not copied here, to see how every kind of caching mode works.

We kindly invite you to have a look at it to fully understand the approaches described in this section. On top of that, we have incorporated some new techniques you may find interesting for testing:

  • Performing different operations within the same request by means of zope.publisher.browser.TestRequest:

    >>> from zope.publisher.browser import TestRequest

    New request
    >>> request = TestRequest()

    >>> banners = getMultiAdapter((self.portal.first_section,
    request), name=u'banners')

    Another new request
    >>> request = TestRequest()
    >>> banners = getMultiAdapter((self.portal.first_section,
    request), name=u'banners_ram')

  • Testing what Zope logs (or puts out in the console) by redirecting it to the standard output (sys.stdout).

The only piece of test code we'll mention here is the way we can use a browser view from outside a Zope Page Template (that is, without using the @@ notation):

>>> from zope.component import getMultiAdapter
>>> banners = getMultiAdapter((self.portal.first_section,
request), name=u'banners')
>>> banners.banners()

If we were in a page template, we'd call the banners method above like this:

<tal:view define="view context/@@banners;
banners view/banners">
<!-- do something with the banners -->

If we wanted to check the banners method by calling it directly from the URL we could use:


In the last two cases, the call to the @@banners browser view has two implicit arguments: context and request. These two are explicit, however, when calling it from inside Python code. The context is self.portal.first_section and the request is an instance of TestRequest (in production code we generally use the same request parameter used in the _init_ method).

There's more...

Luckily, yes, there's even more! Consider having a look at the plone.memoize package to see what the other alternatives are.

See also

  • Customizing a new portlet according to our requirements

(For more resources on Plone, see here.)

Testing server load and benchmarking our code

In the previous recipes, we have seen how to improve the general performance of our website by setting the proper HTTP headers for server responses (Installing CacheFu with a policy product) and by adding caching facilities to the underlying Python code (Improving performance by tweaking expensive code).

It's now time to verify if those changes really worked.

In this section, we will introduce and work with FunkLoad, a functional and load web tester.

Unfortunately, at the time of writing, FunkLoad doesn't work in Windows. This is because of the lack of support for os.fork(), which is a core function used by FunkLoad: one thread for every concurrent user. However, a fix for it is expected to be applied soon.

Getting ready

To install FunkLoad we will need to make several changes to the original bulidout.cfg file. Thanks to the extendible capability provided by buildout, we can separate those modifications in a different file and merge the entire configuration when building the instance up.

How to do it...

    1. Create this funkload.cfg file in the instance root folder:

      The real funkload.cfg can be found in the available code for download. We have shortened it here to highlight some interesting options.


      extends =
      parts +=
      eggs +=
      recipe = collective.recipe.plonesite
      site-id = pox.banner.tests
      instance = instance
      site-replace = True
      products = pox.banner

    2. Build your instance again and empower it with FunkLoad. Inside your buildout folder run:

      ./bin/buildout -c funkload.cfg

      The -c option above is to specify an alternative configuration file. The default is buildout.cfg, but you can use whatever you want.

    3. Launch the FunkLoad recorder. Like with zope.testrecorder and Selenium, we can record FunkLoad test cases by navigating and clicking on a live website. So start your instance:

      ./bin/instance fg

      Go inside the tests sub-package where you want to store the new test file:

      cd ./src/pox.banner/pox/banner/tests

      And launch the FunkLoad recorder:

      ../../../../../bin/fl-record CreateSectionBanner

Of course, you can replace all the double dots above with the absolute route of your instance folder. The point here is that you must run the fl-record command with the name of the module you want to create in the folder in which you want it to be stored.

You'll get an output like this:

  1. Edit proxy settings in the web browser:

    FunkLoad recorder is actually a proxy running in port 8090. To record all our interactions, we must edit our web browser network settings:

    • Add localhost:8090 as the HTTP proxy.
    • Make sure the proxy above will be used even when browsing our Plone site located in http://localhost:8080/.

    For Firefox in Linux, go to the Edit | Preferences menu, Advanced | Network tab, Settings button. For Firefox in Windows, use the Tools | Options menu instead.

    We may choose another port number if we run the fl-record command with the -p option:

    ../../../../../bin/fl-record -p 9090 CreateSectionBanner

  2. Record the very test. For testing purposes, we have created a new pox.banner.tests Plone site during instance buildout. So go to http://localhost:8080/pox.banner.tests and start recording the test:
    • Log in as a manager with the Log in left-hand portlet (admin: admin).
    • Click on the Add new drop-down menu and then click on the Section option.
    • Type Section1 in the Section title field and then press the Save button.
    • Click again on the Add new drop-down menu, but click on the Banner option this time.
    • Type Banner1 in the Banner title field and some HTML code in Banner HTML field (or just leave the default value) and then click on Save.

    That's it, the test is finished. Press Ctrl + C in the FunkLoad recorder console to stop recording. You should see a message like this:

  3. Modify the just generated test file:

    We must first modify the base class of the test to accommodate it to the Zope environment. Add this import line:

    from collective.funkload import testcase

    And change the class definition with this one:

    #class CreateSectionBAnner(FunkLoadTestCase):
    class CreateSectionBanner(testcase.FLTestCase):

    Unlike zope.testrecorder and Selenium IDE, FunkLoad doesn't record either mouse clicks or keyboard hits, but HTT P requests. Thus any particular URL that should be dynamic during testing or benching and has been recorded statically must be replaced with Python code to reconstruct the usable URL.

    Open the generated test_CreateSectionBanner module to make some changes. The lines:

    ... + "/pox.banner.tests /+/addSection",
    params=[['form.title', 'Section1'],
    ['', 'Save']],
    description="Saving Section add form"

    should be replaced with

    section =
    server_url + "/pox.banner.tests /+/addSection",
    params=[['form.title', 'Section1'],
    ['', 'Save']],
    description="Saving Section add form"
    section_url = server_url + section.url.rsplit('/', 1)[0]

    This way we get a handle to the created section and its URL. Recall that /view is added to the section URL after saving it, that's why we rsplit it.

    Then replace every appearance of

    server_url + "/pox.banner.tests /section1/+add++Banner"


    section_url + "/++add++Banner"

    We did this because in subsequent tests or benches, the newly created section could be saved with a different ID.

    For Archetypes content types, we must make some additional adjustments, given that Archetypes create temporary objects with a very specific ID. See the full file for an example on how to get the correct URLs.

    You may also find inline validation requests (performed with AJAX) inside the testing code. Just remove them.

    One last important modification is adding a test_suite method with a specific level.

    def test_suite():
    suite = unittest.makeSuite(CreateSectionBanner)
    suite.level = 5
    return suite

    When running regular tests with Zope test runner, all suites with level 1 are executed by default. If we didn't specify a higher level, this test suite might fail because it always needs the instance to be running. By adding a custom level, we prevent this from happening.

    Note that the test_suite method must be placed outside the CreateSectionBanner class.

  4. Run the test with Zope test runner:

    This is an optional step.

    As with Selenium functional tests, FunkLoad needs a live instance for the test suite to be run:

    ./bin/instance fg

    As we said earlier, FunkLoad test suites can be run with Zope test runner as any other kind of test. To do so, use the following command:

    ./bin/instance test -s pox.banner -t CreateSectionBanner -a5

    The -t option sets the specific test we want to run in the pox.banner package (test_ prefix may be omitted, like here).The -a option specifies the highest level of test suites we want to run (that is, test suites with levels 1 to 5 will be run here).

  5. Run the test with FunkLoad test runner:This is an optional step.Be sure the instance is running. Then run the following command:

    ./bin/fl-run-test pox.banner.tests.test_CreateSectionBanner

    Unlike with Zope test runner, we must indicate the full module dotted name when using the fl-run-test command.

How it works...

We have created a new configuration file for our Zope instance: funkload.cfg. Although we have already used the extends parameter in previous chapters, we want to identify it here and explain how it works: buildout.cfg is extended by funkload.cfg. Thus, when building an instance with the last one, every configuration option in the first one will be also considered.

Due to the extension nature of this funkload.cfg file, we must add parts and eggs by using the += operator. If we had used the regular parts = ... line, we would have replaced and missed all other parts defined in buildout.cfg.

Finally, we create a new Plone site with the collective.recipe.plonesite recipe. This one will be used to test the pox.banner product (that's why it is automatically installed). Unlike in buildout.cfg, this Plone site will be replaced each time we run the buildout process (site-replace = True).

There's more...

We started this chapter with the installation of CacheFu and its configuration for our content types to get them better response time. After that we played around with several alternatives for caching expensive code results. However, we didn't test or bench any of those features.

We explicitly wanted to show how to solve some tricky parts of FunkLoad test creation. We expect the example above is clear enough for you to create your own FunkLoad test and bench it, with and without CacheFu configurations.

Bench configuration file

When recording the test earlier, an additional CreateSectionBanner.conf file was created inside the tests sub-package. Let's see some of its configuration options.Please refer to the accompanying code for the full file.

# Main section
# the User-Agent header to send default is 'FunkLoad/1.xx' examples:
#user_agent = Opera/8.0 (Windows NT 5.1; U; en)

We can specify the user_agent we wish when running a test or bench.

# Monitoring configuration

We can also monitor server performance when running benchmarks: CPU, memory, and network activity.

# Configuration for bench mode fl-run-bench

# cycles = list of cycles with their number of concurrent users
#cycles = 1:2:3
cycles = 1:2:3:5:10

# duration = duration of a cycle in seconds
#duration = 30
duration = 120

# startup_delay = time to wait between starting-up threads in seconds
startup_delay = 0.2

# sleep_time = time to wait between test in seconds
sleep_time = 1

# cycle_time = time to wait between cycle in seconds
cycle_time = 1

We have made some changes to the default values. We added two extra cycles with 5 and 10 concurrent users (cycles are separated with a colon), and we extended the duration of the cycle from 30 to 120 seconds. Consequently, we'll have five cycles: the first one with one user (read: one thread or browser request) that will repeat the test for 120 seconds, the second cycle with two concurrent users that will perform test tasks for 120 seconds, and so on.

Running the bench

We now have a special pox.banner.tests Plone site to run our tests and benchmarks, a test suite we want to run, and its corresponding configuration file. Just be sure the instance is up before running the following command to start the bench:

./bin/funkload bench -s pox.banner -a5

This will call the funkload script with the bench command and two well known other options: -s, to specify the package we want to test and -a that tells the level of the considered tests suites.You'll get an output similar to this:

Cycle #0 with 1 virtual users
* Current time: 2009-09-28T00:41:57.559282
* Starting threads: . done.
* Logging for 120s (until 2009-09-28T00:43:57.817400): ............
* Waiting end of threads: . done.
* Waiting cycle sleeptime 1s: ... done.
* End of cycle, 123.00s elapsed.
* Cycle result: **SUCCESSFUL**, 12 success, 0 failure, 0 errors.
Creating html report: ...done:

As stated in the output, we get an HTML report with lots of information about the load test we have just performed. We include here a few graphics as examples of the information you can find.

This first chart is one of several that show the server's activity during the whole bench:

This second graphic shows the response time of a particular web page during the test:

This report is way comprehensive and will help you in identifying possible bottlenecks in your code or configuration. On top of that, after tweaking your code you can run the bench again to get the results of your improvements and you will be able to show your boss nicely colored drawings!

Further reading

FunkLoad is incredibly powerful and it has lots of other features not explained here. To mention just one of them, you can create differential reports based on two situations: before and after your improvements.

For more information about FunkLoad visit the project's website at

See also

  • Using Selenium functional tests
  • Zope Functional testing

Further resources on this subject:





You've been reading an excerpt of:

Plone 3 Products Development Cookbook

Explore Title
comments powered by Disqus