fix(product): Update performance issue (#14150)

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2025-12-01 16:45:18 +01:00
committed by GitHub
parent 3cf1e5e9de
commit 0f835381e9
3 changed files with 168 additions and 5 deletions

View File

@@ -309,6 +309,7 @@ export function moduleIntegrationTestRunner<TService = any>({
moduleDependencies,
joinerConfig = [],
schema = "public",
dbName,
debug = false,
testSuite,
resolve,
@@ -337,6 +338,7 @@ export function moduleIntegrationTestRunner<TService = any>({
moduleDependencies,
joinerConfig,
schema,
dbName,
debug,
resolve,
injectedDependencies,

View File

@@ -11,6 +11,7 @@ import {
ProductImage,
ProductType,
} from "@models"
import { setTimeout } from "timers/promises"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { UpdateProductInput } from "@types"
@@ -24,6 +25,8 @@ jest.setTimeout(300000)
moduleIntegrationTestRunner<IProductModuleService>({
moduleName: Modules.PRODUCT,
// dbName: "product_update_performance",
// debug: true,
testSuite: ({ MikroOrmWrapper, service }) => {
describe("ProductModuleService products", function () {
let productCollectionOne: ProductCollection
@@ -171,6 +174,162 @@ moduleIntegrationTestRunner<IProductModuleService>({
productTwo = res[1]
})
it.skip("test update performance", async () => {
const PRODUCT_COUNT = 1000
const VARIANTS_PER_PRODUCT = 100
const OPTION_VALUES_COUNT = 10 // 10 x 10 = 100 variant combinations
// Generate option values for 2 options
const sizeValues = Array.from(
{ length: OPTION_VALUES_COUNT },
(_, i) => `size-${i + 1}`
)
const colorValues = Array.from(
{ length: OPTION_VALUES_COUNT },
(_, i) => `color-${i + 1}`
)
// Generate all variant combinations
const generateVariants = () => {
const variants: {
title: string
sku: string
options: { size: string; color: string }
}[] = []
for (let s = 0; s < OPTION_VALUES_COUNT; s++) {
for (let c = 0; c < OPTION_VALUES_COUNT; c++) {
variants.push({
title: `Variant ${sizeValues[s]}-${colorValues[c]}`,
sku: `SKU-${sizeValues[s]}-${
colorValues[c]
}-${Date.now()}-${Math.random()}`,
options: {
size: sizeValues[s],
color: colorValues[c],
},
})
}
}
return variants
}
// Generate random number of images (10-50)
const generateImages = () => {
const imageCount = Math.floor(Math.random() * 41) + 10 // 10-50 images
return Array.from({ length: imageCount }, (_, i) => ({
url: `https://example.com/image-${
i + 1
}-${Date.now()}-${Math.random()}.jpg`,
}))
}
// Build product data
const productsData = Array.from(
{ length: PRODUCT_COUNT },
(_, i) => ({
title: `Performance Test Product ${i + 1}`,
handle: `perf-product-${i + 1}-${Date.now()}`,
status: ProductStatus.PUBLISHED,
options: [
{ title: "size", values: sizeValues },
{ title: "color", values: colorValues },
],
variants: generateVariants(),
images: generateImages(),
})
)
console.log(`Creating ${PRODUCT_COUNT} products...`)
console.log(`Each product has ${VARIANTS_PER_PRODUCT} variants`)
console.log(
`Each product has 2 options with ${OPTION_VALUES_COUNT} values each`
)
console.log(
`Each product has 10-50 images (random), total images: ${productsData.reduce(
(sum, p) => sum + p.images.length,
0
)}`
)
const startTime = Date.now()
// Create products in batches to avoid memory issues
const BATCH_SIZE = 10
const createdProducts: any[] = []
for (let i = 0; i < PRODUCT_COUNT; i += BATCH_SIZE) {
const batch = productsData.slice(i, i + BATCH_SIZE)
const batchStart = Date.now()
const products = await service.createProducts(batch)
createdProducts.push(...products)
const batchEnd = Date.now()
console.log(
`Batch ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(
PRODUCT_COUNT / BATCH_SIZE
)} created in ${batchEnd - batchStart}ms`
)
}
const createEndTime = Date.now()
console.log(`\nTotal creation time: ${createEndTime - startTime}ms`)
console.log(
`Average per product: ${
(createEndTime - startTime) / PRODUCT_COUNT
}ms`
)
// Retrieve a sample product to verify structure
const sampleProduct = await service.retrieveProduct(
createdProducts[0].id,
{
relations: ["variants", "images", "options", "options.values"],
}
)
console.log(`\nSample product verification:`)
console.log(` - Variants: ${sampleProduct.variants.length}`)
console.log(` - Options: ${sampleProduct.options.length}`)
console.log(` - Images: ${sampleProduct.images.length}`)
/**
* ----------------------------------------------------------------------------
* ----------------------------------------------------------------------------
* ----------------------------------------------------------------------------
*/
console.log(`IT IS TIME TO CLEAR THE LOGS`)
await setTimeout(2000)
const productToUpdateId = createdProducts[0].id
createdProducts[0].variants[0].title = "updated variant 1"
function formatVariantOptions(variant) {
const result = {}
for (const option of variant.options) {
result[option.option.title] = option.value
}
return result
}
createdProducts[0].variants.forEach((variant) => {
variant.options = formatVariantOptions(variant)
})
const now = performance.now()
await service.updateProducts(productToUpdateId, {
title: "updated title",
variants: createdProducts[0].variants,
})
const end = performance.now()
console.log(`Update time: ${end - now}ms`)
console.log("break")
}, 1000000)
it("should update multiple products", async () => {
await service.upsertProducts([
{ id: productOne.id, title: "updated title 1" },

View File

@@ -58,8 +58,8 @@ import {
UpdateTypeInput,
VariantImageInputArray,
} from "../types"
import { joinerConfig } from "./../joiner-config"
import { eventBuilders } from "../utils/events"
import { joinerConfig } from "./../joiner-config"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
@@ -1743,12 +1743,15 @@ export default class ProductModuleService
.registerSubscriber(new subscriber(sharedContext))
}
const productIds = data.map((d) => d.id).filter(Boolean)
const originalProducts = await this.productService_.list(
{
id: data.map((d) => d.id),
id: productIds,
},
{
relations: ["options", "options.values", "variants", "images", "tags"],
relations: ["options", "options.values", "tags"],
take: productIds.length,
},
sharedContext
)
@@ -2012,10 +2015,9 @@ export default class ProductModuleService
// Re map options to handle non serialized data as well
dbOptions =
originalProducts
?.map((originalProduct) =>
?.flatMap((originalProduct) =>
originalProduct.options.map((option) => option)
)
.flat()
.filter(Boolean) ?? []
}