CClientScript performance optimizaion puzzle.

Hello friends,

While reading sources of CCLientScript.php I've end up with a clue that an algorythm used in the code to inject header script/styles and other stuff wasn't optimal. Here's a quote from source:

		if($html!=='')


		{


			$count=0;


			$output=preg_replace('/(<titleb[^>]*>|<\/heads*>)/is','<###head###>$1',$output,1,$count);


			if($count)


				$output=str_replace('<###head###>',$html,$output);


			else


				$output=$html.$output;


		}

You see, first we have preg_replace which ends string searching right after first match - thats okay. Next we have full-text str_replace which is faster than PCRE but searches by the end of the string. Also, we have body-begin injection and body-end injection which do exactly the same. So, to be exact, we have:

preg_replace * 3 (slower but breaks in time) + str_replace * 3 (faster, till the end). You know, I always tend to enhance things to achieve even better results and decided to write an optimized algorythm. Here is the source of my version of TClientScript:

class TClientScript extends CClientScript


{


    public function render(&$output)


    {


        $this->renderCoreScripts();


        


        if (!empty($this->scriptMap))


            $this->remapScripts();


        


        $output = preg_replace_callback


        (


            '/<(head|bodyb[^>]*|\/bodys*)>r?n?/is',


            array($this, 'replace'), $output, 3


        );


    }


    


    protected function replace($matches)


    {


        $output = '';


        if (strpos(strtolower($matches[0]), '<head') === 0)


        {


            $this->renderHead($output);


            return $matches[0] . $output;


        }


        if (strpos(strtolower($matches[0]), '<body') === 0 && $this->enableJavaScript)


        {


            $this->renderBodyBegin($output);


            return $matches[0] . $output;


        }


        if (strpos(strtolower($matches[0]), '</body') === 0 && $this->enableJavaScript)


        {


            $this->renderBodyEnd($output);


            return $output . $matches[0];


        }


    }


}

As you can see, I'm using preg_replace_callback to optimize number of cycles used by PHP engine. It still calls parent function to generate replacement blocks and preg_replace is still called. However, it's receiving an empty string. So, I decided to ignore this and keep things simple.

After override was ready, I started to test things. Well, what can I say. With a 5 KB page (layout + view) it didn't show any performance outcome. So, I have placed a dummy content into layout file and increased an output size up to 675 kb. What my surprise was when it appeared that my optimized algorythm was only spoiling things. Results are:

"Optimized": 7 MB, 1 SEC;

"Original": 7 MB, 0.55 SEC;

We always value what we do with care. However, this doesn't mean what we do makes things better… Once again, I am convinced Qiang is doing a great job and knows exactly what he does.

Now, could anyone explain me WHY MY OVERRIDE DO NOT WORK?! :)

It could be because your method uses 6 strpos calls in total. With large string to search in, this might be expensive. I'm not quite sure.

strpos only searches within a match which is up to 20 bytes at most.

When searching for </body, it will use 3 strpos.

True. Total 1 + 2 + 3 = 6 str_pos calls. Also $this->enableJavaScript check should go first. Will fix and test then…

I have found what was wrong.

First, it was impossible to see performance difference on a small file.

Second, I used my environment profiler which wasn't that good I was thinking.

I have tried one more algorythm which was using preg_replace_callback which was substituting shortcuts (<###head###> and etc.) and strtr function after that. It was 2.5 times slower than YII itself.

Finally, I have come up with a solution which will boost this replacement functionality 3+ times comparing to YII implementation. Of course, this only matters for high-traffic websites (my case). Here is the source:

class TClientScript extends CClientScript


{    


    public function render(&$output)


    {


        $time = microtime(true);


        $byte = memory_get_usage(true);


        


        $this->renderCoreScripts();


        


        if (!empty($this->scriptMap))


            $this->remapScripts();


        


        $output = preg_replace_callback


        (


            '/<(head|bodyb[^>]*|\/bodys*)>r?n?/is',


            array($this, 'replace'), $output, 3


        );


        


        print('<script>alert("Time: ' . (microtime(true) - $time) . ' rnByte: ' . (memory_get_usage(true) - $byte) . '");</script>');


        exit();


    }


    


    protected function replace($matches)


    {


        $output = '';


        $match = strtolower(substr($matches[0], 0, 5));


        if ($match == '<head')


        {


            $this->renderHead($output);


            return $matches[0] . $output;


        }


        if ($this->enableJavaScript && $match == '<body')


        {


            $this->renderBodyBegin($output);


            return $$matches[0] . $output;


        }


        if ($this->enableJavaScript && $match == '</bod')


        {


            $this->renderBodyEnd($output);


            return $output . $matches[0];


        }


    }


}

It's ticket #364. Recommend to include into YII.