perf: fix critical O(n²) bottleneck in mergeSalesAndAdsData

- The ads-only metadata lookup was doing a nested forEach (O(ads × sales))
- Now pre-builds asinMetadata map during sales aggregation for O(1) lookups
- This reduces complexity from O(n²) to O(n) for large datasets
- Should dramatically improve loading time for Weekly Sales tab
This commit is contained in:
Christian Vidal Wolf
2026-01-22 11:52:03 +01:00
parent 3586e3cdd4
commit fa69a68b57
+15 -14
View File
@@ -446,6 +446,9 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
const createKey = (asin: string, customer: string, year: number, week: number) => const createKey = (asin: string, customer: string, year: number, week: number) =>
`${asin.trim().toUpperCase()}|${customer.trim().toUpperCase()}|${year}|${week}`; `${asin.trim().toUpperCase()}|${customer.trim().toUpperCase()}|${year}|${week}`;
// Pre-build metadata lookup map for O(1) access (instead of O(n) nested loop)
const asinMetadata = new Map<string, { sku: string; title: string; line: string }>();
// 1. Aggregate Sales by ASIN|Customer|Year|Week (combine all SKUs) // 1. Aggregate Sales by ASIN|Customer|Year|Week (combine all SKUs)
const salesMap = new Map<string, { const salesMap = new Map<string, {
sellOut: number; sellOut: number;
@@ -462,15 +465,21 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
salesData.forEach(sale => { salesData.forEach(sale => {
const weekNum = sale.week || 0; const weekNum = sale.week || 0;
if (weekNum === 0) return; // Skip records without week data for weekly analysis if (weekNum === 0) return;
const key = createKey(sale.asin, sale.customer, sale.year, weekNum); const key = createKey(sale.asin, sale.customer, sale.year, weekNum);
// Build metadata map: ASIN|Customer -> best metadata
const metaKey = `${sale.asin.trim().toUpperCase()}|${sale.customer.trim().toUpperCase()}`;
const existingMeta = asinMetadata.get(metaKey);
if (!existingMeta || (sale.title && sale.title.length > (existingMeta.title?.length || 0))) {
asinMetadata.set(metaKey, { sku: sale.sku, title: sale.title, line: sale.line });
}
if (salesMap.has(key)) { if (salesMap.has(key)) {
const existing = salesMap.get(key)!; const existing = salesMap.get(key)!;
existing.sellOut += sale.sellOut; existing.sellOut += sale.sellOut;
existing.units += sale.units; existing.units += sale.units;
// Keep the best metadata (longest title, first non-empty SKU)
if (sale.title && sale.title.length > (existing.title?.length || 0)) { if (sale.title && sale.title.length > (existing.title?.length || 0)) {
existing.title = sale.title; existing.title = sale.title;
} }
@@ -513,7 +522,7 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
const mergedData: CombinedKPIs[] = []; const mergedData: CombinedKPIs[] = [];
const processedKeys = new Set<string>(); const processedKeys = new Set<string>();
// 3. Create ONE record per ASIN/Customer/Year/Week from sales, attach ads if available // 3. Create ONE record per ASIN/Customer/Year/Week from sales
salesMap.forEach((sale, key) => { salesMap.forEach((sale, key) => {
processedKeys.add(key); processedKeys.add(key);
const ad = adsMap.get(key); const ad = adsMap.get(key);
@@ -529,7 +538,6 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
const salesOrganic = Math.max(0, salesTotal - adSales); const salesOrganic = Math.max(0, salesTotal - adSales);
const unitsOrganic = Math.max(0, unitsTotal - adUnits); const unitsOrganic = Math.max(0, unitsTotal - adUnits);
// KPIs
const acos = adSales > 0 ? (adCost / adSales) * 100 : 0; const acos = adSales > 0 ? (adCost / adSales) * 100 : 0;
const tacos = salesTotal > 0 ? (adCost / salesTotal) * 100 : 0; const tacos = salesTotal > 0 ? (adCost / salesTotal) * 100 : 0;
const roas = adCost > 0 ? adSales / adCost : 0; const roas = adCost > 0 ? adSales / adCost : 0;
@@ -568,18 +576,11 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
}); });
}); });
// 4. Add ads-only records (ASINs with ads but no sales in the filtered data) // 4. Add ads-only records - use O(1) metadata lookup instead of O(n) loop
adsMap.forEach((ad, key) => { adsMap.forEach((ad, key) => {
if (!processedKeys.has(key)) { if (!processedKeys.has(key)) {
// Look up metadata from sales data for this ASIN (any week) const metaKey = `${ad.asin.trim().toUpperCase()}|${ad.country.trim().toUpperCase()}`;
let meta: { sku: string; title: string; line: string } | undefined; const meta = asinMetadata.get(metaKey);
salesMap.forEach((sale, saleKey) => {
if (saleKey.startsWith(ad.asin.trim().toUpperCase() + '|' + ad.country.trim().toUpperCase())) {
if (!meta || sale.title?.length > meta.title?.length) {
meta = { sku: sale.sku, title: sale.title, line: sale.line };
}
}
});
mergedData.push({ mergedData.push({
id: `ads-only-${key}`, id: `ads-only-${key}`,