Interchange Guides: Optimization

Mike Heins

This documentation is free; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.

It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

Abstract


Table of Contents

Software optimizations
Interchange

Software optimizations

Interchange

General-purpose benchmarking

One of the most simple and straightforward methods to check whether the code is able to complete a task within a reasonable time is benchmarking. For the purpose, Interchange offers the benchmark tag which is found in the eg/usertag/ directory of the Interchange tarball distribution.

The benchmark reference page contains all the relevant installation and usage notes.

Optimizing lists

Interchange has powerful capabilities (such as searching) that allow you to produce lists of items for use in category lists, product lists, indexes, and other navigation tools or data reports.

These are a two-edged sword, though. Lists of hundreds or thousands of entries can be returned, and techniques that work well displaying only a few items may slow to a crawl when a large list is returned.

In general, when you are displaying only one item (such as on a flypage) or a small list (such as shopping cart contents), you can be pretty carefree in your use of ITL tags. When there are thousands of items, though, you cannot; each ITL tag requires parsing and argument building, and all complex tests or embedded Perl blocks cause the Safe module to evaluate code.

The Safe module is pretty fast considering what it does, but it can only generate a few thousand instances per second even on a fast system. And the ITL tag parser can likewise only parse thousands of tags per CPU second.

What to do? You want to provide complex conditional tests but you don't want your system to slow to a crawl. Luckily, there are techniques which can speed up complex lists by orders of magnitude.

[PREFIX-tag]

[PREFIX-tag] constructs are the fastest way to retrieve loop data. Let's say we want to find all our products (search in all ProductFiles databases) and display descriptions of all the products found:

[loop prefix=foo search="ra=yes"]

  [foo-data products description]
  [comment]is slightly faster than   [/comment]

  [foo-field description]
  [comment]which is MUCH faster than [/comment]

  [data products description [foo-code]]
  [comment]which is faster than      [/comment]

  [data table=products column=description key="[foo-code]"]

[/loop]

The loop tags are interpreted by means of fast regular expression scans of the loop container text, and fetch an entire row of data in one query.

The data ITL tag interpretation is delayed until after the loop is finished, whereby the ITL tag parser must find the tag, build a parameter list, and then fetch the data with a separate query.

If there are repeated references to the same field in the loop, the speedup can be 10x or more.

Pre-fetch data

The mv_return_fields variable (otherwise known as the "rf" parameter in one-click terminology) defines a comma-separated list of fields you want returned from a search. This, in effect, kind of pre-fetches the data you want to use within a loop.

Once the records are returned, the fields can be accessed using the [PREFIX-param field] syntax. The fields can also be referenced using [PREFIX-pos N], where the N represents the ordinal position (starting from 0) in the field list.

That said, the following are equivalent in effect but the second variant is much, much faster:

<pre>

Benchmark loop-field list: [benchmark start=1]
  <!-- [loop search="ra=yes/st=db"]
       [loop-code] price: [loop-field price] [/loop] -->
       TIME: [benchmark]

Benchmark loop-param list: [benchmark start=1]
  <!-- [loop search="ra=yes/st=db/rf=sku,price"]
       [loop-code] price: [loop-param price] [/loop] -->
       TIME: [benchmark]

</pre>

Row counting and display

[PREFIX-alternate N] can be used for row counting and display.

A common need when building tables is to conditionally close the table row or data containers. I see a lot of code that manually inserts new rows every three columns:

[loop search="ra=yes"]
  [calc] return '<tr>' if [loop-increment] == 1; return[/calc]
  [calc] return '' if [loop-increment] % 3; return '</tr>' [/calc]
[/loop]

Much faster, by a few orders of magnitude than the above, is:

[loop search="ra=yes"]
  [loop-change 1][condition]1[/condition]<tr>[/loop-change 1]
  [loop-alternate 3]</tr>[/loop-alternate]
[/loop]

If you think you need to close the final row by checking the final count, look at this complete example done the right way:

[loop search="ra=yes"]
  [on-match]
    <table>
    <tr>
  [/on-match]

  [list]
    <td>[loop-code]</td>
    [loop-alternate 3]</tr><tr>[/loop-alternate]
  [/list]

  [on-match]
    </tr>
    </table>
  [/on-match]

  [no-match]
    No match, sorry.
  [/no-match]
[/loop]

The above is a hundred times faster than anything you can build with multiple calc tags.

Use [PREFIX-calc] instead of [calc] or [perl]

Using [PREFIX-calc], you can execute the same code as with calc, but with two benefits: you will not trigger ITL parsing, and the code will be executed during the loop instead of after it.

The [PREFIX-calc] object has complete access to all normal embedded Perl objects like $Values, $Carts, $Tag, and such. If you want to access data tables from within the loop (such as products or pricing), just call the following above the loop:

[perl tables="products pricing" /]

ADVANCED: Precompile and execute

For repetitive routines, you can achieve a considerable savings in CPU by pre-compiling your embedded Perl code. The precompilation can occur either once at catalog configuration time, or once at time of list execution.

When you compile routines at the time of the list execution (using [item-sub NAME] ... CODE ... [/item-sub]), only one Safe evaluation will be done, and every time the [loop-exec NAME] is called, it will be a direct call to the routine. This can be 10 times or more faster than separate calc calls, or 5 times faster than separate [PREFIX-calc calls. Here's an example:

[benchmark start=1]

loop-calc:
<!--
  [loop search="st=db/fi=country/ra=yes/ml=1000"]
    [loop-calc]
      my $code = q{[loop-code]};
      return "code '$code' reversed is " . reverse($code);
    [/loop-calc]
  [/loop]
-->

[benchmark]

<p>
[benchmark start=1]

loop-sub and loop-exec:
<!--
  [loop search="st=db/fi=country/ra=yes/ml=1000"]
    [loop-sub country_compare]
      my $code = shift;
      return "code '$code' reversed is " . reverse($code);
    [/loop-sub]
    [loop-exec country_compare][loop-code][/loop-exec]
  [/loop]
-->

[benchmark]

ADVANCED: Execute and save with [query ...]

You can run [query arrayref=KEYNAME sql="... SQL ..."], which saves the results of the search/query in a Perl reference. It is then available in $Tmp->{KEYNAME}.

This is the fastest possible method to display a list. Observe:

[set waiting_for]os28004[/set]

[benchmark start=1] Query plus embedded Perl
<!--
  [query arrayref=myref sql="select sku,price,description from products" /]
  
  [perl]
    # Get the query results, has multiple fields
    my $ary = $Tmp->{myref};
    my $out = '';
    
    foreach $line (@$ary) {
      my ($sku, $price, $desc) = @$line;
      if($sku eq $Scratch->{waiting_for}) {
        $out .= "We were waiting for this one!!!!\n";
      }
      $out .= "sku: $sku price: $price description: $desc\n";
    }
    return $out;
  [/perl]
-->

TIME: [benchmark]
<p>

[benchmark start=1] Just query

<!--
  [query list=1 sql="select sku,price,description from products"]
  
  [if scratch waiting_for eq '[sql-code]']
    We were waiting for this one!!!!
  [/if]
  
  sku: [sql-code]
  price: [sql-param price]
  desc: [sql-param description]  
  [/query]
-->

TIME: [benchmark]

Take advantage of "implicit" TRUE and FALSE values

Consider these two snippets:

[if scratch KEY]
... do something ...
[/if]

and:

[if scratch KEY == '1']
... do something ...
[/if]

The first variant does not require Perl evaluation. It simply checks to see if the value is blank or 0, and assumes TRUE if it is anything but.

Of course, this requires your code to return blank or value 0 for FALSE results (instead of say, "No" or " "), but then we can talk about a 20-35% speed-up.

Here's a sample program to time the results:

Overhead: 

[benchmark start=1]

<!--
	[loop search="ra=yes"]
		[set cert][loop-field gift_cert][/set]
	[/loop]
-->

[benchmark]
<p>


"if scratch cert": 
[benchmark start=1]

<!--
[loop search="ra=yes"]
	[set cert][loop-field gift_cert][/set]
	[loop-code] [if scratch cert] YES [else] NO [/else][/if]
	[loop-code] [if scratch cert] YES [else] NO [/else][/if]
	[loop-code] [if scratch cert] YES [else] NO [/else][/if]
	[loop-code] [if scratch cert] YES [else] NO [/else][/if]
	[loop-code] [if scratch cert] YES [else] NO [/else][/if]
[/loop]
-->

[benchmark]
<p>


"if scratch cert == 1": 
[benchmark start=1]

<!--
[loop search="ra=yes"]
[set cert][loop-field gift_cert][/set]
[loop-code] [if scratch cert == 1] YES [else] NO [/else][/if]
[loop-code] [if scratch cert == 1] YES [else] NO [/else][/if]
[loop-code] [if scratch cert == 1] YES [else] NO [/else][/if]
[loop-code] [if scratch cert == 1] YES [else] NO [/else][/if]
[loop-code] [if scratch cert == 1] YES [else] NO [/else][/if]
[/loop]
-->

[benchmark]
<p>

[page @@MV_PAGE@@]Run again</a>

Interpolation and reparsing

Avoid interpolate=1 and reparse=1 whenever possible. A separate tag parser must be spawned every time you do this. Many times people use this without needing it.

Session variables

Avoid saving large values to scratch space, as these will be written to the users session. If you need them only for the current page, use tmpn and tmp instead of set and seti (temporary variables are automatically deleted at the end of current page processing - before the user's session is saved).

You can also retrieve values using [scratchd VARIABLE_NAME] to return the contents and delete them from the session at the same time.

DocBook!Interchange!