7.9 KiB
7.9 KiB
Backend Performance-Analyse: Sell-Funktionen
Identifizierte Performance-Probleme
1. N+1 Query Problem in sellAllProducts()
Problem:
Die Funktion sellAllProducts() macht für jedes Inventory-Item mehrere separate Datenbankabfragen:
-
Erste Schleife (Zeile 1702-1711):
calcRegionalSellPrice()→ machtTownProductWorth.findOne()für jedes ItemgetCumulativeTaxPercentWithExemptions()→ macht mehrere Queries pro Item:FalukantCharacter.findOne()PoliticalOffice.findAll()mit Includes- Rekursive SQL-Query für Steuerberechnung
addSellItem()→ machtBranch.findOne()undDaySell.findOne()/create()für jedes Item
-
Zweite Schleife (Zeile 1714-1724):
RegionData.findOne()für jedes ItemgetCumulativeTaxPercent()→ rekursive SQL-Query für jedes ItemcalcRegionalSellPrice()→ erneutTownProductWorth.findOne()für jedes Item
Beispiel: Bei 10 Items werden gemacht:
- 10x
TownProductWorth.findOne()(2x pro Item) - 10x
RegionData.findOne() - 10x
getCumulativeTaxPercentWithExemptions()(mit mehreren Queries) - 10x
getCumulativeTaxPercent()(rekursive SQL) - 10x
addSellItem()(mit 2 Queries pro Item) - = ~70+ Datenbankabfragen für 10 Items
2. Ineffiziente addSellItem() Implementierung
Problem:
- Wird für jedes Item einzeln aufgerufen
- Macht
Branch.findOne()für jedes Item (könnte gecacht werden) DaySell.findOne()undcreate()/update()für jedes Item
Lösung: Batch-Operation implementieren, die alle DaySell Einträge auf einmal verarbeitet.
3. Doppelte Berechnungen in sellAllProducts()
Problem:
- Preis wird zweimal berechnet (Zeile 1705 und 1718)
- Steuer wird zweimal berechnet (Zeile 1706 und 1717)
calcRegionalSellPrice()wird zweimal aufgerufen mit denselben Parametern
4. Fehlende Indizes
Potenzielle fehlende Indizes:
falukant_data.town_product_worth(product_id, region_id)- sollte unique seinfalukant_data.inventory(stock_id, product_id, quality)- für schnelle Lookupsfalukant_data.knowledge(character_id, product_id)- für Knowledge-Lookupsfalukant_data.political_office(character_id)- für Steuerbefreiungen
5. Ineffiziente getCumulativeTaxPercentWithExemptions()
Problem:
- Lädt alle PoliticalOffices jedes Mal neu, auch wenn sich nichts geändert hat
- Macht komplexe rekursive SQL-Query für jedes Item separat
- Könnte gecacht werden (z.B. pro User+Region Kombination)
Empfohlene Optimierungen
1. Batch-Loading für sellAllProducts()
async sellAllProducts(hashedUserId, branchId) {
// ... existing code ...
// Batch-Load alle benötigten Daten VOR den Schleifen
const regionIds = [...new Set(inventory.map(item => item.stock.branch.regionId))];
const productIds = [...new Set(inventory.map(item => item.productType.id))];
// 1. Lade alle TownProductWorth Einträge auf einmal
const townWorths = await TownProductWorth.findAll({
where: {
productId: { [Op.in]: productIds },
regionId: { [Op.in]: regionIds }
}
});
const worthMap = new Map();
townWorths.forEach(tw => {
worthMap.set(`${tw.productId}-${tw.regionId}`, tw.worthPercent);
});
// 2. Lade alle RegionData auf einmal
const regions = await RegionData.findAll({
where: { id: { [Op.in]: regionIds } }
});
const regionMap = new Map(regions.map(r => [r.id, r]));
// 3. Berechne Steuern für alle Regionen auf einmal
const taxMap = new Map();
for (const regionId of regionIds) {
const tax = await getCumulativeTaxPercentWithExemptions(falukantUser.id, regionId);
taxMap.set(regionId, tax);
}
// 4. Berechne Preise und Steuern in einer Schleife
const sellItems = [];
for (const item of inventory) {
const regionId = item.stock.branch.regionId;
const worthPercent = worthMap.get(`${item.productType.id}-${regionId}`) || 50;
const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0;
const pricePerUnit = calcRegionalSellPrice(item.productType, knowledgeVal, regionId, worthPercent);
const cumulativeTax = taxMap.get(regionId);
// ... rest of calculation ...
sellItems.push({ branchId: item.stock.branch.id, productId: item.productType.id, quantity: item.quantity });
}
// 5. Batch-Update DaySell Einträge
await this.addSellItemsBatch(sellItems);
// ... rest of code ...
}
2. Batch-Operation für addSellItem()
async addSellItemsBatch(sellItems) {
// Gruppiere nach (regionId, productId, sellerId)
const grouped = new Map();
for (const item of sellItems) {
const branch = await Branch.findByPk(item.branchId);
if (!branch) continue;
const key = `${branch.regionId}-${item.productId}-${item.sellerId}`;
if (!grouped.has(key)) {
grouped.set(key, {
regionId: branch.regionId,
productId: item.productId,
sellerId: item.sellerId,
quantity: 0
});
}
grouped.get(key).quantity += item.quantity;
}
// Batch-Update oder Create
for (const [key, data] of grouped) {
const [daySell, created] = await DaySell.findOrCreate({
where: {
regionId: data.regionId,
productId: data.productId,
sellerId: data.sellerId
},
defaults: { quantity: data.quantity }
});
if (!created) {
daySell.quantity += data.quantity;
await daySell.save();
}
}
}
3. Caching für getCumulativeTaxPercentWithExemptions()
// Cache für Steuerberechnungen (z.B. 5 Minuten)
const taxCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 Minuten
async function getCumulativeTaxPercentWithExemptions(userId, regionId) {
const cacheKey = `${userId}-${regionId}`;
const cached = taxCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.value;
}
// ... existing calculation ...
taxCache.set(cacheKey, { value: tax, timestamp: Date.now() });
return tax;
}
4. Optimierte calcRegionalSellPrice()
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
// Wenn worthPercent nicht übergeben wurde UND wir es nicht aus dem Cache haben,
// dann hole es aus der DB
if (worthPercent === null) {
const townWorth = await TownProductWorth.findOne({
where: { productId: product.id, regionId: regionId }
});
worthPercent = townWorth?.worthPercent || 50;
}
// ... rest of calculation ...
}
5. Datenbank-Indizes hinzufügen
-- Index für town_product_worth (sollte unique sein)
CREATE UNIQUE INDEX IF NOT EXISTS idx_town_product_worth_product_region
ON falukant_data.town_product_worth(product_id, region_id);
-- Index für inventory Lookups
CREATE INDEX IF NOT EXISTS idx_inventory_stock_product_quality
ON falukant_data.inventory(stock_id, product_id, quality);
-- Index für knowledge Lookups
CREATE INDEX IF NOT EXISTS idx_knowledge_character_product
ON falukant_data.knowledge(character_id, product_id);
-- Index für political_office Lookups
CREATE INDEX IF NOT EXISTS idx_political_office_character
ON falukant_data.political_office(character_id);
Geschätzter Performance-Gewinn
- Vorher: ~70+ Queries für 10 Items
- Nachher: ~15-20 Queries für 10 Items (Batch-Loading + Caching)
- Geschätzte Verbesserung: 70-80% weniger Datenbankabfragen
Priorität
- Hoch: Batch-Loading für
sellAllProducts()(größter Impact) - Hoch: Batch-Operation für
addSellItem() - Mittel: Caching für Steuerberechnungen
- Mittel: Datenbank-Indizes
- Niedrig: Doppelte Berechnungen entfernen