I have checked the memory whilst sending and receiving data over one connection, and I appear to be correctly clearing variables, as the memory returns to its previous value.
But for some reason if I make a new connection, then close the connection, memory is leaked. I believe the problem may be occurring when a socket is accepted.
I am using PHP 5.2.10
Hopefully one of you can find the time to have a play with the source and figure out where its gone wrong. Thanks in advance
<?php
Class SuperSocket
{
var $listen = array();
var $status_listening = FALSE;
var $sockets = array();
var $event_callbacks = array();
var $recvq = 1;
var $parent;
var $delay = 100; // 10,000th of a second
var $data_buffer = array();
function SuperSocket($listen = array('127.0.0.1:123'))
{
$listen = array_unique($listen);
foreach ($listen as $address)
{
list($address, $port) = explode(":", $address, 2);
$this->listen[] = array("ADDR" => trim($address), "PORT" => trim($port));
};
}
function start()
{
if ($this->status_listening)
{
return FALSE;
};
$this->sockets = array();
$cursocket = 0;
foreach ($this->listen as $listen)
{
if ($listen['ADDR'] == "*")
{
$this->sockets[$cursocket]['socket'] = socket_create_listen($listen['PORT']);
$listen['ADDR'] = FALSE;
}
else
{
$this->sockets[$cursocket]['socket'] = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
};
if ($this->sockets[$cursocket]['socket'] < 0)
{
return FALSE;
};
if (#socket_bind($this->sockets[$cursocket]['socket'], $listen['ADDR'], $listen['PORT']) < 0)
{
return FALSE;
};
if (socket_listen($this->sockets[$cursocket]['socket']) < 0)
{
return FALSE;
};
if (!socket_set_option($this->sockets[$cursocket]['socket'], SOL_SOCKET, SO_REUSEADDR, 1))
{
return FALSE;
};
if (!socket_set_nonblock($this->sockets[$cursocket]['socket']))
{
return FALSE;
};
$this->sockets[$cursocket]['info'] = array("ADDR" => $listen['ADDR'], "PORT" => $listen['PORT']);
$this->sockets[$cursocket]['channels'] = array();
$this->sockets[$cursocket]['id'] = $cursocket;
$cursocket++;
};
$this->status_listening = TRUE;
}
function new_socket_loop(&$socket)
{
$socket =& $this->sockets[$socket['id']];
if ($newchannel = #stream_socket_accept($socket['socket'], 0));//#socket_accept($socket['socket']))
{
socket_set_nonblock($newchannel);
$socket['channels'][]['socket'] = $newchannel;
$channel = array_pop(array_keys($socket['channels']));
$this->remote_address($newchannel, $remote_addr, $remote_port);
$socket['channels'][$channel]['info'] = array('ADDR' => $remote_addr, 'PORT' => $remote_port);
$event = $this->event("NEW_SOCKET_CHANNEL");
if ($event)
$event($socket['id'], $channel, $this);
};
}
function endswith($string, $test) {
$strlen = strlen($string);
$testlen = strlen($test);
if ($testlen > $strlen) return false;
return substr_compare($string, $test, -$testlen) === 0;
}
function recv_socket_loop(&$socket)
{
$socket =& $this->sockets[$socket['id']];
foreach ($socket['channels'] as $channel_id => $channel)
{
unset($buffer);#Flush buffer
$status = #socket_recv($channel['socket'], $buffer, $this->recvq, 0);
if ($status === 0 && $buffer === NULL)
{
$this->close($socket['id'], $channel_id);
}
elseif (!($status === FALSE && $buffer === NULL))
{
$sockid = $socket['id'];
if(!isset($this->data_buffer[$sockid]))
$this->data_buffer[$sockid]='';
if($buffer!="\r"&&$buffer!="\n")
{
//Putty ends with \r\n
$this->data_buffer[$sockid].=$buffer;
}
else if($buffer!="\n") //ignore the additional newline char \n
{
$event = $this->event("DATA_SOCKET_CHANNEL");
if ($event)
$event($socket['id'], $channel_id, $this->data_buffer[$sockid], $this);
unset($this->data_buffer[$sockid]);
}
};
}
}
function stop()
{
$this->closeall();
$this->status_listening = FALSE;
foreach ($this->sockets as $socket_id => $socket)
{
socket_shutdown($socket['socket']);
socket_close($socket['socket']);
};
$event = $this->event("SERVER_STOP");
if ($event)
$event($this);
}
function closeall($socket_id = NULL)
{
if ($socket_id === NULL)
{
foreach ($this->sockets as $socket_id => $socket)
{
foreach ($socket['channels'] as $channel_id => $channel)
{
$this->close($socket_id, $channel_id);
}
}
}
else
{
foreach ($this->sockets[$socket_id]['channels'] as $channel_id => $channel)
{
$this->close($socket_id, $channel_id);
};
};
}
function close($socket_id, $channel_id)
{
unset($this->data_buffer[$socket_id]); //clear the sockets data buffer
$arrOpt = array('l_onoff' => 1, 'l_linger' => 1);
#socket_shutdown($this->sockets[$socket_id]['channels'][$channel_id]['socket']);
#socket_close($this->sockets[$socket_id]['channels'][$channel_id]['socket']);
$event = $this->event("LOST_SOCKET_CHANNEL");
if ($event)
$event($socket_id, $channel_id, $this);
}
function loop()
{
while ($this->status_listening)
{
usleep($this->delay);
foreach ($this->sockets as $socket)
{
$this->new_socket_loop($socket);
$this->recv_socket_loop($socket);
};
$event = $this->event("END_SOCKET_CHANNEL");
if ($event)
$event($this);
};
}
function write($socket_id, $channel_id, $buffer)
{
#socket_write($this->sockets[$socket_id]['channels'][$channel_id]['socket'], $buffer);
#socket_write($this->sockets[$socket_id]['channels'][$channel_id]['socket'], 'Server memory usage: '.memory_get_usage().'/'.memory_get_peak_usage(true)."\r\n");
}
function get_channel_info($socket_id, $channel_id)
{
return $this->sockets[$socket_id]['channels'][$channel_id]['info'];
}
function get_socket_info($socket_id)
{
$socket_info = $this->sockets[$socket_id]['info'];
if (empty($socket_info['ADDR']))
{
$socket_info['ADDR'] = "*";
};
return $socket_info;
}
function get_raw_channel_socket($socket_id, $channel_id)
{
return $this->sockets[$socket_id]['channels'][$channel_id]['socket'];
}
function remote_address($channel_socket, &$ipaddress, &$port)
{
socket_getpeername($channel_socket, $ipaddress, $port);
}
function event($name)
{
if (isset($this->event_callbacks[$name]))
return $this->event_callbacks[$name];
}
function assign_callback($name, $function_name)
{
$this->event_callbacks[$name] = $function_name;
}
};
?>
Server.php
include("supersocket.class.php");
function startswith($string, $test) {
return strpos($string, $test, 0) === 0;
}
function newdata($socket_id, $channel_id, $buffer, &$server)
{
//$server->write($socket_id, $channel_id, ">".$buffer."\r\n");
if($buffer=="STOP")
{
$server->stop();
}
else if($buffer=="DATETIME")
{
$server->write($socket_id, $channel_id, ">".date("dmYHis")."\r\n");
}
else
{
$server->write($socket_id, $channel_id, ">BAD\r\n");
}
};
function newclient($socket_id, $channel_id, &$server)
{
$server->write($socket_id, $channel_id, "HEADER\n\r");
}
$socket = new SuperSocket(array('127.0.0.1:12345'));
$socket->assign_callback("DATA_SOCKET_CHANNEL", "newdata");
$socket->assign_callback("NEW_SOCKET_CHANNEL", "newclient");
$socket->start();
//set_time_limit(60*2);
set_time_limit(60*60*24*5); //5 days
$socket->loop();
Edit: sorry you might need to change the socket accept back to:
if ($newchannel = #socket_accept($socket['socket']))
then close the connection, memory is leaked
This is a tricky one - even the standard reference counting garbage collector only kicks in at intervals which are difficult to predict. Calling gc_collect_cycles() should trigger the gc though. Try calling that whenever you close a connection and see if it makes a difference.
If you're still seeing problems - then check if you've got the cyclic reference counter compiled in - if not, then get it.
The Channel array was never removed upon closing the connection, surprised no one picked up on this. Memory usage is now super tight.
unset($this->sockets[$socket_id]['channels'][$channel_id]);
But it does mean that any event for LOST_SOCKET_CHANNEL is pretty useless for the time being.
Will accept my own answer when stack over flow allows. Thanks for all your help ppl .. i guess..
Related
I have two functions which appear to be slowing down PHP, and I believe its to do with the use of array_merge in the loops, only I am struggling to figure out how I can get around not doing so.
function fuseOzone($field) {
global $ozone;
if(isset($ozone['all'])) {
foreach($ozone['all'] as $priority => $functions) {
if(isset($ozone[$field][$priority])) {
$ozone[$field][$priority] = array_merge($ozone['all'][$priority], $ozone[$field][$priority]);
} else {
$ozone[$field][$priority] = array_merge($ozone['all'][$priority], array());
$ozone[$field][$priority] = array_unique($ozone[$field][$priority]);
}
}
}
if(isset($ozone[$field])) {
ksort($ozone[$field]);
}
}
function applyOzone($field, $content) {
global $ozone;
fuseOzone($field);
$args = array_slice(func_get_args(), 2);
if(isset($ozone[$field])) {
foreach($ozone[$field] as $priority => $functions) {
if(!is_null($functions)) {
foreach($functions as $function) {
if(!function_exists($function['function'])) {
continue;
}
$all_args = array_merge(array($content), $args);
$function_name = $function['function'];
$accepted_args = $function['accepted_args'];
if($accepted_args === 1) {
$the_args = array($content);
} elseif($accepted_args > 1) {
$the_args = array_slice($all_args, 0, $accepted_args);
} elseif($accepted_args === 0) {
$the_args = null;
} else {
$the_args = $all_args;
}
$content = call_user_func_array($function_name, $the_args);
}
}
}
}
return $content;
}
These functions are part of a larger class which essentially operates much like Wordpress actions. I.e. I can hook into a portion of content with filtering or such.
I'm working on a function to recursively remove arrays and objects recursively. The problem is that certain recursions may be inside private properties of objects.
below is what I tried as well as the entries I tried to use.
this is my entrie
class TestOBJ{
private $fooClosure = null;
public $bar = 5;
private $myPrivateRecursion = null;
private $aimArrayAndContainsRecursion = [];
public function __construct()
{
$this->fooClosure = function(){
echo 'pretty closure';
};
}
public function setMyPrivateRecursion(&$obj){
$this->myPrivateRecursion = &$obj;
}
public function setObjInsideArray(&$obj){
$this->aimArrayAndContainsRecursion[] = &$obj;
}
}
$std = new stdClass();
$std->std = 'any str';
$std->obj = new stdClass();
$std->obj->other = &$std;
$obj = new TestOBJ();
$obj->bar = new TestOBJ();
$obj->bar->bar = 'hey brow, please works';
$obj->bar->setMyPrivateRecursion($std);
my entrie is var $obj
and this is my function / solution
function makeRecursionStack($vector, &$stack = [], $from = null)
{
if ($vector) {
if (is_object($vector) && !in_array($vector, $stack, true) && !is_callable($vector)) {
$stack[] = &$vector;
if (get_class($vector) === 'stdClass') {
foreach ($vector as $key => $value) {
if (in_array($vector->{$key}, $stack, true)) {
$vector->{$key} = null;
} else {
$vector->{$key} = $this->makeRecursionStack($vector->{$key}, $stack, $key);
}
}
return $vector;
} else {
$object = new \ReflectionObject($vector);
$reflection = new \ReflectionClass($vector);
$properties = $reflection->getProperties();
if ($properties) {
foreach ($properties as $property) {
$property = $object->getProperty($property->getName());
$property->setAccessible(true);
if (!is_callable($property->getValue($vector))) {
$private = false;
if ($property->isPrivate()) {
$property->setAccessible(true);
$private = true;
}
if (in_array($property->getValue($vector), $stack, true)) {
$property->setValue($vector, null);
} else {
//if($property->getName() === 'myPrivateRecursion' && $from === 'bar'){
//$get = $property->getValue($vector);
//$set = $this->makeRecursionStack($get, $stack, $property->getName());
//$property->setValue($vector, $set);
//pre_clear_buffer_die($property->getValue($vector));
//}
$property->setValue($vector, $this->makeRecursionStack($property->getValue($vector), $stack, $property->getName()));
}
if ($private) {
$property->setAccessible(false);
}
}
}
}
return $vector;
}
} else if (is_array($vector)) {
$nvector = [];
foreach ($vector as $key => $value) {
$nvector[$key] = $this->makeRecursionStack($value, $stack, $key);
}
return $nvector;
} else {
if (is_object($vector) && !is_callable($vector)) {
return null;
}
}
}
return $vector;
}
The place where I have comments is where I noticed the problem. if the If is not commented there $get would receive a stdClass that has recursion and this works perfectly and $set would receive the stdClass without recursion. In that order.
$get =
$set =
After this lines
$property->setValue($vector, $set);
pre_clear_buffer_die($property->getValue($vector));
i obtain this
I try to put other value like an bool or null inside property and after set the $set but it's not works.
P.S: pre_clear_buffer_die kill php buffer, init other buffer and show var inside a <pre> after exit from script. Is an debugger function.
We built an API to directly access other social networks APIs using our keys.
I'm trying to build a fuction to access that API.
The default function has been written and is working.
Question
How can I specify a new array to target the json data?
This will override the default setting.
function SocialAPI($handle, $service, $path="") {
$handle = strtolower($handle);
$service = strtolower($service);
$api = file_get_contents("https://api.service.domain.com/v1/Social?handle=$handle&service=$service");
if($api !== false) {
$data = json_decode($api, true);
if($data !== null) {
if($service === "twitter") {
return $data['0']['followers_count'];
}
if($service === "instagram") {
if(!empty($path)) {
while($id = array_shift($path)) {
echo $data[$id];
}
return $data;
} else {
return $data['user']['followed_by']['count'];
}
}
} else {
return false;
}
} else {
return "API call failed.";
}
}
//Test API Function - ** TO BE DELETED **
echo SocialAPI("JohnDoe", "Instagram", "['user']['full_name']");
exit();
function array_deref($data, $keys) {
return empty($keys) ? $data
: array_deref($data[$keys[0]], array_slice($data, 1))
}
function SocialAPI($handle, $service, $path="") {
$handle = strtolower($handle);
$service = strtolower($service);
$api = file_get_contents("https://api.service.domain.com/v1/Social?handle=$handle&service=$service");
if ($api === false) {
return "API call failed.";
}
$data = json_decode($api, true);
if($data !== null) {
return false;
}
if ($service === "twitter") {
if (empty($path)) $path = ['0','followers_count'];
return array_deref($data, $path);
} elseif ($service === "instagram") {
if (empty($path)) $path = ['user','followed_by'];
return array_deref($data, $path);
}
}
//Test API Function - ** TO BE DELETED **
echo SocialAPI("JohnDoe", "Instagram", ['user', 'full_name']);
echo SocialAPI("JohnDoe", "Instagram");
exit();
I added a utility function, array_deref, to walk the arrays recursively (calls itself to handle each level down).
I have the simple app below. I'm turning off query logging in Laravel, I'm unsetting where possible, yet this function will only process about 800 records before I'm out of RAM on my 2GB Linode. I know I'm asking a lot of you guys but I can't seem to see where I'm leaking memory.
There are really only two major steps.
Step 1 - Move records from a temp table to production
class ListingMigrator
{
public function __construct($tempListing, $feed)
{
$this->tempListing = $tempListing;
$this->listing = $this->listingInstance();
$this->feed = $feed;
}
public static function migrateListing($listing, $feed)
{
$instance = new static($listing, $feed);
return $instance->migrate();
}
public function migrate()
{
$this->addExternalData();
$this->populateListing();
$this->processPhotos();
$this->deleteTempListing();
}
private function listingInstance()
{
DB::connection()->disableQueryLog();
$listing = Listing::findByMud($this->tempListing->matrix_unique_id);
return $listing ?: new Listing;
}
private function processPhotos()
{
$retsApi = new RetsFeedApi($this->feed);
/* Initialize Object */
$rets = $retsApi->findMostRecent();
$photos = $rets->getPhotosForListing($this->listing->matrix_unique_id);
foreach ($photos as $photo)
{
$uploader = new PhotoProcessor($this->listing, $photo);
$uploader->process();
}
}
private function populateListing()
{
DB::connection()->disableQueryLog();
$this->listing->fill($this->tempListing->toArray());
$this->listing->imported_at = $this->tempListing->created_at;
$this->listing->board = $this->tempListing->board;
return $this->listing->save();
}
private function addExternalData()
{
// Get Google lattitude and longitude
$googlecoords = getGoogleMapInfo($this->tempListing->FullAddress, $this->tempListing->City);
$this->listing->GoogleLat = $googlecoords['GoogleLat'];
$this->listing->GoogleLong = $googlecoords['GoogleLong'];
// Add or update the Subdivision Table (helper function)
$subdivisiondata = SubdivisionUpdate($this->tempListing->board, $this->tempListing->SubCondoName, $this->tempListing->Development);
$this->listing->SubdivisionID = $subdivisiondata['id'];
}
private function deleteTempListing()
{
return $this->tempListing->delete();
}
}
Step 2 - Download photos and reupload to Amazon S3
class PhotoProcessor
{
public function __construct(Listing $listing, $photoData)
{
$this->bucket = 'real-estate-listings';
$this->s3 = App::make('aws')->get('s3');
$this->tempFileName = 'app/storage/processing/images/retsphotoupload';
$this->photoData = $photoData;
$this->listing = $listing;
$this->photo = new RetsPhoto;
}
public function process()
{
$this->storeTempFile();
$this->storeFileInfo();
$this->buildPhoto();
$success = $this->pushToS3();
// if Result has the full URL or you want to build it, add it to $this->photo
DB::connection()->disableQueryLog();
$this->listing->photos()->save($this->photo);
$this->removeTempFile();
unset ($this->photoData);
return $success;
}
private function storeTempFile()
{
return File::put($this->tempFileName, $this->photoData['Data']) > 0;
}
private function storeFileInfo()
{
$fileInfo = getimagesize($this->tempFileName);
// Could even be its own object
$this->fileInfo = [
'width' => $fileInfo[0],
'height' => $fileInfo[1],
'mimetype' => $fileInfo['mime'],
'extension' => $this->getFileExtension($fileInfo['mime'])
];
}
private function buildPhoto()
{
$this->photo->number = $this->photoData['Object-ID']; // Storing this because it is relevant order wise
$this->photo->width = $this->fileInfo['width'];
$this->photo->height = $this->fileInfo['height'];
$this->photo->path = $this->getFilePath();
}
private function getFilePath()
{
$path = [];
if ($this->listing->City == NULL)
{
$path[] = Str::slug('No City');
}
else
{
$path[] = Str::slug($this->listing->City, $separator = '-');
}
if ($this->listing->Development == NULL)
{
$path[] = Str::slug('No Development');
}
else
{
$path[] = Str::slug($this->listing->Development, $separator = '-');
}
if ($this->listing->Subdivision == NULL)
{
$pathp[] = Str::slug('No Subdivision');
}
else
{
$path[] = Str::slug($this->listing->Subdivision, $separator = '-');
}
if ($this->listing->MLSNumber == NULL)
{
$pathp[] = Str::slug('No MLSNumber');
}
else
{
$path[] = Str::slug($this->listing->MLSNumber, $separator = '-');
}
$path[] = $this->photoData['Object-ID'].'.'.$this->fileInfo['extension'];
return strtolower(join('/', $path));
}
private function pushToS3()
{
return $this->s3->putObject([
'Bucket' => $this->bucket,
'Key' => $this->photo->path,
'ContentType'=> $this->fileInfo['mimetype'],
'SourceFile' => $this->tempFileName
]);
}
private function getFileExtension($mime)
{
// Use better algorithm than this
$ext = str_replace('image/', '', $mime);
return $ext == 'jpeg' ? 'jpg' : $ext;
}
private function removeTempFile()
{
return File::delete($this->tempFileName);
}
}
Edit to show RetsPhoto
class RetsPhoto extends Eloquent {
protected $table = 'rets_property_photos';
public function listing() {
return $this->belongsTo('Listing', 'matrix_unique_id', 'matrix_unique_id');
}
}
Edit #2: Chunk Call
This is in the app/command and the only thing in there is the fire() function below:
public function fire()
{
// Turn off query logging
DB::connection()->disableQueryLog();
$feeds = RetsFeed::where('active','=',1)->get();
foreach ($feeds as $feed)
{
$class = "TempListing{$feed->board}";
$listings = $class::orderBy('MatrixModifiedDT','desc');
$listings->chunk(50, function($listings) use($feed) {
$listings->each(function($listing) use ($feed) {
ListingMigrator::migrateListing($listing,$feed);
echo "Feed: $feed->board\r\n";
echo "SubcondoName: $listing->SubCondoName\r\n";
echo "Development: $listing->Development\r\n";
echo "\r\n";
});
});
}
}
I think I have figured it out.
Your system holds in memory all of the photo data. As witnessed by the unset ($this->photoData);
The problem is that you need to first complete the process function. Your application is not likely processing ANY photos so when you keep grabbing them from the file system you run out of memory BEFORE you even process a single one.
To Confirm this, simply grab 1 file not using the chunk method.
I am not very familar with Laravel, it could be grabbing all of the files all at once as well and eating the ram.
You can do some tracing with memory_get_usage(true) to find out exactly where the ram is getting eaten from. I would suggest analysing the fire method first.
could you please help me to find what cause this process to reach 500MB of memory usage.
It is basically an html page downloader.
Despite the fact that the process is stable (and do not exceed that limit), it' meant to use on low performing machine and I'm not satisfied.
The size of the mysql table 'Sites' is 170MB.
following the script code.
Thanks in advance.
function start() {
try {
global $log;
$db = getConnection();
Zend_Db_Table::setDefaultAdapter($db);
$log->logInfo("logger start");
while (1) {
$sitesTable = new Zend_Db_Table('Sites');
$rowset = $sitesTable->fetchAll();
foreach ($rowset as $row) {
if (time() >= (strtotime($row->lastUpdate) + $row->pollingHours * 60 * 60)) {
db_updateHtml($row);
}
}
}
} catch (Exception $e) {
global $log;
$log->logError($e->getMessage());
}
}
function db_updateHtml($siteRecord) {
try {
if ($siteRecord instanceof Zend_Db_Table_Row) {
$rowwithConnection = $siteRecord;
$url = $siteRecord->url;
$idSite = $siteRecord->idSite;
$crawler = new Crawler();
$sitesTable = new Zend_Db_Table('Sites');
//$rowwithConnection = $sitesTable->fetchRow(
// $sitesTable->select()->where('idSite = ?', $idSite));
$newHtml = HtmlDbEncode($crawler->get_web_page($url));
if (strlen($newHtml) < 10) {
global $log;
$log->logError("Download failed for: url: $url \t idsite: $idSite ");
}
if ($rowwithConnection->isChecked != 0) {
$rowwithConnection->oldHtml = $rowwithConnection->newHtml;
$rowwithConnection->isChecked = 0;
}
$rowwithConnection->newHtml = $crawler->get_web_page($url);
$rowwithConnection->lastUpdate = date("Y-m-d H:i:s");
//$rowwithConnection->diffHtml = getDiff($rowwithConnection->oldHtml, $rowwithConnection->newHtml, false, $rowwithConnection->minLengthChange);
$rowwithConnection->diffHtml = getDiffFromRecord($rowwithConnection, false, $rowwithConnection->minLengthChange);
/* if (strlen($rowwithConnection->diffHtml) > 30) {
$rowwithConnection->lastChanged = $rowwithConnection->lastUpdate;
} */
$rowwithConnection->save();
} else {
$log->logCrit("siteRecord is uninitialized");
}
} catch (Exception $e) {
global $log;
$log->logError($e->getMessage());
}
}
function getDiffFromRecord($row, $force = false, $minLengthChange = 100) {
if ($row instanceof Zend_Db_Table_Row) {
require_once '/var/www/diff/library/finediff.php';
include_once '/var/www/diff/library/Text/Diff.php';
$diff = new AndreaDiff();
$differences = $diff->getDiff($row->oldHtml, $row->newHtml);
if ($diff->isChanged($minLengthChange) || $force) {
$row->lastChanged = $row->lastUpdate;
$row->isChecked = false;
return ($differences);
}
}
return null;
}
function getConnection() {
try {
$pdoParams = array(
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true
);
$db = new Zend_Db_Adapter_Pdo_Mysql(array(
'host' => '127.0.0.1',
'username' => 'root',
'password' => 'administrator',
'dbname' => 'diff',
'driver_options' => $pdoParams
));
return $db;
} catch (Exception $e) {
global $log;
$log->logError($e->getMessage());
}
}
1) Try use fetch method, not fetchAll:
foreach($sitesTable->fetch() as $row){
//...
}
2) try to unset all variables which store html code (if you save it in memory), at last iteration i suppose variable $rowwithConnection will have html code inside.
When i want profile php application i use xhprof it will save you a LOT of time. Good Luck!