Dissecting onion FrameworkBench's implementation

In this article I will dissect the onion benchmark as used on the FameworkBench benchmark suite. The version I'm dissecting is at http://bit.ly/18tdbOj. Some bugs were found in the benchmark code when writing this article and these are commented as they are spotted. The version used for the benchmark round 4 is as described in this article, bugs included, with the exception of the new fortunes test, to be used at FrameworkBench round 5, yet to be released.


We will start our journey at main, line 263:
This is the main onion initialization. Onion users can have as many servers as desired, each one is created with onion_new, which receives one parameter that sets options for the server. Here we are using O_POOL for a pool mode: some threads are created, each waiting for a new request and serving the requests as needed. Other options are described at http://bit.ly/18tdTLi.

Next, we are initializing a test_data structure which is necessary to keep track of the database connections. As onion does not have an ORM (yet?), in this example to access the database we are using the MySQL C API, creating a fixed amount of connections. A semaphore is used to prevent the excess of simultaneous requests and a mutex to change the status of the connections if necessary. This simple operation is managed with functions get_connection and free_connection. These two functions need further improvement to avoid a possible congestion at database access.

Next, at 286, we are initializing a onion_dict with the "message" : "Hello world!" data.
onion_dict_add(data.hello,"message","Hello, world"0);
onion_dict_add final parameter decides how you use memory management. Users inform the dictionary if the second and third parameters are to be:
  1. used as straight pointers (no copy),
  2. copied,
  3. automatically deleted (give ownership).
  4. specific type for data, for example OD_DICT to add a dictionary.
Options 1 and 3 are not exclusive. Option 2 implies option 3 on the copied data. Default for option 4 is a text string.

These options are used all through onion to save memory and improve performance.

In the testing code, we are using 0 as flags, which means that the data can be used as passed, and does not need to be copied nor freed.

At 288 we are creating and setting the root handler.
onion_set_root_handler(o, onion_handler_new((void*)&muxer, (void*)&data, NULL));
As requests arrive to onion they are served by this root handler. It can have subhandlers for specific conditions; as the conditions are programmed in the handler itself, there are endless possibilities  as virtual host, ip discrimination, authentication handling, urls based on regex a-la Django (onion_url), or as done here for performance, manual checking of url paths. Check the documentation as the most common options are easily handled and normally user do not have to create their manual muxers, unless performance requires it. This root handler receives a data pointer, to the test_data structure, and does not require any destructor call. For any request it will call the mutex function.

Finally we start the listening, and after it we deinitialize all data. The listening will continue until onion_listen_stop is called, which is called on SIGINT and SIGTERM signals.


The muxer, at 277,  is the first example of a handler function. It receives some user data and then the request and the response.

/// Multiplexes to the proper handler depending on the path.
/// As there is no proper database connection pool, take one connection randomly, and uses it.
onion_connection_status muxer(struct test_data *data, onion_request *req, onion_response *res){
  const char *path=onion_request_get_path(req);
  if (strcmp(path, "")==0)
    return return_json(data->hello, req, res);
  if (strcmp(path, "json")==0)
    return return_json_libjson(NULL, req, res);

We are extracting the current path and comparing it to the known paths, calling the appropiate handler. If the path is empty, calls return_json, if it is json, return_json_libjson, and so on. Finally if none fits, an OCS_INTERNAL_ERROR is returned. Actually it could have been better to return OCS_NOT_PROCESSED, so if there are more handlers in the chain, next can try and so on, and if none at all processed it it would return a 404 not found (customizable). Next version may fix this.


The first implementation of json uses the internal onion_dict_to_json, that just converts a onion_dict to a JSON string. There is no, by the moment, support for lists, integers, nor json to dict. But for small json is more than enough. This has better performance than the libjson version, but its more limited.

We are creating an onion_block, which is a kind of variable length string, to print the json into. Then we are setting the content-type and length, and finally we are writing it into the response:
onion_response_set_header(res, "Content-Type","application/json");
onion_response_set_length(res, size);
onion_response_write(res, onion_block_data(bl), size);

Setting the length was advised as it helps the keep alive; but since a commit on 6 May, if the response is small a delayed header mechanism checks the size before sending anything and the content-length is automatically inserted. This helps to have keep alive even for request with unknown size.

After this the block is freed, and we return the marker that the request has been processed:


This version does as return_json, but uses libjson. It also creates the JSON object everytime it is called, and copies it to a string. Then the setting of header, length, and write is performed in the same way.


This test uses the MySQL C API as indicated before. At line 91:
const char *nqueries_str=onion_request_get_query(req,"queries");
int queries=(nqueries_str) ? atoi(nqueries_str) : 1;
We are getting the GET parameter "queries" and storing it into nqueries_str. If the query does not exist, onion_request_get_query will return NULL, and on next line, depending on this we will convert it to a integer or set it to 1.

On following lines it does the SQL query and create the json object. Then this object is converted to a string and sent as shown on the return_json_libjson example.


For the fortunes example there were quite a lot more requirements, as using a templating system, UTF-8, and HTML escaping. For HTML escaping part code was developed for onion, so now by default the templating system automatically escapes variables. There is no option, right now, to not escape variables.

Onion's templating system is otemplate. With it you can write normal HTML (or any text based code) with specific tags to allow inserting data from varaibles, looping, internationalization (i18n) and extending base templates. It is heavily based on Django's, and normally you will not note the difference on the template side, if use is kept to basics. Looking at the Makefile we can see the compilation of the template to C source code:
base_html.c: otemplate base.html 
onion/build/tools/otemplate/otemplate base.html base_html.c
The generated source file have several functions. For this example I decided to define the function I needed manually at line 120:
onion_connection_status fortunes_html_template(onion_dict *context, onion_request *req, onion_response *res);
This function receives a context dictionary and the normal request and response objects. It can be called at the end of your view function, and the context will be freed automatically. On the function itself we prepare the onion_dict, first creating a temporary struct for the data, with dynamic resizing, and filled using MySQL C API. Then we sort it as requested by the benchmark requirements, and prepare the dict itself. For the dict the OD_DICT flag is used to embed sub dicts. As onion_dict does not support arrays, the for loops on the template itself is on the values of the dicts, on the order set by the keys. For this reason we have to insert the values as { "00000001" : { "id" : "1", "message" : "...." } }, and so on.

When everything is ready we just call the fortunes_html_template; when it returns the dict will be automatically deallocated.

On the template side we use a base.html, with blocks and a title variable. This is extended at fortunes.html, where we loop over the fortunes and print them as requested:

{% for f in fortunes %}
{% endfor %}

Closing up

This is a dissection of the onion implementation of the benchmark used a ThemEmpower's BenchmarkTest, and as can be seen onion tries to help where possible to make a C web application as easy as possible to develop. Real power on onion is not performance, but ease of use. There are some parts that might need some more work, as the templating system, but just at is is now it creates an ease of use unseen before when creating HTTP servers with C. Also performance does not hurt, but that's a problem of onion's internals, not of the interface.

If there is some reader that would like some specifics to be more described, or have some ideas on how it might be better implemented, on this test or on onion itself, please leave your comments.

No comments:

Post a Comment