From 26ed4956e170030aeaa24e44e1c56ec8c7960e34 Mon Sep 17 00:00:00 2001 From: Feror Date: Sat, 30 May 2026 18:47:22 +0200 Subject: [PATCH] perf(editor): extend textkit profiling probes --- .../EditorPerformanceProfiling.swift | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/Sources/SaplingEditor/EditorPerformanceProfiling.swift b/Sources/SaplingEditor/EditorPerformanceProfiling.swift index f81189d..e32e86a 100644 --- a/Sources/SaplingEditor/EditorPerformanceProfiling.swift +++ b/Sources/SaplingEditor/EditorPerformanceProfiling.swift @@ -391,6 +391,31 @@ public enum EditorBenchmarkProfiler { notes: "\(attributedStringResult.value.styledLineCount) styled lines" )) + let coldViewportStack = makeTextKitStack(attributedString: textStorage) + let coldViewportResult = measure { + coldViewportStack.layoutManager.glyphRange( + forBoundingRect: NSRect(x: 0, y: 50_000, width: 760, height: 900), + in: coldViewportStack.textContainer + ) + } + measurements.append(EditorBenchmarkMeasurement( + name: "cold_viewport_glyph_range_middle", + category: .layout, + durationMilliseconds: coldViewportResult.durationMilliseconds, + notes: "Visible glyph range before explicit full-document layout" + )) + + let coldLineFragmentStack = makeTextKitStack(attributedString: textStorage) + let coldLineFragmentResult = measure { + lineFragmentRect(atCharacterLocation: coldLineFragmentStack.textStorage.length / 2, in: coldLineFragmentStack) + } + measurements.append(EditorBenchmarkMeasurement( + name: "cold_line_fragment_calculation_midpoint", + category: .layout, + durationMilliseconds: coldLineFragmentResult.durationMilliseconds, + notes: "lineFragmentRect near midpoint before explicit full-document layout" + )) + let layoutInvalidationResult = measure { var actualRange = NSRange(location: 0, length: 0) layoutManager.invalidateLayout( @@ -425,6 +450,30 @@ public enum EditorBenchmarkProfiler { notes: "NSLayoutManager.ensureLayout for text container" )) + let cachedLineFragmentResult = measure { + lineFragmentRect(atCharacterLocation: textStorage.length / 2, in: ( + textStorage: textStorage, + layoutManager: layoutManager, + textContainer: textContainer + )) + } + measurements.append(EditorBenchmarkMeasurement( + name: "cached_line_fragment_calculation_midpoint", + category: .layout, + durationMilliseconds: cachedLineFragmentResult.durationMilliseconds, + notes: "lineFragmentRect near midpoint after full layout" + )) + + let textContainerUsedRectResult = measure { + layoutManager.usedRect(for: textContainer) + } + measurements.append(EditorBenchmarkMeasurement( + name: "text_container_used_rect_after_full_layout", + category: .layout, + durationMilliseconds: textContainerUsedRectResult.durationMilliseconds, + notes: "NSTextContainer usedRect after full layout" + )) + let firstViewportResult = measure { layoutManager.glyphRange( forBoundingRect: NSRect(x: 0, y: 0, width: 760, height: 900), @@ -544,5 +593,37 @@ public enum EditorBenchmarkProfiler { _ = changedSource return measurements } + + private static func makeTextKitStack(attributedString: NSAttributedString) -> ( + textStorage: NSTextStorage, + layoutManager: NSLayoutManager, + textContainer: NSTextContainer + ) { + let textStorage = NSTextStorage(attributedString: attributedString) + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: NSSize(width: 760, height: CGFloat.greatestFiniteMagnitude)) + textContainer.widthTracksTextView = false + textContainer.heightTracksTextView = false + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + return (textStorage, layoutManager, textContainer) + } + + @discardableResult + private static func lineFragmentRect( + atCharacterLocation characterLocation: Int, + in stack: ( + textStorage: NSTextStorage, + layoutManager: NSLayoutManager, + textContainer: NSTextContainer + ) + ) -> NSRect { + guard stack.textStorage.length > 0 else { return .zero } + + let characterLocation = max(0, min(characterLocation, stack.textStorage.length - 1)) + let glyphIndex = stack.layoutManager.glyphIndexForCharacter(at: characterLocation) + var effectiveRange = NSRange(location: 0, length: 0) + return stack.layoutManager.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: &effectiveRange) + } #endif }