Skip to content

Commit 336e90e

Browse files
committed
ACP2E-4153: [Cloud] Sitemap generation performance is significantly degraded
1 parent b9f5d6f commit 336e90e

File tree

2 files changed

+298
-20
lines changed

2 files changed

+298
-20
lines changed

app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php

Lines changed: 126 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2011 Adobe
4+
* All Rights Reserved.
55
*/
66
namespace Magento\Sitemap\Model\ResourceModel\Catalog;
77

88
use Magento\Catalog\Model\Product\Image\UrlBuilder;
99
use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
1010
use Magento\Framework\App\ObjectManager;
11+
use Magento\Framework\DataObject;
12+
use Magento\Framework\Exception\LocalizedException;
13+
use Magento\Sitemap\Model\SitemapConfigReaderInterface;
1114
use Magento\Store\Model\Store;
1215

1316
/**
@@ -21,6 +24,11 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
2124
{
2225
public const NOT_SELECTED_IMAGE = 'no_selection';
2326

27+
/**
28+
* Batch size for loading product images to avoid database IN() clause limits
29+
*/
30+
private const IMAGE_BATCH_SIZE = 1000;
31+
2432
/**
2533
* Collection Zend Db select
2634
*
@@ -35,6 +43,13 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
3543
*/
3644
protected $_attributesCache = [];
3745

46+
/**
47+
* Cached product images to avoid N+1 queries
48+
*
49+
* @var array
50+
*/
51+
private $_productImagesCache = [];
52+
3853
/**
3954
* @var \Magento\Catalog\Model\Product\Gallery\ReadHandler
4055
* @since 100.1.0
@@ -46,6 +61,11 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
4661
*/
4762
protected $_sitemapData = null;
4863

64+
/**
65+
* @var SitemapConfigReaderInterface
66+
*/
67+
private $sitemapConfigReader;
68+
4969
/**
5070
* @var \Magento\Catalog\Model\ResourceModel\Product
5171
*/
@@ -107,6 +127,7 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
107127
* @param \Magento\Framework\App\Config\ScopeConfigInterface|null $scopeConfig
108128
* @param UrlBuilder $urlBuilder
109129
* @param ProductSelectBuilder $productSelectBuilder
130+
* @param SitemapConfigReaderInterface $sitemapConfigReader
110131
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
111132
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
112133
*/
@@ -125,7 +146,8 @@ public function __construct(
125146
?\Magento\Catalog\Helper\Image $catalogImageHelper = null,
126147
?\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null,
127148
?UrlBuilder $urlBuilder = null,
128-
?ProductSelectBuilder $productSelectBuilder = null
149+
?ProductSelectBuilder $productSelectBuilder = null,
150+
?SitemapConfigReaderInterface $sitemapConfigReader = null
129151
) {
130152
$this->_productResource = $productResource;
131153
$this->_storeManager = $storeManager;
@@ -138,6 +160,8 @@ public function __construct(
138160
$this->imageUrlBuilder = $urlBuilder ?? ObjectManager::getInstance()->get(UrlBuilder::class);
139161
$this->productSelectBuilder = $productSelectBuilder ??
140162
ObjectManager::getInstance()->get(ProductSelectBuilder::class);
163+
$this->sitemapConfigReader = $sitemapConfigReader ??
164+
ObjectManager::getInstance()->get(SitemapConfigReaderInterface::class);
141165

142166
parent::__construct($context, $connectionName);
143167
}
@@ -307,7 +331,7 @@ public function getCollection($storeId)
307331
$this->_addFilter($store->getId(), 'status', $this->_productStatus->getVisibleStatusIds(), 'in');
308332

309333
// Join product images required attributes
310-
$imageIncludePolicy = $this->_sitemapData->getProductImageIncludePolicy($store->getId());
334+
$imageIncludePolicy = $this->sitemapConfigReader->getProductImageIncludePolicy($store->getId());
311335
if (\Magento\Sitemap\Model\Source\Product\Image\IncludeImage::INCLUDE_NONE != $imageIncludePolicy) {
312336
$this->_joinAttribute($store->getId(), 'name', 'name');
313337
if (\Magento\Sitemap\Model\Source\Product\Image\IncludeImage::INCLUDE_ALL == $imageIncludePolicy) {
@@ -318,7 +342,23 @@ public function getCollection($storeId)
318342
}
319343

320344
$query = $connection->query($this->prepareSelectStatement($this->_select));
345+
346+
// First, collect all product data without loading images
347+
$productRows = [];
348+
$productIds = [];
321349
while ($row = $query->fetch()) {
350+
$productRows[] = $row;
351+
$productIds[] = $row[$this->getIdFieldName()];
352+
}
353+
354+
// Pre-load all images in batch to avoid N+1 queries
355+
$imageIncludePolicy = $this->sitemapConfigReader->getProductImageIncludePolicy($store->getId());
356+
if (\Magento\Sitemap\Model\Source\Product\Image\IncludeImage::INCLUDE_NONE != $imageIncludePolicy) {
357+
$this->_preloadAllProductImages($productIds, $store->getId());
358+
}
359+
360+
// Now create products with cached image data
361+
foreach ($productRows as $row) {
322362
$product = $this->_prepareProduct($row, $store->getId());
323363
$products[$product->getId()] = $product;
324364
}
@@ -332,12 +372,12 @@ public function getCollection($storeId)
332372
* @param array $productRow
333373
* @param int $storeId
334374
*
335-
* @return \Magento\Framework\DataObject
375+
* @return DataObject
336376
* @throws \Magento\Framework\Exception\LocalizedException
337377
*/
338378
protected function _prepareProduct(array $productRow, $storeId)
339379
{
340-
$product = new \Magento\Framework\DataObject();
380+
$product = new DataObject();
341381

342382
$product['id'] = $productRow[$this->getIdFieldName()];
343383
if (empty($productRow['url'])) {
@@ -352,16 +392,14 @@ protected function _prepareProduct(array $productRow, $storeId)
352392
/**
353393
* Load product images
354394
*
355-
* @param \Magento\Framework\DataObject $product
395+
* @param DataObject $product
356396
* @param int $storeId
357397
* @return void
358398
*/
359399
protected function _loadProductImages($product, $storeId)
360400
{
361401
$this->_storeManager->setCurrentStore($storeId);
362-
/** @var $helper \Magento\Sitemap\Helper\Data */
363-
$helper = $this->_sitemapData;
364-
$imageIncludePolicy = $helper->getProductImageIncludePolicy($storeId);
402+
$imageIncludePolicy = $this->sitemapConfigReader->getProductImageIncludePolicy($storeId);
365403

366404
// Get product images
367405
$imagesCollection = [];
@@ -372,7 +410,7 @@ protected function _loadProductImages($product, $storeId)
372410
$product->getImage() != self::NOT_SELECTED_IMAGE
373411
) {
374412
$imagesCollection = [
375-
new \Magento\Framework\DataObject(
413+
new DataObject(
376414
['url' => $this->getProductImageUrl($product->getImage())]
377415
),
378416
];
@@ -388,7 +426,7 @@ protected function _loadProductImages($product, $storeId)
388426
}
389427

390428
$product->setImages(
391-
new \Magento\Framework\DataObject(
429+
new DataObject(
392430
['collection' => $imagesCollection, 'title' => $product->getName(), 'thumbnail' => $thumbnail]
393431
)
394432
);
@@ -404,22 +442,38 @@ protected function _loadProductImages($product, $storeId)
404442
*/
405443
protected function _getAllProductImages($product, $storeId)
406444
{
407-
$product->setStoreId($storeId);
408-
$gallery = $this->mediaGalleryResourceModel->loadProductGalleryByAttributeId(
409-
$product,
410-
$this->mediaGalleryReadHandler->getAttribute()->getId()
411-
);
412-
445+
$productId = $product->getId();
413446
$imagesCollection = [];
414-
if ($gallery) {
447+
448+
// Use cached images if available (from batch loading)
449+
if (isset($this->_productImagesCache[$productId])) {
450+
$gallery = $this->_productImagesCache[$productId];
415451
foreach ($gallery as $image) {
416-
$imagesCollection[] = new \Magento\Framework\DataObject(
452+
$imagesCollection[] = new DataObject(
417453
[
418454
'url' => $this->getProductImageUrl($image['file']),
419455
'caption' => $image['label'] ? $image['label'] : $image['label_default'],
420456
]
421457
);
422458
}
459+
} else {
460+
// Fallback to individual query (should rarely happen now)
461+
$product->setStoreId($storeId);
462+
$gallery = $this->mediaGalleryResourceModel->loadProductGalleryByAttributeId(
463+
$product,
464+
$this->mediaGalleryReadHandler->getAttribute()->getId()
465+
);
466+
467+
if ($gallery) {
468+
foreach ($gallery as $image) {
469+
$imagesCollection[] = new DataObject(
470+
[
471+
'url' => $this->getProductImageUrl($image['file']),
472+
'caption' => $image['label'] ? $image['label'] : $image['label_default'],
473+
]
474+
);
475+
}
476+
}
423477
}
424478

425479
return $imagesCollection;
@@ -449,6 +503,58 @@ public function prepareSelectStatement(\Magento\Framework\DB\Select $select)
449503
return $select;
450504
}
451505

506+
/**
507+
* Pre-load all product images in batched queries to avoid N+1 problem while respecting DB limits
508+
*
509+
* @param array $productIds
510+
* @param int $storeId
511+
* @return void
512+
* @throws LocalizedException
513+
*/
514+
private function _preloadAllProductImages($productIds, $storeId)
515+
{
516+
if (empty($productIds)) {
517+
return;
518+
}
519+
520+
// Split into smaller batches to avoid hitting database IN() clause limits
521+
$productBatches = array_chunk($productIds, self::IMAGE_BATCH_SIZE);
522+
523+
$linkField = $this->_productResource->getLinkField();
524+
$connection = $this->getConnection();
525+
526+
foreach ($productBatches as $batch) {
527+
// Use the existing createBatchBaseSelect method for each batch
528+
$select = $this->mediaGalleryResourceModel->createBatchBaseSelect(
529+
$storeId,
530+
$this->mediaGalleryReadHandler->getAttribute()->getId()
531+
);
532+
533+
$select->where('entity.' . $linkField . ' IN (?)', $batch);
534+
535+
// Add ordering to ensure consistent results
536+
$select->order(['entity.' . $linkField, 'position']);
537+
538+
$result = $connection->fetchAll($select);
539+
540+
// Group images by product ID
541+
foreach ($result as $row) {
542+
$productId = $row[$linkField];
543+
if (!isset($this->_productImagesCache[$productId])) {
544+
$this->_productImagesCache[$productId] = [];
545+
}
546+
$this->_productImagesCache[$productId][] = $row;
547+
}
548+
}
549+
550+
// Ensure all requested products have an entry (even if empty)
551+
foreach ($productIds as $productId) {
552+
if (!isset($this->_productImagesCache[$productId])) {
553+
$this->_productImagesCache[$productId] = [];
554+
}
555+
}
556+
}
557+
452558
/**
453559
* Get product image URL from image filename
454560
*

0 commit comments

Comments
 (0)