Files
yourpart3/PERFORMANCE_ANALYSIS.md

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:

  1. Erste Schleife (Zeile 1702-1711):

    • calcRegionalSellPrice() → macht TownProductWorth.findOne() für jedes Item
    • getCumulativeTaxPercentWithExemptions() → macht mehrere Queries pro Item:
      • FalukantCharacter.findOne()
      • PoliticalOffice.findAll() mit Includes
      • Rekursive SQL-Query für Steuerberechnung
    • addSellItem() → macht Branch.findOne() und DaySell.findOne()/create() für jedes Item
  2. Zweite Schleife (Zeile 1714-1724):

    • RegionData.findOne() für jedes Item
    • getCumulativeTaxPercent() → rekursive SQL-Query für jedes Item
    • calcRegionalSellPrice() → erneut TownProductWorth.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() und create()/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 sein
  • falukant_data.inventory(stock_id, product_id, quality) - für schnelle Lookups
  • falukant_data.knowledge(character_id, product_id) - für Knowledge-Lookups
  • falukant_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

  1. Hoch: Batch-Loading für sellAllProducts() (größter Impact)
  2. Hoch: Batch-Operation für addSellItem()
  3. Mittel: Caching für Steuerberechnungen
  4. Mittel: Datenbank-Indizes
  5. Niedrig: Doppelte Berechnungen entfernen