Project

General

Profile

Actions

개선(improvement) #2628

open

개선(improvement) #2310: [BE] Optimize Memory, CPU API search list flight

[BE] CRITICAL-4: String Concatenation in Streams

Added by Dante Le 3 months ago. Updated 3 months ago.

Status:
신규(New)
Priority:
긴급(Emergency)
Assignee:
Start date:
Due date:
12/15/2025 (about 3 months late)
% Done:

0%

Estimated time:
4.00 h
Part:
Build env.:

Description

Parent Issue: #2310 [BE] Optimize Memory, CPU API search list flight

  1. 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.

  1. 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

  1. Problem Code Examples
  1. 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
    
  1. 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));
    
  1. 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

  1. Required Fix
  1. 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();
  1. 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());
  1. 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);
});
  1. 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();
}
  1. 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

  1. 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

  1. 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

  1. Reference

See `docs/flight-list-itinerary/VERIFIED_ISSUES.md` - CRITICAL-4 section for complete analysis.

  1. 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

Actions #1

Updated by Dante Le 3 months ago

  • Parent task set to #2310
Actions

Also available in: Atom PDF

Add picture from clipboard (Maximum size: 50 MB)