memcache profiler storage support added

fix CS

fix CS + remove unneeded else

add documentation, change protected methods as private

rename var

throw exception for invalid name, index fix

memcache profiler storage support added, fix CS and minor bugs

fix CS

removed unneeded else

- memcached support added
- improved performance (serialization, index)

updated code to last version of Profiler
This commit is contained in:
Andrej Hudec 2011-12-01 22:18:49 +01:00
parent 46b00b1001
commit 747429341e
6 changed files with 849 additions and 4 deletions

View File

@ -202,10 +202,12 @@ class FrameworkExtension extends Extension
// Choose storage class based on the DSN
$supported = array(
'sqlite' => 'Symfony\Component\HttpKernel\Profiler\SqliteProfilerStorage',
'mysql' => 'Symfony\Component\HttpKernel\Profiler\MysqlProfilerStorage',
'file' => 'Symfony\Component\HttpKernel\Profiler\FileProfilerStorage',
'mongodb' => 'Symfony\Component\HttpKernel\Profiler\MongoDbProfilerStorage',
'sqlite' => 'Symfony\Component\HttpKernel\Profiler\SqliteProfilerStorage',
'mysql' => 'Symfony\Component\HttpKernel\Profiler\MysqlProfilerStorage',
'file' => 'Symfony\Component\HttpKernel\Profiler\FileProfilerStorage',
'mongodb' => 'Symfony\Component\HttpKernel\Profiler\MongoDbProfilerStorage',
'memcache' => 'Symfony\Component\HttpKernel\Profiler\MemcacheProfilerStorage',
'memcached' => 'Symfony\Component\HttpKernel\Profiler\MemcachedProfilerStorage',
);
list($class, ) = explode(':', $config['dsn'], 2);
if (!isset($supported[$class])) {

View File

@ -0,0 +1,260 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\Profiler;
/**
* Base Memcache storage for profiling information in a Memcache.
*
* @author Andrej Hudec <pulzarraider@gmail.com>
*/
abstract class BaseMemcacheProfilerStorage implements ProfilerStorageInterface
{
const TOKEN_PREFIX = 'sf_profiler_';
protected $dsn;
protected $lifetime;
/**
* Constructor.
*
* @param string $dsn A data source name
* @param string $username
* @param string $password
* @param int $lifetime The lifetime to use for the purge
*/
public function __construct($dsn, $username = '', $password = '', $lifetime = 86400)
{
$this->dsn = $dsn;
$this->lifetime = (int) $lifetime;
}
/**
* {@inheritdoc}
*/
public function find($ip, $url, $limit, $method)
{
$indexName = $this->getIndexName();
$indexContent = $this->getValue($indexName);
if (!$indexContent) {
return array();
}
$profileList = explode("\n", $indexContent);
$result = array();
foreach ($profileList as $item) {
if ($limit === 0) {
break;
}
if ($item=='') {
continue;
}
list($itemToken, $itemIp, $itemMethod, $itemUrl, $itemTime, $itemParent) = explode("\t", $item, 6);
if ($ip && false === strpos($itemIp, $ip) || $url && false === strpos($itemUrl, $url) || $method && false === strpos($itemMethod, $method)) {
continue;
}
$result[$itemToken] = array(
'token' => $itemToken,
'ip' => $itemIp,
'method' => $itemMethod,
'url' => $itemUrl,
'time' => $itemTime,
'parent' => $itemParent,
);
--$limit;
}
return array_values($result);
}
/**
* {@inheritdoc}
*/
public function purge()
{
$this->flush();
}
/**
* {@inheritdoc}
*/
public function read($token)
{
if (empty($token)) {
return false;
}
$profile = $this->getValue($this->getItemName($token));
if (false !== $profile) {
$profile = $this->createProfileFromData($token, $profile);
}
return $profile;
}
/**
* {@inheritdoc}
*/
public function write(Profile $profile)
{
$data = array(
'token' => $profile->getToken(),
'parent' => $profile->getParentToken(),
'children' => array_map(function ($p) { return $p->getToken(); }, $profile->getChildren()),
'data' => $profile->getCollectors(),
'ip' => $profile->getIp(),
'method' => $profile->getMethod(),
'url' => $profile->getUrl(),
'time' => $profile->getTime(),
);
if ($this->setValue($this->getItemName($profile->getToken()), $data, $this->lifetime)) {
// Add to index
$indexName = $this->getIndexName();
$indexRow = implode("\t", array(
$profile->getToken(),
$profile->getIp(),
$profile->getMethod(),
$profile->getUrl(),
$profile->getTime(),
$profile->getParentToken(),
))."\n";
return $this->appendValue($indexName, $indexRow, $this->lifetime);
}
return false;
}
/**
* Retrieve item from the memcache server
*
* @param string $key
*
* @return mixed
*/
abstract protected function getValue($key);
/**
* Store an item on the memcache server under the specified key
*
* @param string $key
* @param mixed $value
* @param int $expiration
*
* @return boolean
*/
abstract protected function setValue($key, $value, $expiration = 0);
/**
* Flush all existing items at the memcache server
*
* @return boolean
*/
abstract protected function flush();
/**
* Append data to an existing item on the memcache server
* @param string $key
* @param string $value
* @param int $expiration
*
* @return boolean
*/
abstract protected function appendValue($key, $value, $expiration = 0);
private function createProfileFromData($token, $data, $parent = null)
{
$profile = new Profile($token);
$profile->setIp($data['ip']);
$profile->setMethod($data['method']);
$profile->setUrl($data['url']);
$profile->setTime($data['time']);
$profile->setCollectors($data['data']);
if (!$parent && $data['parent']) {
$parent = $this->read($data['parent']);
}
if ($parent) {
$profile->setParent($parent);
}
foreach ($data['children'] as $token) {
if (!$token) {
continue;
}
if (!$childProfileData = $this->getValue($this->getItemName($token))) {
continue;
}
$profile->addChild($this->createProfileFromData($token, $childProfileData, $profile));
}
return $profile;
}
/**
* Get item name
*
* @param string $token
*
* @return string
*/
private function getItemName($token)
{
$name = self::TOKEN_PREFIX . $token;
if ($this->isItemNameValid($name)) {
return $name;
}
return false;
}
/**
* Get name of index
*
* @return string
*/
private function getIndexName()
{
$name = self::TOKEN_PREFIX . 'index';
if ($this->isItemNameValid($name)) {
return $name;
}
return false;
}
private function isItemNameValid($name)
{
$length = strlen($name);
if ($length > 250) {
throw new \RuntimeException(sprintf('The memcache item key "%s" is too long (%s bytes). Allowed maximum size is 250 bytes.', $name, $length));
}
return true;
}
}

View File

@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\Profiler;
use Memcache;
/**
* Memcache Profiler Storage
*
* @author Andrej Hudec <pulzarraider@gmail.com>
*/
class MemcacheProfilerStorage extends BaseMemcacheProfilerStorage
{
/**
* @var Memcache
*/
private $memcache;
/**
* Internal convenience method that returns the instance of the Memcache
*
* @return Memcache
*/
protected function getMemcache()
{
if (null === $this->memcache) {
if (!preg_match('#^memcache://(.*)/(.*)$#', $this->dsn, $matches)) {
throw new \RuntimeException('Please check your configuration. You are trying to use Memcache with an invalid dsn. "' . $this->dsn . '"');
}
$host = $matches[1];
$port = $matches[2];
$memcache = new Memcache;
$memcache->addServer($host, $port);
$this->memcache = $memcache;
}
return $this->memcache;
}
/**
* {@inheritdoc}
*/
protected function getValue($key)
{
return $this->getMemcache()->get($key);
}
/**
* {@inheritdoc}
*/
protected function setValue($key, $value, $expiration = 0)
{
return $this->getMemcache()->set($key, $value, false, $expiration);
}
/**
* {@inheritdoc}
*/
protected function flush()
{
return $this->getMemcache()->flush();
}
/**
* {@inheritdoc}
*/
protected function appendValue($key, $value, $expiration = 0)
{
$memcache = $this->getMemcache();
if (method_exists($memcache, 'append')) {
//Memcache v3.0
if (!$result = $memcache->append($key, $value, false, $expiration)) {
return $memcache->set($key, $value, false, $expiration);
}
return $result;
}
//simulate append in Memcache <3.0
$content = $memcache->get($key);
return $memcache->set($key, $content . $value, false, $expiration);
}
}

View File

@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\Profiler;
use Memcached;
/**
* Memcached Profiler Storage
*
* @author Andrej Hudec <pulzarraider@gmail.com>
*/
class MemcachedProfilerStorage extends BaseMemcacheProfilerStorage
{
/**
* @var Memcached
*/
private $memcached;
/**
* Internal convenience method that returns the instance of the Memcached
*
* @return Memcached
*/
protected function getMemcached()
{
if (null === $this->memcached) {
if (!preg_match('#^memcached://(.*)/(.*)$#', $this->dsn, $matches)) {
throw new \RuntimeException('Please check your configuration. You are trying to use Memcached with an invalid dsn. "' . $this->dsn . '"');
}
$host = $matches[1];
$port = $matches[2];
$memcached = new Memcached;
//disable compression to allow appending
$memcached->setOption(Memcached::OPT_COMPRESSION, false);
$memcached->addServer($host, $port);
$this->memcached = $memcached;
}
return $this->memcached;
}
/**
* {@inheritdoc}
*/
protected function getValue($key)
{
return $this->getMemcached()->get($key);
}
/**
* {@inheritdoc}
*/
protected function setValue($key, $value, $expiration = 0)
{
return $this->getMemcached()->set($key, $value, false, $expiration);
}
/**
* {@inheritdoc}
*/
protected function flush()
{
return $this->getMemcached()->flush();
}
/**
* {@inheritdoc}
*/
protected function appendValue($key, $value, $expiration = 0)
{
$memcached = $this->getMemcached();
if (!$result = $memcached->append($key, $value)) {
return $memcached->set($key, $value, $expiration);
}
return $result;
}
}

View File

@ -0,0 +1,194 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Tests\Component\HttpKernel\Profiler;
use Symfony\Component\HttpKernel\Profiler\MemcacheProfilerStorage;
use Symfony\Component\HttpKernel\Profiler\Profile;
class DummyMemcacheProfilerStorage extends MemcacheProfilerStorage
{
public function getMemcache()
{
return parent::getMemcache();
}
}
class MemcacheProfilerStorageTest extends \PHPUnit_Framework_TestCase
{
protected static $storage;
public static function tearDownAfterClass()
{
if (self::$storage) {
self::$storage->purge();
}
}
protected function setUp()
{
if (!extension_loaded('memcache')) {
$this->markTestSkipped('MemcacheProfilerStorageTest requires that the extension memcache is loaded');
}
self::$storage = new DummyMemcacheProfilerStorage('memcache://127.0.0.1/11211', '', '', 86400);
try {
self::$storage->getMemcache();
} catch(\Exception $e) {
$this->markTestSkipped('MemcacheProfilerStorageTest requires that there is a Memcache server present on localhost');
}
if (self::$storage) {
self::$storage->purge();
}
}
public function testStore()
{
for ($i = 0; $i < 10; $i ++) {
$profile = new Profile('token_'.$i);
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo.bar');
$profile->setMethod('GET');
self::$storage->write($profile);
}
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar', 20, 'GET')), 10, '->write() stores data in the Memcache');
}
public function testChildren()
{
$parentProfile = new Profile('token_parent');
$parentProfile->setIp('127.0.0.1');
$parentProfile->setUrl('http://foo.bar/parent');
$childProfile = new Profile('token_child');
$childProfile->setIp('127.0.0.1');
$childProfile->setUrl('http://foo.bar/child');
$parentProfile->addChild($childProfile);
self::$storage->write($parentProfile);
self::$storage->write($childProfile);
// Load them from storage
$parentProfile = self::$storage->read('token_parent');
$childProfile = self::$storage->read('token_child');
// Check child has link to parent
$this->assertNotNull($childProfile->getParent());
$this->assertEquals($parentProfile->getToken(), $childProfile->getParentToken());
// Check parent has child
$children = $parentProfile->getChildren();
$this->assertEquals(1, count($children));
$this->assertEquals($childProfile->getToken(), $children[0]->getToken());
}
public function testStoreSpecialCharsInUrl()
{
// The SQLite storage accepts special characters in URLs (Even though URLs are not
// supposed to contain them)
$profile = new Profile('simple_quote');
$profile->setUrl('127.0.0.1', 'http://foo.bar/\'');
self::$storage->write($profile);
$this->assertTrue(false !== self::$storage->read('simple_quote'), '->write() accepts single quotes in URL');
$profile = new Profile('double_quote');
$profile->setUrl('127.0.0.1', 'http://foo.bar/"');
self::$storage->write($profile);
$this->assertTrue(false !== self::$storage->read('double_quote'), '->write() accepts double quotes in URL');
$profile = new Profile('backslash');
$profile->setUrl('127.0.0.1', 'http://foo.bar/\\');
self::$storage->write($profile);
$this->assertTrue(false !== self::$storage->read('backslash'), '->write() accepts backslash in URL');
$profile = new Profile('comma');
$profile->setUrl('127.0.0.1', 'http://foo.bar/,');
self::$storage->write($profile);
$this->assertTrue(false !== self::$storage->read('comma'), '->write() accepts comma in URL');
}
public function testStoreDuplicateToken()
{
$profile = new Profile('token');
$profile->setUrl('http://example.com/');
$this->assertTrue(self::$storage->write($profile), '->write() returns true when the token is unique');
$profile->setUrl('http://example.net/');
$this->assertTrue(self::$storage->write($profile), '->write() returns true when the token is already present in the Memcache');
$this->assertEquals('http://example.net/', self::$storage->read('token')->getUrl(), '->write() overwrites the current profile data');
$this->assertCount(1, self::$storage->find('', '', 1000, ''), '->find() does not return the same profile twice');
}
public function testRetrieveByIp()
{
$profile = new Profile('token');
$profile->setIp('127.0.0.1');
$profile->setMethod('GET');
self::$storage->write($profile);
$this->assertEquals(count(self::$storage->find('127.0.0.1', '', 10, 'GET')), 1, '->find() retrieve a record by IP');
$this->assertEquals(count(self::$storage->find('127.0.%.1', '', 10, 'GET')), 0, '->find() does not interpret a "%" as a wildcard in the IP');
$this->assertEquals(count(self::$storage->find('127.0._.1', '', 10, 'GET')), 0, '->find() does not interpret a "_" as a wildcard in the IP');
}
public function testRetrieveByUrl()
{
$profile = new Profile('simple_quote');
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo.bar/\'');
$profile->setMethod('GET');
self::$storage->write($profile);
$profile = new Profile('double_quote');
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo.bar/"');
$profile->setMethod('GET');
self::$storage->write($profile);
$profile = new Profile('backslash');
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo\\bar/');
$profile->setMethod('GET');
self::$storage->write($profile);
$profile = new Profile('percent');
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo.bar/%');
$profile->setMethod('GET');
self::$storage->write($profile);
$profile = new Profile('underscore');
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo.bar/_');
$profile->setMethod('GET');
self::$storage->write($profile);
$profile = new Profile('semicolon');
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo.bar/;');
$profile->setMethod('GET');
self::$storage->write($profile);
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/\'', 10, 'GET')), 1, '->find() accepts single quotes in URLs');
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/"', 10, 'GET')), 1, '->find() accepts double quotes in URLs');
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo\\bar/', 10, 'GET')), 1, '->find() accepts backslash in URLs');
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/;', 10, 'GET')), 1, '->find() accepts semicolon in URLs');
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/%', 10, 'GET')), 1, '->find() does not interpret a "%" as a wildcard in the URL');
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/_', 10, 'GET')), 1, '->find() does not interpret a "_" as a wildcard in the URL');
}
}

View File

@ -0,0 +1,194 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Tests\Component\HttpKernel\Profiler;
use Symfony\Component\HttpKernel\Profiler\MemcachedProfilerStorage;
use Symfony\Component\HttpKernel\Profiler\Profile;
class DummyMemcachedProfilerStorage extends MemcachedProfilerStorage
{
public function getMemcached()
{
return parent::getMemcached();
}
}
class MemcachedProfilerStorageTest extends \PHPUnit_Framework_TestCase
{
protected static $storage;
public static function tearDownAfterClass()
{
if (self::$storage) {
self::$storage->purge();
}
}
protected function setUp()
{
if (!extension_loaded('memcached')) {
$this->markTestSkipped('MemcachedProfilerStorageTest requires that the extension memcached is loaded');
}
self::$storage = new DummyMemcachedProfilerStorage('memcached://127.0.0.1/11211', '', '', 86400);
try {
self::$storage->getMemcached();
} catch(\Exception $e) {
$this->markTestSkipped('MemcachedProfilerStorageTest requires that there is a Memcache server present on localhost');
}
if (self::$storage) {
self::$storage->purge();
}
}
public function testStore()
{
for ($i = 0; $i < 10; $i ++) {
$profile = new Profile('token_'.$i);
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo.bar');
$profile->setMethod('GET');
self::$storage->write($profile);
}
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar', 20, 'GET')), 10, '->write() stores data in the Memcache');
}
public function testChildren()
{
$parentProfile = new Profile('token_parent');
$parentProfile->setIp('127.0.0.1');
$parentProfile->setUrl('http://foo.bar/parent');
$childProfile = new Profile('token_child');
$childProfile->setIp('127.0.0.1');
$childProfile->setUrl('http://foo.bar/child');
$parentProfile->addChild($childProfile);
self::$storage->write($parentProfile);
self::$storage->write($childProfile);
// Load them from storage
$parentProfile = self::$storage->read('token_parent');
$childProfile = self::$storage->read('token_child');
// Check child has link to parent
$this->assertNotNull($childProfile->getParent());
$this->assertEquals($parentProfile->getToken(), $childProfile->getParentToken());
// Check parent has child
$children = $parentProfile->getChildren();
$this->assertEquals(1, count($children));
$this->assertEquals($childProfile->getToken(), $children[0]->getToken());
}
public function testStoreSpecialCharsInUrl()
{
// The SQLite storage accepts special characters in URLs (Even though URLs are not
// supposed to contain them)
$profile = new Profile('simple_quote');
$profile->setUrl('127.0.0.1', 'http://foo.bar/\'');
self::$storage->write($profile);
$this->assertTrue(false !== self::$storage->read('simple_quote'), '->write() accepts single quotes in URL');
$profile = new Profile('double_quote');
$profile->setUrl('127.0.0.1', 'http://foo.bar/"');
self::$storage->write($profile);
$this->assertTrue(false !== self::$storage->read('double_quote'), '->write() accepts double quotes in URL');
$profile = new Profile('backslash');
$profile->setUrl('127.0.0.1', 'http://foo.bar/\\');
self::$storage->write($profile);
$this->assertTrue(false !== self::$storage->read('backslash'), '->write() accepts backslash in URL');
$profile = new Profile('comma');
$profile->setUrl('127.0.0.1', 'http://foo.bar/,');
self::$storage->write($profile);
$this->assertTrue(false !== self::$storage->read('comma'), '->write() accepts comma in URL');
}
public function testStoreDuplicateToken()
{
$profile = new Profile('token');
$profile->setUrl('http://example.com/');
$this->assertTrue(self::$storage->write($profile), '->write() returns true when the token is unique');
$profile->setUrl('http://example.net/');
$this->assertTrue(self::$storage->write($profile), '->write() returns true when the token is already present in the Memcache');
$this->assertEquals('http://example.net/', self::$storage->read('token')->getUrl(), '->write() overwrites the current profile data');
$this->assertCount(1, self::$storage->find('', '', 1000, ''), '->find() does not return the same profile twice');
}
public function testRetrieveByIp()
{
$profile = new Profile('token');
$profile->setIp('127.0.0.1');
$profile->setMethod('GET');
self::$storage->write($profile);
$this->assertEquals(count(self::$storage->find('127.0.0.1', '', 10, 'GET')), 1, '->find() retrieve a record by IP');
$this->assertEquals(count(self::$storage->find('127.0.%.1', '', 10, 'GET')), 0, '->find() does not interpret a "%" as a wildcard in the IP');
$this->assertEquals(count(self::$storage->find('127.0._.1', '', 10, 'GET')), 0, '->find() does not interpret a "_" as a wildcard in the IP');
}
public function testRetrieveByUrl()
{
$profile = new Profile('simple_quote');
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo.bar/\'');
$profile->setMethod('GET');
self::$storage->write($profile);
$profile = new Profile('double_quote');
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo.bar/"');
$profile->setMethod('GET');
self::$storage->write($profile);
$profile = new Profile('backslash');
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo\\bar/');
$profile->setMethod('GET');
self::$storage->write($profile);
$profile = new Profile('percent');
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo.bar/%');
$profile->setMethod('GET');
self::$storage->write($profile);
$profile = new Profile('underscore');
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo.bar/_');
$profile->setMethod('GET');
self::$storage->write($profile);
$profile = new Profile('semicolon');
$profile->setIp('127.0.0.1');
$profile->setUrl('http://foo.bar/;');
$profile->setMethod('GET');
self::$storage->write($profile);
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/\'', 10, 'GET')), 1, '->find() accepts single quotes in URLs');
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/"', 10, 'GET')), 1, '->find() accepts double quotes in URLs');
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo\\bar/', 10, 'GET')), 1, '->find() accepts backslash in URLs');
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/;', 10, 'GET')), 1, '->find() accepts semicolon in URLs');
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/%', 10, 'GET')), 1, '->find() does not interpret a "%" as a wildcard in the URL');
$this->assertEquals(count(self::$storage->find('127.0.0.1', 'http://foo.bar/_', 10, 'GET')), 1, '->find() does not interpret a "_" as a wildcard in the URL');
}
}