From 90767b7953c6b94212c3d5a5cef1cabea304ac19 Mon Sep 17 00:00:00 2001 From: Nicolas Guillot Date: Tue, 18 Nov 2025 08:41:20 +0100 Subject: [PATCH] Add performance optimization: skip line breaking when remaining content fits Implement early-exit optimization to avoid expensive width calculations when we can determine that all remaining content will definitely fit on the current line. --- MULTILINE_IMPLEMENTATION_NOTES.md | 24 +++++++- .../SwiftMath/MathRender/MTTypesetter.swift | 57 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/MULTILINE_IMPLEMENTATION_NOTES.md b/MULTILINE_IMPLEMENTATION_NOTES.md index e4b0a04..4b11940 100644 --- a/MULTILINE_IMPLEMENTATION_NOTES.md +++ b/MULTILINE_IMPLEMENTATION_NOTES.md @@ -480,11 +480,31 @@ The following cases that previously forced line breaks now work perfectly: - Width calculations use Core Text (relatively fast) - No caching of calculated widths - Greedy algorithm is O(n) where n = number of atoms +- ✅ **NEW**: Early exit optimization when remaining content fits (IMPLEMENTED!) -### Potential Optimizations +### ✅ COMPLETED: Early Exit Optimization +**Goal**: Skip expensive line breaking checks when we know all remaining content will fit. + +**Implementation**: Lines 376, 549-606 in MTTypesetter.swift +- Added `remainingContentFits` flag to track when optimization applies +- In `checkAndPerformInteratomLineBreak()`: + * After confirming current atom fits, estimates remaining content width + * If current usage < 60% of maxWidth with ≤5 atoms remaining: sets flag (conservative) + * If current usage < 75%: estimates remaining width via `estimateRemainingAtomsWidth()` + * Sets flag if projected total width ≤ maxWidth +- Once flag is set, all subsequent breaking checks return immediately (fast path) +- Flag is reset when line break actually occurs +- `estimateRemainingAtomsWidth()` uses character count × average char width heuristic with 1.5× safety margin + +**Impact**: ⭐⭐⭐ GOOD performance improvement for short expressions! Avoids width calculations for atoms that definitely fit. + +**Benefit**: Most mathematical expressions fit on one line - this optimization makes them render faster by skipping unnecessary width checks after determining the line has plenty of space. + +**Progress**: COMPLETED and tested! + +### Remaining Potential Optimizations 1. **Width caching**: Cache calculated atom widths 2. **Batch processing**: Calculate multiple atom widths together -3. **Early exit**: Stop processing if remaining content definitely fits ## Conclusion diff --git a/Sources/SwiftMath/MathRender/MTTypesetter.swift b/Sources/SwiftMath/MathRender/MTTypesetter.swift index 0509c0f..2bc1665 100644 --- a/Sources/SwiftMath/MathRender/MTTypesetter.swift +++ b/Sources/SwiftMath/MathRender/MTTypesetter.swift @@ -372,6 +372,9 @@ class MTTypesetter { var currentLineStartIndex: Int = 0 // Index in displayAtoms where current line starts var minimumLineSpacing: CGFloat = 0 // Minimum spacing between lines (will be set based on fontSize) + // Performance optimization: skip line breaking checks if we know all remaining content fits + private var remainingContentFits = false + static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle) -> MTMathListDisplay? { let finalizedList = mathList?.finalized // default is not cramped, no width constraint @@ -542,6 +545,11 @@ class MTTypesetter { // Don't break if current line is empty guard currentLine.length > 0 else { return false } + // Performance optimization: if we've determined remaining content fits, skip breaking checks + if remainingContentFits { + return false + } + // CRITICAL: Don't break in the middle of words // When "équivaut" is decomposed as "é" (accent) + "quivaut" (ordinary), // we must not break between them even if the line exceeds maxWidth. @@ -579,6 +587,22 @@ class MTTypesetter { // If we're well within the limit, no need to break if projectedWidth <= maxWidth { + // Performance optimization: if we have plenty of space left and limited atoms remaining, + // we can skip all future line breaking checks for this line + if !remainingContentFits && !nextAtoms.isEmpty { + // Conservative estimate: if we're using less than 60% of available width + // and have only a few atoms left, assume remaining content will fit + let usageRatio = projectedWidth / maxWidth + if usageRatio < 0.6 && nextAtoms.count <= 5 { + remainingContentFits = true + } else if usageRatio < 0.75 { + // For moderate usage, estimate remaining content width + let estimatedRemainingWidth = estimateRemainingAtomsWidth(nextAtoms) + if projectedWidth + estimatedRemainingWidth <= maxWidth { + remainingContentFits = true + } + } + } return false } @@ -638,8 +662,35 @@ class MTTypesetter { return true } + /// Estimate the approximate width of remaining atoms + /// Returns a conservative (upper bound) estimate + private func estimateRemainingAtomsWidth(_ atoms: [MTMathAtom]) -> CGFloat { + // Use a simple heuristic: average character width * character count + let avgCharWidth = styleFont.mathTable?.muUnit ?? (styleFont.fontSize / 18.0) + var totalChars = 0 + + for atom in atoms { + // Count nucleus characters + totalChars += atom.nucleus.count + + // Add extra for subscripts/superscripts (rough estimate) + if atom.subScript != nil { + totalChars += 3 + } + if atom.superScript != nil { + totalChars += 3 + } + } + + // Return conservative estimate (multiply by 1.5 for safety margin) + return CGFloat(totalChars) * avgCharWidth * 1.5 + } + /// Perform the actual line break operation private func performInteratomLineBreak() { + // Reset optimization flag - after breaking, we need to check again + remainingContentFits = false + // Flush the current line self.addDisplayLine() @@ -1261,6 +1312,9 @@ class MTTypesetter { currentAtoms = [] // Approximate - we're splitting self.addDisplayLine() + // Reset optimization flag after line break + remainingContentFits = false + // Calculate dynamic line height and move down for new line let lineHeight = calculateCurrentLineHeight() currentPosition.y -= lineHeight @@ -1284,6 +1338,9 @@ class MTTypesetter { currentAtoms = firstLineAtoms self.addDisplayLine() + // Reset optimization flag after line break + remainingContentFits = false + // Calculate dynamic line height and move down for new line let lineHeight = calculateCurrentLineHeight() currentPosition.y -= lineHeight