Files
yourpart3/PERFORMANCE_ANALYSIS.md

231 lines
7.9 KiB
Markdown

# 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()`**
```javascript
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()`**
```javascript
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()`**
```javascript
// 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()`**
```javascript
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**
```sql
-- 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