231 lines
7.9 KiB
Markdown
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
|
|
|