I wrote an artisan command to export exercises from a database, to standalone packages that can be used in an e-learning system like moodle, ...
It is a huge amount of exercises, and after a while the memory gets exhausted.
I tried to unset variables, activate the garbage collector, disabled the query log, and did some profiling, but till now with no success
I attached my script below, with each exercise I process, the memory usage adds up with 300k
Any idea's what I can do?
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
set_time_limit(0);
class ExerciseExportCommand extends Command {
/**
* The console command name.
*
* #var string
*/
protected $name = 'exercises:export';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Export all exercises of a method or a specific exercise.';
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* #return void
*/
public function fire()
{
try {
DB::disableQueryLog();
ini_set('memory_limit','1024M');
$this->info('Initial: ' . number_format(memory_get_usage(), 0, '.', ',') . " bytes\n");
$base = base_path() . "/export/";
$playerPath = base_path() . "/public/preview/dist/";
$uploadsPath = base_path() . "/public/uploads/";
$methodId = $this->option('method');
$categoryId = $this->option('category');
$exerciseId = $this->option('exercise');
$this->info("Swing baby...");
$this->info("Let's export some exercises, shall we ?");
//we make an array which holds all the exercises we have to export
$exercises = array();
if($methodId === NULL && $categoryId === NULL && $exerciseId === NULL){
//we are here now anyways, let's do all exercises at once...
$this->comment("Nothing specified, let's do all exercises ");
$exercises2 = Exercise::all();
foreach ($exercises2 as $exercise){
array_push($exercises, $exercise->id);
}
unset($exercises2);
}
//get all exercises for given methodId
if($methodId !== NULL){
$method = Method::with('categories.exercises')->find($methodId);
if($method == NULL) break;
$this->comment("We are ready to roll method " . $method->code);
foreach($method->categories as $category){
foreach($category->exercises as $exercise->id){
array_push($exercises, $exercise);
}
}
unset($method);
}
//get all exercises for given categoryId
if($categoryId !== NULL){
$category = Category::with('exercises')->find($categoryId);
if($category == NULL) break;
$this->comment("We are ready to roll category " . $category->name_prefix . " " . $category->name);
foreach($category->exercises as $exercise->id){
array_push($exercises, $exercise);
}
unset($category);
}
if($exerciseId != null){
$exercise = Exercise::find($exerciseId);
if($exercise != NULL) {
array_push($exercises, $exercise->id);
$this->comment("Exercise added for export: " . $exercise->name_prefix . " " . $exercise->name);
} else {
}
unset($exercise);
}
if(empty($exercises)){
$this->error("No exercises could be found for given method/exercise");
exit();
} else {
$this->comment("Currently counting " . count($exercises) . " exerises to export");
}
$fs = new Filesystem();
//loop the exercises and publish like a charm
foreach($exercises as $exerciseId){
$exercise = Exercise::find($exerciseId);
//determine destination
$path = $base . $exercise->getPath();
$this->comment("starting exercise " . $exercise->id);
//check if path exists, if it does, wipe it out
if($fs->exists($path)){
$fs->deleteDirectory($path, true);
$this->comment("wiped out " . $path);
}
//copy player files
//echo "copying " . $path . "<br />";
$fs->copyDirectory($playerPath, $path);
$fs->cleanDirectory($path."styles/skins");
//copy only necesary skin files to save disk space
$method = $exercise->method();
if($fs->exists($playerPath."styles/skins/".$method->code)){
$fs->copyDirectory($playerPath."styles/skins/".$method->code, $path."styles/skins/".$method->code);
} elseif($method->code == "kameleonspelling" || $method->code == "kameleontaalbeschouwing"){
$fs->copyDirectory($playerPath."styles/skins/kameleon", $path."styles/skins/kameleon");
}
if($fs->exists($playerPath."styles/skins/".$method->code.".css")){
$fs->copy($playerPath."styles/skins/".$method->code.".css", $path."styles/skins/".$method->code.".css");
}
$this->comment("copied player files to " . $path);
//copy resources
//echo "copying resources " . $path . "<br />";
$fs->copyDirectory($uploadsPath . $exercise->id . "/", $path);
$this->comment("copied resources to " . $path);
//copy slide resources
$slides = Slide::where('exerciseID',"=",$exercise->id)->get();
mkdir($path."slides/");
foreach ($slides as $slide) {
$image = $slide->image()->first();
if($image != NULL){
$this->info($uploadsPath."slides/".$image->resourceUri);
$this->info($path."slides/".$image->resourceUri);
$fs->copy($uploadsPath."slides/".$image->resourceUri, $path."slides/".$image->resourceUri);
}
unset($image);
}
$this->comment("copied slide resources to " . $path);
//save xml file
$content = Exercise::getXmlContent($exercise->id);
$fs->put($path . "exercise.xml", View::make('xml', $content));
$this->comment("saved xml to " . $path);
$this->info("finished exercise " . $exercise->id);
unset($method);
unset($content);
unset($slides);
gc_collect_cycles();
$this->info('Peak: ' . number_format(memory_get_peak_usage(), 0, '.', ',') . " bytes\n");
$this->info('End: ' . number_format(memory_get_usage(), 0, '.', ',') . " bytes\n");
}
$this->info("Awesome Possum => finished all exercises ");
$this->info('Peak: ' . number_format(memory_get_peak_usage(), 0, '.', ',') . " bytes\n");
$this->info('End: ' . number_format(memory_get_usage(), 0, '.', ',') . " bytes\n");
} catch(Exception $e){
$this->error($e->getMessage());
$this->comment($e->getTraceAsString());
}
}
/**
* Get the console command arguments.
*
* #return array
*/
protected function getArguments()
{
return array(
//array('example', InputArgument::REQUIRED, 'An example argument.'),
);
}
/**
* Get the console command options.
*
* #return array
*/
protected function getOptions()
{
return array(
array('method', null, InputOption::VALUE_OPTIONAL, 'The id of a method for which all the exercises to export.', null),
array('category', null, InputOption::VALUE_OPTIONAL, 'The id of a category for which all the exercises to export.', null),
array('exercise', null, InputOption::VALUE_OPTIONAL, 'The id of an exercise to export.', null),
);
}
}
This is a dump of my xdebug trace command, with the 20 most memory consuming statements:
Showing the 20 most costly calls sorted by 'memory-own'.
Inclusive Own
function #calls time memory time memory
-------------------------------------------------------------------------------------------------------
debug_backtrace 646 0.0420 20353496 0.0420 20353496
Composer\Autoload\ClassLoader->loadClass 259 0.1911 17556224 0.1139 13953824
PDOStatement->execute 743 0.1184 13729408 0.1184 13729408
array_merge 4051 0.1282 3894816 0.1282 3894816
Illuminate\Database\Eloquent\Model->newInstance 1534 0.4715 3806344 0.0791 3732712
PDOStatement->fetchAll 742 0.0323 2364264 0.0323 2364264
Illuminate\Database\Eloquent\Model->newBaseQueryBuilder 738 0.6625 2177352 0.0657 1688968
explode 3396 0.1026 1296960 0.1026 1296960
Illuminate\Database\Eloquent\Model->newFromBuilder 1534 0.6883 5139552 0.0944 1259576
str_replace 10254 0.3176 1228824 0.3176 1228824
compact 920 0.0339 1181384 0.0339 1181384
PDO->prepare 743 0.1403 816488 0.1403 816488
sprintf 2381 0.0741 802968 0.0741 802968
implode 5586 0.1722 536688 0.1722 536688
array_map 864 0.3164 588512 0.0386 477088
get_class_methods 15 0.0059 472296 0.0059 472296
Illuminate\Database\Eloquent\Model->newQuery 738 0.9783 3044352 0.0656 448488
include 263 6.7525 5732672 0.0468 410416
call_user_func_array 1585 0.5734 3937936 0.0659 357056
SplFileInfo->getPathname 2724 0.0847 344768 0.0847 344768
Turns out that DB::disableQueryLog();fixed it after all. !!
At first I thought it didn't help, because memory kept adding up, and I manually cancelled my script each time. Now while debugging with Memtrack I kept my command running and i noticed after a while the memory usage stagnates.
I'm guessing that the garbage collector doesn't clean up the memory until it decides its necesary?
What is your servers memory_limit?
One solution could be that you split up your command in smaller commands, and run them accordingly. Maybe automate that a new command is run after one is completed?
I would also suggest using some sort of queue provider. This way you could split up the workload to a longer period of time. Additionally the workload is not distributed on your server at all.
Laravel has built in support for: Pheanstalk, Amazon SQS and IronMQ.
Heres the doc link: Laravel queue docs
Related
I have about 1500 students in my database and I need to generate an invoice for each one. For this purpose I am using this library https://github.com/barryvdh/laravel-snappy . When I try to generate I get this error:
The file '/var/www/skmpastebimaslt/public_html/storage/checks/24767 Dominykas Butkevičius 1581703819.pdf' was not created (command: /usr/local/bin/wkhtmltopdf --lowquality '/tmp/knp_snappy5e46e28b5d61f2.13362077.html' '/var/www/skmpastebimaslt/public_html/storage/checks/24767 Dominykas Butkeviius 1581703819.pdf').
https://gyazo.com/3a3f60713601a663a1360cd65fee9914
Invoices are generated for four users then the app crashes. The most interesting thing is that even though the error says the file was not created but it was created, I can find it in my storage folder.
My code:
public function saveCheckPdf(Student $student, Check $check = null, $fileName = null)
{
if (empty($fileName)) {
$fileName = 'kvitas_' . time() . '.pdf';
}
$path = storage_path('checks/' . $fileName . '.pdf');
$this->renderCheckPdf($student, $check)->save($path);
return $path;
}
private function renderCheckPdf(Student $student, Check $check = null)
{
$bills = $this->billRepository->getCurrentSeasonBills($student->id);
$payments = $this->paymentRepository->getCurrentSeasonPayments($student->id);
$fileName = $student->fullname . ' kvitas';
$billsSum = $bills->sum('sum');
$paymentsSum = $payments->sum('sum');
if (empty($check)) {
$check = !empty($student->checks[0]) ? $student->checks[0] : false;
}
return newPDF::loadView('student.check_pdf', compact('student', 'payments', 'bills', 'billsSum', 'paymentsSum', 'fileName', 'check'));
}
Then I just loop through each user and call saveCheckPdf method in my controller like this
$this->pdfService->saveCheckPdf($student, null, $student->id . ' ' . $student->fullname . ' ' . time());
Maybe anyone faced the same problem?
I've this github package for my Laravel application, using composer, which creates sitemaps, but I am not able to understand where those sitemaps are rendered.
I had luck in finding the main sitemap after hitting /sitemap.xml URL location of my website, but I am unable to find other sitemaps like Google News sitemap one, which is supported by the package.
The responsible function seems this one, but my coding skills restrict me from understanding the rendered path:
/**
* Returns document with all sitemap items from $items array
*
* #param string $format (options: xml, html, txt, ror-rss, ror-rdf, google-news)
* #param string $style (path to custom xls style like '/styles/xsl/xml-sitemap.xsl')
*
* #return View
*/
public function render($format = 'xml', $style = null)
{
// limit size of sitemap
if ($this->model->getMaxSize() > 0 && count($this->model->getItems()) > $this->model->getMaxSize())
{
$this->model->limitSize($this->model->getMaxSize());
}
else if ('google-news' == $format && count($this->model->getItems()) > 1000)
{
$this->model->limitSize(1000);
}
else if ('google-news' != $format && count($this->model->getItems()) > 50000)
{
$this->model->limitSize();
}
$data = $this->generate($format, $style);
if ('html' == $format)
{
return $data['content'];
}
return $this->response->make($data['content'], 200, $data['headers']);
}
Im generating my sitemap on HomeController, with this function:
public function sitemap()
{
$settings_general = Utils::getSettings("general");
if ($settings_general->generate_sitemap == 1) {
// create new sitemap object
$sitemap = App::make("sitemap");
// get all posts from db
$posts = DB::table('posts')->orderBy('created_at', 'desc')->limit(600)->get();
// add every post to the sitemap
foreach ($posts as $post) {
$sitemap->add(URL::to('/') . "/" . $post->slug, $post->updated_at, '1', 'hourly', null, $post->title);
}
$pages = DB::table('pages')->orderBy('created_at', 'desc')->get();
// add every page to the sitemap
foreach ($pages as $page) {
$sitemap->add(URL::to('/') . "/" . $page->slug, $page->updated_at, '1', 'hourly', null, $page->title);
}
$categories = DB::table('categories')->orderBy('created_at', 'desc')->get();
// add every category to the sitemap
foreach ($categories as $category) {
$sub_categories = SubCategories::where('parent_id', $category->id)->get();
$sitemap->add(URL::to('/') . "/category/" . $category->slug, $category->updated_at, '1', 'hourly', null, $category->title);
foreach ($sub_categories as $sub_category) {
$sitemap->add(URL::to('/') . "/category/" . $category->slug . "/" . $sub_category->slug, $category->updated_at, '1', 'hourly', null, $category->title);
}
}
return $sitemap->render('xml');
}
}
I noticed that joomla tag input field is quite stupid. It loads everything from db, in this case 9K tags. Obviously ui becomes so slow.
Any ideas how to fix it? it seems there is already an ajax functionality present, so why not rely on that completely? J ways are crazy.
1 idea is to modify getOption method, and load only the tags that are related to current article editor is editing.
But in this context I don't seem to have article id.
Any ideas how to solve situation? I'm sure some of you've run into this :S
/**
* Method to get a list of tags
*
* #return array The field option objects.
*
* #since 3.1
*/
protected function getOptions()
{
$published = $this->element['published']? $this->element['published'] : array(0,1);
$db = JFactory::getDbo();
$query = $db->getQuery(true)
->select('DISTINCT a.id AS value, a.path, a.title AS text, a.level, a.published, a.lft')
->from('#__tags AS a')
->join('LEFT', $db->qn('#__tags') . ' AS b ON a.lft > b.lft AND a.rgt < b.rgt');
// Filter language
if (!empty($this->element['language']))
{
$query->where('a.language = ' . $db->q($this->element['language']));
}
$query->where($db->qn('a.lft') . ' > 0');
// Filter on the published state
if (is_numeric($published))
{
$query->where('a.published = ' . (int) $published);
}
elseif (is_array($published))
{
JArrayHelper::toInteger($published);
$query->where('a.published IN (' . implode(',', $published) . ')');
}
$query->order('a.lft ASC');
// Get the options.
$db->setQuery($query);
try
{
$options = $db->loadObjectList();
}
catch (RuntimeException $e)
{
return false;
}
// Block the possibility to set a tag as it own parent
if ($this->form->getName() == 'com_tags.tag')
{
$id = (int) $this->form->getValue('id', 0);
foreach ($options as $option)
{
if ($option->value == $id)
{
$option->disable = true;
}
}
}
// Merge any additional options in the XML definition.
$options = array_merge(parent::getOptions(), $options);
// Prepare nested data
if ($this->isNested())
{
$this->prepareOptionsNested($options);
}
else
{
$options = JHelperTags::convertPathsToNames($options);
}
return $options;
}
So I've modified list that gets preloaded, only to load tags that are present in the article (saved as belonging to article). Autocomplete still works with ajax so no loss of functionality there.
I am making an application in which i am uploading file from my custom tab as follows
i created a observer for event called catalog_product_save_after. In the particular function, i am uploading those files to catalog/product folder and its get uploaded to those folder without any error.
But when i am trying to assign those images to that particular product, i got the following error.
File was not uploaded at file E:\xampp\htdocs\magento\lib\Varien\File\Uploader.php on line 152
If i check system.log then i see the 2012-08-24T11:33:15+00:00 ERR (3): User Error: Some transactions have not been committed or rolled back in E:\xampp\htdocs\magento\lib\Varien\Db\Adapter\Pdo\Mysql.php on line 3645 this error.
My observer function is as follows
public function Savedata($observer)
{
$params = Mage::app()->getRequest()->getParams();
$product = $observer->getEvent()->getProduct();
$product_id = $product->getId();
$filesData = array();
$keysData = array_keys($_FILES);
foreach($keysData as $keys)
{
$uploader = new Varien_File_Uploader("$keys");
$uploader->setAllowedExtensions(array('jpeg', 'jpg', 'png', 'tiff'));
$uploader->setAllowRenameFiles(true);
$uploader->setFilesDispersion(false);
$path = Mage::getBaseDir('media') . DS . "catalog" . DS . "product";
if(!is_dir($path))
{
mkdir($path);
}
$extension = pathinfo($_FILES["$keys"]['name'], PATHINFO_EXTENSION);
$date = new DateTime();
$file_name = $keys . "_" . $product_id . "." . $extension;
$_FILES["$keys"]['name'] = $file_name;
$uploader->save($path, $_FILES["$keys"]['name']);
$filesData[] = $path .DS. $_FILES["$keys"]['name'];
}
print_r($filesData);
///till this works file.. when i add following code, program dies.
try
{
$productData = Mage::getModel('catalog/product')->load($product_id);
print_r($productData->getData());
foreach($filesData as $file)
{
$productData->addImageToMediaGallery($file, "image" ,false, false);
}
$productData->save();
$productData = Mage::getModel('catalog/product')->load($product_id);
print_r($productData->getData());
}
catch (Exception $e)
{
echo $e->getMessage() . "||" . $e->getCode() . "||" . $e->getFile() . "||" . $e->getLine();
}
exit();
}
any suggestion where i am going wrong? why this error occur?
i am using magento 1.7
I see that you're going to update your catalog/product object when you're saving it.
You are using event catalog_product_save_after.
Inside the observer, you call:
$productData = Mage::getModel('catalog/product')->load($product_id);
...
$productData->save();
$productData is catalog/product object -> you saved it.
You can guess what will happen, it will trigger event catalog_product_save_after and so on, looping forever.
I see that you call $productData->save(); because in this event, the value has been saved and can not be changed.
So instead of using catalog_product_save_after, you can use catalog_product_save_before.
The same like your code, but remove $productData->save(); because it will automatically call save.
Mage_Core_Model_Abstract
public function save()
{
/**
* Direct deleted items to delete method
*/
if ($this->isDeleted()) {
return $this->delete();
}
if (!$this->_hasModelChanged()) {
return $this;
}
$this->_getResource()->beginTransaction();
$dataCommited = false;
try {
$this->_beforeSave();
if ($this->_dataSaveAllowed) {
//see this line below, it will call save, so you don't need to call save in your `catalog_product_save_before` (no looping forever)
$this->_getResource()->save($this);
$this->_afterSave();
}
$this->_getResource()->addCommitCallback(array($this, 'afterCommitCallback'))
->commit();
$this->_hasDataChanges = false;
$dataCommited = true;
} catch (Exception $e) {
$this->_getResource()->rollBack();
$this->_hasDataChanges = true;
throw $e;
}
if ($dataCommited) {
$this->_afterSaveCommit();
}
return $this;
}
I would like to know if magento logs the error rows anywhere when doing an import.
If so, where are the logs files, and how do I turn it on?
Looking at the method definition in app/Mage.php
you will find
/**
* log facility (??)
*
* #param string $message
* #param integer $level
* #param string $file
* #param bool $forceLog
*/
public static function log($message, $level = null, $file = '', $forceLog = false)
{
if (!self::getConfig()) {
return;
}
try {
$logActive = self::getStoreConfig('dev/log/active');
if (empty($file)) {
$file = self::getStoreConfig('dev/log/file');
}
}
catch (Exception $e) {
$logActive = true;
}
if (!self::$_isDeveloperMode && !$logActive && !$forceLog) {
return;
}
static $loggers = array();
$level = is_null($level) ? Zend_Log::DEBUG : $level;
$file = empty($file) ? 'system.log' : $file;
try {
if (!isset($loggers[$file])) {
$logDir = self::getBaseDir('var') . DS . 'log';
$logFile = $logDir . DS . $file;
if (!is_dir($logDir)) {
mkdir($logDir);
chmod($logDir, 0777);
}
if (!file_exists($logFile)) {
file_put_contents($logFile, '');
chmod($logFile, 0777);
}
$format = '%timestamp% %priorityName% (%priority%): %message%' . PHP_EOL;
$formatter = new Zend_Log_Formatter_Simple($format);
$writerModel = (string)self::getConfig()->getNode('global/log/core/writer_model');
if (!self::$_app || !$writerModel) {
$writer = new Zend_Log_Writer_Stream($logFile);
}
else {
$writer = new $writerModel($logFile);
}
$writer->setFormatter($formatter);
$loggers[$file] = new Zend_Log($writer);
}
if (is_array($message) || is_object($message)) {
$message = print_r($message, true);
}
$loggers[$file]->log($message, $level);
}
catch (Exception $e) {
}
}
so logging to your own code, logging becomes as easy as
Mage::log($variable, null, 'yourfile.log', 1);
The 1 as the fourth option is completely option and supersedes the option of turning the logging off in the admin and will always force the system to write to your log. Everything for logging is done in HTTP_ROOT/var/log/
Logging is turned on by default, but if you are using a custom extension and need to insert logging, you can do the above. Any logging by Magento will always be in var/log/exception.log or var/log/system.log but most things will go to system.log unless it is something that uses throwException(), which goes to exception.log
Use direct import of products you will see error row no if any error exists on your screen..
Its fast method to import products
You can active the Exception.log file through Developer config menu.
In magento two file generated for log system.log file and exception.log file both file you find in var/log folder
I tried setting $_debugMode = true in the Mage_ImportExport_Model_Abstract model but it didn't create any logs in my case. I was uploading products using DataFlow method.
Then what I did was to change the Mage_Adminhtml_System_Convert_ProfileController a litte bit. Inside batchRunAction() it generates the errors as
try {
$importData = $batchImportModel->getBatchData();
$adapter->saveRow($importData);
} catch (Exception $e) {
$errors[] = $e->getMessage();
continue;
}
I logged the $errors array along with the current product sku to get what I needed, i.e.,
Mage::log($importData['sku'].' - '.$e->getMessage())