August 2008

Zend_Db_Table time overhead - about 25%

Here I will represent the results of a test, that I ran recently to determine how much time overhead does Zend_Db_Table brings to typical web application. The details follow, but the rough results are that it took about 25% more time for the test cases with Zend_Db_Table to run compared to using directly Zend_Db_Adapter.

My intend was to test this in as close to live enviroment as possible, so the test case that I ran was standart MVC architecture. We have some overhead also from the setup of the front controller and the dispatch process, loading configuration files, setting up Zend_Translate, loading some simple front controller plugin. The only thing that I have disabled is the layout and the viewRenderer action helpers by running

$this->_helper->layout->disableLayout();
$this->_helper->viewRenderer->setNoRender();

in the init() function of my test controller.

I’m running this with PHP 5.2.6 with APC, Zend Framework 1.5.2, MySQL 5.0.67, MySQLi DB connection adapter.

I have the following DB structure:
- table `users` with standart fields for this - username, password, email address, birthday, real name etc, filled in with 2000 users
- table `posts` with fields: id (primary key, autoincrement), userId (int), title (varchar 64), content (text) with index on userId column - filled in with 20000 posts with random generated text and random generated titles. Text is between 250 and 3000 bytes long.
- table `tags` with fields: id (primary key, autoincrement), tag (varchar 32) with index on tag column. Here we have 35000 tags, each tag is md5 hash of some value, so it is 32 bytes long.
- table `tag2posts` which links the two tables - tags and posts. So we have many-to-many relationship between these two. Fields: tagId (int) and postId (int). Here the primary key is on two columns (tagId, postId), and we have extra index on postId column, because we will search “give me posts for this tag” queries. Each post is linked with between 3 and 7 tags. The size of this table is 99896 rows.

My test case does the following actions:
1) Generates name of a test user. All test users have usernames in the form (test_xxx) where xxx is number between 1 and 2000. Then loads all the info about this user.
2) Loads last 5 post titles from the selected user.
3) Random selects one of these posts and loads it - with the text and all linked tags id. (Without loading the actual tag text, as I found later, but this is not crucial for the result of all this)
4) Random selectes one of the tags for this post and loads the titles of the last 20 posts tagged with it.
5) Prints these titles on the screen

I ran the test in 4 different variants. These 4 variants proved to distinguish one from another in the time they took to run.
These 4 variants are:
1) using Zend_Db_Table without metadata cache
2) using Zend_Db_Table with APC as metadata cache
3) writing the SQL queries directly in the controller, using direct Zend_Db_Adapter_Mysqli
4) writing the SQL queries in Model classes, using direct Zend_Db_Adapter_Mysqli

Fastest method is the 3rd one - where we write the SQL queries directly in the controller. This is not good practice and is error prone. The second place with only small difference is for the 4th method - where we hide the queries in Model classes, but do not use Zend_Db_Table. As for the last 2 places - the performance here drops significantly. When we use APC as metadata cache for Zend_Db_Table compared to the case when we do not use metadata cache at all the difference is about 10%.

I ran several times the benchmark about each of the methods, removed the slowest runs and averaged the other numbers to get the final results. Here are the exact numbers:

I tested with level of concurency 5 with 100 requests (ab -c 5 -n 100)
Requests per second:
method 3 - writing the SQL queries directly in the controller, using direct Zend_Db_Adapter_Mysqli: 12.93 rps
method 4 - writing the SQL queries in Model classes, using direct Zend_Db_Adapter_Mysqli: 12.66 rps
method 2 - using Zend_Db_Table with APC as metadata cache: 10.2 rps
method 1 - using Zend_Db_Table without metadata cache 9.2 rps

here is the code of the test controller class:

< ?php
 
class TestController extends Zend_Controller_Action 
{
    /**
     * DB Connection
     *
     * @var Zend_Db_Adapter_Abstract
     */
    protected $db;
 
    protected $cache;
 
    public function init()
    {
        $this->db = Zend_Registry::get('db');
        $this->_helper->layout->disableLayout();
        $this->_helper->viewRenderer->setNoRender();
 
        //setup cache for DB metadata:
 
        // First, set up the Cache
        $frontendOptions = array(
            'automatic_serialization' => true
            );
 
        $backendOptions  = array();
 
        $this->cache = Zend_Cache::factory('Core', 'APC', $frontendOptions, $backendOptions);
        //$this->cache->setOption('caching', false);
    }
 
	public function indexAction()
    {
 
    }
 
    public function generatetestdbAction()
    {
        //$this->insertTags();
        //$this->insertUsers();
        //$this->insertPosts();
        //$this->linkPostsToTags();
    }
 
    public function runtestcaseAction()
    {
//1) Generates name of a test user. All test users have usernames in the form (test_xxx) where xxx is number between 1 and 2000. Then loads all the info about this user.
//2) Loads last 5 post titles from the selected user. 
//3) Random selects one of these posts and loads it - with the text and all linked tags id. (Without loading the actual tag text, as I found later, but this is not crucial for the result of all this)
//4) Random selectes one of the tags for this post and loads the titles of the last 20 posts tagged with it. 
//5) Prints these titles on the screen
//        
        $mode = $this->getRequest()->getParam('mode');
 
        if (!isset($mode)) $mode = 1;
 
        $startUser = 'test_' . mt_rand(1, 2000);
 
        if ($mode == 1)
        {
            //mode 1 is with Zend_Db_Table
            //$users = new Users();
            $users = new Users(array('metadataCache' => $this->cache));
 
            $u = $users->selectUserByUsername($startUser);
            if (!$u) throw new My_Exception('not such start user');
 
            //load last 5 post titles for $u
            //$posts = new Posts();
            $posts = new Posts(array('metadataCache' => $this->cache));
 
            $select = $posts->select();
            $select->from($posts, array('id', 'title'))
                //->where($posts->getAdapter()->quoteInto('userId = ?', $u->id))
                ->where('userId = ?', $u->id)
                ->order('id DESC')
                ->limit(5);
 
            $rows = $posts->fetchAll($select);
 
            //load full info for random post of these:
            $cnt = $rows->count();
            $rows->seek(mt_rand(0, $cnt-1));
            $p = $rows->current();
            $p = $posts->find($p->id);            
            $p = $p->current();                                    
 
            //$t2p = new Tag2Posts();
            $t2p = new Tag2Posts(array('metadataCache' => $this->cache));
            $select = $t2p->select();
            //$select->where($t2p->getAdapter()->quoteInto('postId = ?', $p->id));
            $select->where('postId = ?', $p->id);
            $rows = $t2p->fetchAll($select);
 
            $cnt = $rows->count();
            $rows->seek(mt_rand(0, $cnt-1));
            $tag = $rows->current();
            $tagId = $tag->tagId;
 
            $select = $posts->select();
            $select->from($posts, array('id', 'title'))
                ->join('tag2posts', 'tag2posts.postId=posts.id', array())
                ->where('tag2posts.tagId = ?', $tagId)
                ->order('id DESC')
                ->limit(20);
 
            $rows = $posts->fetchAll($select);
 
            foreach ($rows as $r)
            {
                echo $r->title . "<br />";
            }
        }
        else if ($mode == 2)
        {
            $stmt = $this->db->query("SELECT * FROM users WHERE username = ?", $startUser);
            $u = $stmt->fetch();            
            if (!$u) throw new My_Exception('not such start user');
 
            //load last 5 post titles for $u
            $stmt = $this->db->query("SELECT id, title FROM posts WHERE userId = ? ORDER BY id DESC LIMIT 5", $u['id']);
            $rows = $stmt->fetchAll();                
 
 
            //load full info for random post of these:            
            $key = array_rand($rows);
            $p = $rows[$key];
            $stmt = $this->db->query("SELECT * FROM posts WHERE id = ?", $p['id']);
            $p = $stmt->fetch();                                                
 
            $stmt = $this->db->query("SELECT * FROM tag2posts WHERE postId = ?", $p['id']);
            $rows = $stmt->fetchAll();
 
            $key = array_rand($rows);
            $tag = $rows[$key];
            $tagId = $tag['tagId'];
 
            $stmt = $this->db->query("SELECT id, title FROM posts JOIN tag2posts t2p ON t2p.postId=posts.id " . 
                "WHERE t2p.tagId = ? ORDER BY id DESC LIMIT 20", $tagId);
 
            $rows = $stmt->fetchAll();
 
            foreach ($rows as $r)
            {
                echo $r['title'] . "<br />";
            }                        
        }
        else
        {
            $users = new Users2();
            $u = $users->selectUserByUsername($startUser);
            if (!$u) throw new My_Exception('not such start user');
 
            //load last 5 post titles for $u
            $posts = new Posts2();
            $rows = $posts->getLastPostsForUser($u['id'], 5);
 
            //load full info for random post of these:
            $key = array_rand($rows);
            $p = $rows[$key];
            $p = $posts->getPostById($p['id']);
 
            $tags = $posts->getTagsForPost($p['id']);
            $key = array_rand($tags);
            $tag = $tags[$key];
 
            $rows = $posts->getLastPostsForTag($tag['tagId'], 20); 
 
            foreach ($rows as $r)
            {
                echo $r['title'] . "<br />";
            }
        }
    }
 
    protected function insertTags() {
        //generate 35000 tags
        $this->db->query("TRUNCATE TABLE tags");
        $stmt = $this->db->prepare("INSERT INTO tags SET tag = ?");
        $stmt->bindParam(1, $newTag);
        for ($i = 1; $i < = 35000; $i++)
        {
            $newTag = md5(microtime());
            $stmt->execute();                        
        }
 
        echo '35000 tags inserted';
    }
 
    protected function insertUsers()
    {
        //insert 2000 users
        $this->db->query("DELETE FROM users WHERE username LIKE ?", array(1=>'test_%'));
        $stmt = $this->db->prepare("INSERT INTO users SET username = ?, password = ?, email = ?, active = 1");
        $stmt->bindParam(1, $username);
        $stmt->bindParam(2, $password);
        $stmt->bindParam(3, $email);
 
        $password = Users::computePasswordHash('123456');
        for ($i = 1; $i < = 2000; $i++)
        {
            $username = 'test_'.$i;
            $email = 'test_'.$i.'@example.com';
 
            $stmt->execute();
        }
 
        echo '2000 users inserted';
    }
 
    protected function insertPosts()
    {
        //insert 20000 posts 
        //$this->db->query("TRUNCATE TABLE posts");
        //$this->db->query("TRUNCATE TABLE tag2posts");
 
        $stmt = $this->db->prepare("INSERT INTO posts SET userId = ?, title = ?, content = ?");
        $stmt->bindParam(1, $userId);
        $stmt->bindParam(2, $title);
        $stmt->bindParam(3, $content);
 
        //@todo: increase your time limit, because this one may take some time
        for ($i = 1; $i < = 20000; $i++)
        {
            $userId = mt_rand(16, 2015);
            $title = $this->getRandomText(rand(30, 60));
            $content = $this->getRandomText(rand(250, 3000));
 
            $stmt->execute();
        }
 
        echo '20000 texts inserted';                
    }
 
    protected function linkPostsToTags()
    {
        //link each post to about 5 tags
        //posts are from 1 to 20000
        //tags are from 1 to 35000
 
        $stmt = $this->db->prepare("INSERT INTO tag2posts SET tagId = ?, postId = ?");
        $stmt->bindParam(1, $tagId);
        $stmt->bindParam(2, $postId);
 
        for ($i = 1; $i < = 20000; $i++)
        {
            $postId = $i;
 
            //@todo: better testing for duplicate tag id should be implemented. 
            $numTags = mt_rand(3, 7);            
            for ($j = 1; $j <= $numTags; $j++)
            {
                do
                {
                    $newTagId = mt_rand(1, 35000);                
                }
                while ($newTagId == $tagId);
                $tagId = $newTagId;
 
                $tagId = mt_rand(1, 35000);
                $stmt->execute();
            }            
        }
 
        echo 'tags links inserted';
 
 
    }
 
    public static function getRandomText($numCharacters)
    {
        //algorithm: generate words (2 to 15 characters long) then space until $numCharacters is reached
        $text = '';
        while ($numCharacters > 0)
        {
            $currentWord = '';
 
            $charset = "abcdefghijklmnopqrstuvwxyz";
            $chl = strlen($charset)-1;
 
            $length = mt_rand(2, 15);
            for ($i=0; $i< $length; $i++) $currentWord .= $charset[(mt_rand(0,$chl))];            
 
            $text .= $currentWord;
            $text .= ' ';
            $numCharacters -= ($length + 1);
        }
 
        return $text;
    }
 
 
 
}

Classes Users, Posts and Tag2Posts are very simple, they just extends Zend_Db_Table_Abstract and define the $_name property. For variant 4 - here are example classes Users2 and Posts2:

< ?php
 
class Users2
{
    protected $db;
 
    public function __construct()
    {
        $this->db = Zend_Registry::get('db');
    }
 
    public function selectUserByUsername($username)
    {
        $stmt = $this->db->query("SELECT * FROM users WHERE username = ?", $username);
        $u = $stmt->fetch();            
        return $u;
    }
}
< ?php
 
class Posts2 
{
    protected $db;
 
    public function __construct()
    {
        $this->db = Zend_Registry::get('db');
    }
 
 
    public function getPostById($postId)
    {
        $stmt = $this->db->query("SELECT * FROM posts WHERE id = ?", $postId);
        $p = $stmt->fetch();
 
        return $p;
    }
 
    public function getTagsForPost($postId)
    {
        $stmt = $this->db->query("SELECT * FROM tag2posts WHERE postId = ?", $postId);
        $rows = $stmt->fetchAll();
        return $rows;
    }
 
    public function getLastPostsForUser($userId, $num)
    {
        $stmt = $this->db->query("SELECT id, title FROM posts WHERE userId = ? ORDER BY id DESC LIMIT 5", $userId);
        $rows = $stmt->fetchAll();
        return $rows;
    }
 
    public function getLastPostsForTag($tagId, $num)
    {
        $num = (int) $num;
        $stmt = $this->db->query("SELECT id, title FROM posts JOIN tag2posts t2p ON t2p.postId=posts.id " . 
                "WHERE t2p.tagId = ? ORDER BY id DESC LIMIT $num", $tagId);
 
        $rows = $stmt->fetchAll();
        return $rows;
    }
}

conclusion: Zend_Db_Table is not good for heavy loaded web applications. My personal choice is to go with something like variant 4. The time overhead here is minimal and the code in the controller is most readable.

Zend Framework

Comments (5)

Permalink

Part 6 - (Very) Simple ACL plugin and simple AJAX with JQuery

In this short part I’ll show you very simple approach for securing your admin area. And when I say very simple I really mean it. After this I will show you how with the use of JQuery we can easy add AJAX functionality to our application. I know about the integration with Dojo in the new releases of Zend Framework, but I really prefer JQuery to Dojo in my work, and I’m sure in the manual there will be nice examples about the usage of Dojo, so if you are interested to work with JQuery - you can see how simple you can use it with Zend Framework. If you won’t use JQuery - just skip the second half of this part - you won’t lose anything vital to the project.

So first we will add one more module to our application next to the default one - the admin module. Under the application/modules/ directory we create new subdirectory - admin. Then we add 2 subdirectories under application/modules/admin/ - controllers and views. We add empty IndexController.php file under application/modules/admin/controllers - we will add code here shortly. Then under the application/modules/admin/views/ we add 3 subfolders - filters, helpers and scripts. Under application/modules/admin/views/scripts we add one subfolder - index - this will hold the views for our Index Controller.

If you are lost with so many directory creations - here is what I have now :)

Directory structure for admin module

We will leave the meaningfull code in this admin module until the final part of this series, what we want now is to have placeholder text only. Before we have actual administration tools we want this area secure. So we add only one action - the default one. IndexController.php is really simple:

application/modules/admin/controllers/IndexController.php:

< ?php
 
class Admin_IndexController extends Zend_Controller_Action 
{
    public function indexAction()
    {
 
    }
}

And we will need just as simple view script:

application/modules/admin/views/scripts/index/index.phtml:

admin stuff here

Ok, now our brand new admin module is available at “http://localhost/admin” assuming you are developing on localhost with port 80 :). You can try it and you will see that you can open it even if you are not logged in. Lets change this. Recall that in our users DB table we have isAdmin field. I hope that you have some registered users by now, if you don’t have - please register one now and activate him (or ‘her’ if you prefer, but I will use ‘him’ because it is more natural). Now open your phpMyAdmin or whatever you use for DB administration and find the row in your users table for this user - this will be our administrator :). Change the isAdmin column in this row to be 1. If your user have username ‘admin’ you can use this:

UPDATE `users` SET `isAdmin`=1 WHERE `username`='admin';

Now we will write simple front controller plugin, which will make sure that only users with isAdmin flag set to 1 can access our admin module.

/application/Lib/Controller/Plugin/ACL.php

< ?php
 
class Lib_Controller_Plugin_ACL extends Zend_Controller_Plugin_Abstract 
{
    /**
     * Called before an action is dispatched by Zend_Controller_Dispatcher.
     *
     * This callback allows for proxy or filter behavior.  By altering the
     * request and resetting its dispatched flag (via
     * {@link Zend_Controller_Request_Abstract::setDispatched() setDispatched(false)}),
     * the current action may be skipped.
     * 
     * In this version we have only one rule - for access to 'admin' module we require 'isAdmin'
     * flag of the user to be set to true (this is very simple, but for our current needs is enough)
     *
     * @param  Zend_Controller_Request_Abstract $request
     * @return void
     */
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        $module = $request->getModuleName();
        if ($module == 'admin' && (!Zend_Registry::get('defSession')->currentUser || !Zend_Registry::get('defSession')->currentUser->isAdmin))
        {
            // redirect to index if do not have access 
            // (possible to redirect to some 'access denied' page if needed)
            $request->setModuleName('default')->setControllerName('index')->setActionName('index');            
        }
    }
}

Thats all - on preDispatch (that is before each action is executed) we check the request object for the module name. If it is ‘admin’ and the current user is not admin - we replace the module name, controller name and action name with default/index/index. Better solution is to have special “access denied” page, but I’ll keep the things here as simple as possible. Now we just need to register this plugin with the front controller and we are done:

(in bootstrap.php):

$frontController->registerPlugin(new Lib_Controller_Plugin_ACL());

That’s all - you can open http://localhost/admin (again assuming that the project is on your localhost with port 80) and see the “admin stuff here” message only if you are logged in with user, who have isAdmin flag set to 1.

——————————****************************—————————

:) To part 2 of this part 6.

If you don’t want to use JQuery you can skip this. What is JQuery? A fast, concise, library that simplifies how to traverse HTML documents, handle events, perform animations, and add AJAX. At least they say it is this :). We want to use JQuery to add some form validation in the registration form.

So we download JQuery from JQuery site. Then put the jquery.js file in /public/js directory. Then create subdirectory there - /public/js/registration, here we will place our code for form validation. Actually here it is:
/public/js/register/form.js

$(document).ready(function() {
    $("#username").bind('keyup', usernameCheck);
    $("#email").bind('keyup', emailCheck);
 
});
 
function usernameCheck()
{
    var username = $("#username").val();
 
    if (username.length < 3)
    {
        $("#username_help").html('This field should be at least 3 characters long');
    }
    else if (username.length > 32)
    {
        $("#username_help").html('This field should be maximum 32 characters long');
    }
    else
    {
	    postObject = new Object;
	    postObject.username = username;
	    $.post('/user/checkUsernameAjax/', postObject, 
	      function(data){
	        if (data.valid)
	        {
	            $("#username_help").html('');            
	        }
	        else
	        {
	            $("#username_help").html('This username is already taken');
	        }
	      }, "json" );
	 }
}
 
function emailCheck()
{
    var email = $("#email").val();
 
    {
        postObject = new Object;
        postObject.email = email;
        $.post('/user/checkEmailAjax/', postObject, 
          function(data){
            if (data.valid)
            {
                $("#email_help").html('');            
            }
            else
            {
                $("#email_help").html('This email is already registered');
            }
          }, "json" );
     }
}

what we do here is to add 2 event listeners - to username and email fields. When the user types something there we will check if it is valid. The code, written with JQuery is so easy to read, that I feel stupid to explain lines of code such

    if (username.length < 3)
    {
        $("#username_help").html('This field should be at least 3 characters long');
    }
    else if (username.length > 32)
    {
        $("#username_help").html('This field should be maximum 32 characters long');
    }

What is more interesting is the way we make AJAX (asynchronous) call to our application to check if the username (or the email) is not already taken. I use POST request here to prevent caching of the response, because if I use GET with something like “http://localhost/user/checkUsernameAjax/johny” to check if “johny” is available, then some cache machines, proxy servers or even your own browser will cache the response that johny is available, and after 3 minutes when some other johny actually register himself in the site, you will still get the message that “johny” is available if you check. We want to prevent this so use POST. Other way is to use GET with URL like “http://localhost/user/checkUsernameAjax/johny/someRandomNumberHere”. Anyway, as there are many ways to accomplish something we just pick the POST and go ahead. If you are new to JQuery, the sintax of $.post function may seems a little wired, but using anonymous functions is very common language structure in JQuery, so better get used to it. Of course we could have named function as callback here - which checks if data.valid is true or false, and for some more complex functions may be it is better to have named function. For our single IF-ELSE block function this approach is best.

What is missing from the picture is the “/user/checkUsernameAjax/” and “/user/checkEmailAjax/” actions in the user action controller. If you expect something very very complex now, think again :)

in /application/modules/default/controllers/UserController.php:

    public function checkusernameajaxAction()
    {
        $this->_helper->layout->disableLayout();
 
        $username = $this->getRequest()->getParam('username');
        $usersTable = new Users();
        $select = $usersTable->select();
        $select->where('username = ?', $username);
        $rows = $usersTable->fetchAll($select);
        if ($rows->count())
            $valid = false;
        else
            $valid = true;
 
        $this->_helper->viewRenderer->setNoRender(); 
        $data = array('valid'=>$valid);
        $json = Zend_Json::encode($data);
        echo $json;
    }
 
    public function checkemailajaxAction()
    {
        $this->_helper->layout->disableLayout();
 
        $email = $this->getRequest()->getParam('email');
        $usersTable = new Users();
        $select = $usersTable->select();
        $select->where('email = ?', $email);
        $rows = $usersTable->fetchAll($select);
        if ($rows->count())
            $valid = false;
        else
            $valid = true;
 
        $this->_helper->viewRenderer->setNoRender(); 
        $data = array('valid'=>$valid);
        $json = Zend_Json::encode($data);
        echo $json;
    }

Here we want to return JSON encoded array with one boolean element - ‘valid’. So 1) we disable layout. 2) get the username or the email value which we have to check if is available 3) run the DB query 4) tell the view renderer action helper to not render any view 5) encode our answer and finally 6) print the answer to the client.

Now we will add ‘username_help’ and ‘email_help’ divs to the HTML markup of the registration form. Remember from our form.js that we write our help messages to this two divs. In the getRegistrationForm function in UserController.php we add this lines where we define the username and email fields:

$username->addDecorator(array('ajaxDiv' => 'HtmlTag'), array('tag'=>'div', 'placement'=>'append', 'id'=>'username_help', 'class'=>'errors'));
$email->addDecorator(array('ajaxDiv' => 'HtmlTag'), array('tag'=>'div', 'placement'=>'append', 'id'=>'email_help', 'class'=>'errors'));

The function is too big to paste here, and these are only 2 new lines, so I’ll better paste the context only:

$username->addValidator($validatorNotEmpty, true)->setRequired(true)->setLabel('Username')
        ->addFilter($filterTrim)
        ->addValidator($validatorAlnum)
        ->addValidator($validatorStringLength)
        ->addValidator($validatorUniqueUsername);                
        $username->addDecorator(array('ajaxDiv' => 'HtmlTag'), array('tag'=>'div', 'placement'=>'append', 'id'=>'username_help', 'class'=>'errors'));
        $form->addElement($username);
$email->addValidator($validatorNotEmpty, true)->setRequired(true)->setLabel('Email Address')
        ->addFilter($filterTrim)
        ->addValidator($validatorEmail)
        ->addValidator($validatorUniqueEmail);
        $email->addDecorator(array('ajaxDiv' => 'HtmlTag'), array('tag'=>'div', 'placement'=>'append', 'id'=>'email_help', 'class'=>'errors'));
        $form->addElement($email);

And finally we need to tell the browser to load jquery.js and form.js files. We will use headScript view helper for this. At the end of the registerAction() function add these two lines:

        $this->view->headScript()->appendFile('/js/jquery.js');
        $this->view->headScript()->appendFile('/js/register/form.js');

and in application/layouts/main.phtml in the head section of the html add this:

< ?php echo $this->headScript(); ?>

Now it should be working.

To say this again - if you are reading this and do not know anything about JQuery, but you are excited how with just 10 lines of javascript code and some very simple php functions we added AJAX functionality to our application - read some more about JScript. What we have done here is really simple, after you understand it you will understand the great power, that you have in your hands.

There. I hope you enjoyed our time together today. You know it seems harder and harder to just sit back and enjoy the finer things in life. Well, until next time…