Enhance SaleSection component to manage selling state with improved user feedback. Disable buttons during selling, show status messages for sellAll actions, and update translations for new statuses.
This commit is contained in:
230
PERFORMANCE_ANALYSIS.md
Normal file
230
PERFORMANCE_ANALYSIS.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user