개선(improvement) #2628
open개선(improvement) #2310: [BE] Optimize Memory, CPU API search list flight
[BE] CRITICAL-4: String Concatenation in Streams
0%
Description
Parent Issue: #2310 [BE] Optimize Memory, CPU API search list flight
- Summary
Inefficient string concatenation in loops creating 200-320 String objects per basket request. Each `+` operator creates new StringBuilder and String objects, adding significant garbage allocation overhead.
- Location
File: `web-api/src/main/java/com/ohmy/api/service/flight/FlightListItineraryService.java`
Primary Locations:
- Lines 2865-2903: Basket fare matching (main offender)
- Lines 1553-1558: `getSelectedJsonNodeCombinedFares()` - itemCode generation
- Lines 1691-1696: `getSelectedJsonNodeOnewayFares()` - itemCode generation
- Problem Code Examples
- Lines 2865-2903: Basket fare key concatenation
// String concatenation pattern repeated in loops String basketFareKey = basketFareItem.getGdsCompCode() + "//" + basketFareItem.getFareTypeCode() + "//" + basketFareItem.getFareBasisCode() + "//" + basketFareItem.getAdultFareAmount() + "//" + basketFareItem.getChildFareAmount() + "//" + basketFareItem.getInfantFareAmount(); // Multiple object allocations for each concatenation: // 1. StringBuilder creation // 2. Multiple append() calls // 3. Final toString() creates new String // 4. All intermediate Strings discarded
- Lines 1553-1558, 1691-1696: String.format() overhead
// String.format() creates Formatter object each call flightFareJsonNode.put("itemCode", String.format("%s_%s_%s_%s_%s", itineraryArrayNode.at("/0/segments/0/origin/cityCodeIata").textValue(), itineraryArrayNode.at("/0/segments/0/destination/cityCodeIata").textValue(), itineraryArrayNode.at("/0/segments/0/departureDate").textValue(), itineraryArrayNode.at("/0/segments/0/departureTime").textValue(), fareIndex));
- Performance Impact
Memory Allocation:
- Per basket request: 200-320 String objects created
- Per concatenation: 3-5 temporary objects (StringBuilder + intermediate Strings)
- String.format() overhead: New Formatter instance per call
Calculation Example (Basket Mode):
- 50 basket fares × 6 fields concatenated = 300 String operations
- Each operation creates ~3 temporary objects = 900 objects
- Average 40-80 bytes per String = 36-72 KB garbage per basket request
CPU Impact:
- StringBuilder allocation overhead
- Array resizing in StringBuilder
- String.format() parsing format string each time
- Required Fix
- 1. Use StringBuilder with Pre-calculated Capacity
// BEFORE: String concatenation
String basketFareKey = basketFareItem.getGdsCompCode() + "//" +
basketFareItem.getFareTypeCode() + "//" +
basketFareItem.getFareBasisCode() + "//" +
basketFareItem.getAdultFareAmount() + "//" +
basketFareItem.getChildFareAmount() + "//" +
basketFareItem.getInfantFareAmount();
// AFTER: StringBuilder with capacity
StringBuilder keyBuilder = new StringBuilder(128); // Pre-calculate expected size
keyBuilder.append(basketFareItem.getGdsCompCode())
.append("//")
.append(basketFareItem.getFareTypeCode())
.append("//")
.append(basketFareItem.getFareBasisCode())
.append("//")
.append(basketFareItem.getAdultFareAmount())
.append("//")
.append(basketFareItem.getChildFareAmount())
.append("//")
.append(basketFareItem.getInfantFareAmount());
String basketFareKey = keyBuilder.toString();
- 2. Replace String.format() with Direct Concatenation
// BEFORE: String.format() overhead
flightFareJsonNode.put("itemCode", String.format("%s_%s_%s_%s_%s",
originCity, destCity, departureDate, departureTime, fareIndex));
// AFTER: StringBuilder
StringBuilder itemCodeBuilder = new StringBuilder(64);
itemCodeBuilder.append(originCity)
.append('_')
.append(destCity)
.append('_')
.append(departureDate)
.append('_')
.append(departureTime)
.append('_')
.append(fareIndex);
flightFareJsonNode.put("itemCode", itemCodeBuilder.toString());
- 3. Cache Computed Keys (If Keys are Reused)
// If same keys are computed multiple times, cache them
private static final Map<String, String> fareKeyCache = new ConcurrentHashMap<>();
String cacheKey = compCode + "|" + fareType + "|" + fareBasis;
String basketFareKey = fareKeyCache.computeIfAbsent(cacheKey, k -> {
// Build key once, reuse many times
return buildFareKey(basketFareItem);
});
- 4. Extract to Helper Method
private String buildBasketFareKey(FlightListItineraryBasketItemVo basketFareItem) {
StringBuilder sb = new StringBuilder(128);
sb.append(basketFareItem.getGdsCompCode())
.append("//")
.append(basketFareItem.getFareTypeCode())
.append("//")
.append(basketFareItem.getFareBasisCode())
.append("//")
.append(basketFareItem.getAdultFareAmount())
.append("//")
.append(basketFareItem.getChildFareAmount())
.append("//")
.append(basketFareItem.getInfantFareAmount());
return sb.toString();
}
- Acceptance Criteria
✅ MUST maintain 100% identical API response (no logic changes)
✅ Replace all `+` string concatenations in loops with StringBuilder
✅ Pre-calculate StringBuilder capacity based on expected string length
✅ Replace String.format() calls with direct StringBuilder or concatenation
✅ Reduce String object allocation from 200-320 to < 50 per basket request
✅ Verify performance improvement with memory profiling
- Constraints
⚠️ NO LOGIC CHANGES ALLOWED - This is a performance optimization only
- Do not modify the format or content of generated strings
- Maintain exact same delimiter patterns ("//" and "_")
- API response must remain byte-for-byte identical
- Related Issues
- #2610 - CRITICAL-1: Excessive JsonNode Traversal (Fixed ✅)
- #2611 - AtomicReference Memory Retention (Fixed ✅)
- #2626 - CRITICAL-2: O(n³) Nested Loop Anti-Pattern
- #2627 - CRITICAL-3: Repeated Deep Copy Operations
- Part of Phase 2: Quick Wins in optimization roadmap
- Reference
See `docs/flight-list-itinerary/VERIFIED_ISSUES.md` - CRITICAL-4 section for complete analysis.
- Testing Requirements
1. Unit test: Verify string outputs are identical before/after
2. Memory profiling: Count String object allocations in basket mode
3. Load test: 50 basket requests, measure garbage reduction
4. Response validation: Byte-for-byte comparison of itemCode fields