fix(engine): silence PERF logs by default, fix "modifying state during view update"
PerfTrace was opt-out (MD_PERF=0 to silence) so every Debug build printed a PERF line per keystroke unconditionally. Flipped to opt-in (MD_PERF=1 to enable) - still fully available for future perf work, just quiet by default. "Modifying state during view update": onCodeBlockSelectionChange, onSelectedTextChange, and onCommentAnchorRectsChange could all fire synchronously from inside NativeTextViewWrapper.updateNSView's own call stack - a programmatic edit (pendingInlineReplacement/ pendingTextInsertion/pendingTextRangeReplacement) re-enters textViewDidChangeSelection/textDidChange synchronously (AppKit resets selection on edit), which is still a SwiftUI view update in progress. Calling straight into the embedder's @State setter there is exactly what trips the warning. Routed all three through new fireCodeBlockSelectionChange/fireSelectedTextChange/ fireCommentAnchorRectsChange helpers on the coordinator that defer one runloop tick via DispatchQueue.main.async - same technique NativeTextViewWrapper already uses to clear its own pending* bindings, just centralized instead of ad-hoc per call site. 322/322 tests passing.
This commit is contained in:
+4
-2
@@ -9,7 +9,9 @@
|
||||
// which costs grow with file size instead of staying constant. The whole point:
|
||||
// type in a short file, then a long one, and compare `total` for the same edit.
|
||||
//
|
||||
// Toggle: set the env var MD_PERF=0 in the run scheme to silence.
|
||||
// Toggle: set the env var MD_PERF=1 in the run scheme to enable.
|
||||
// Off by default even in Debug — opt-in, not opt-out, so a normal
|
||||
// debug run stays quiet.
|
||||
// Debug-only — the whole thing compiles out in Release.
|
||||
// Remove before shipping (this file + the `PerfTrace.` call sites).
|
||||
//
|
||||
@@ -18,7 +20,7 @@ import Foundation
|
||||
|
||||
enum PerfTrace {
|
||||
#if DEBUG
|
||||
static var enabled = ProcessInfo.processInfo.environment["MD_PERF"] != "0"
|
||||
static var enabled = ProcessInfo.processInfo.environment["MD_PERF"] == "1"
|
||||
/// Opt-in for the sampled full-rebuild verifier asserts (wiki splice,
|
||||
/// backtick census, parse buffer). They run 3× O(doc) work synchronously
|
||||
/// on every 64th keystroke — periodic spikes that pollute the PERF
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@ import AppKit
|
||||
extension NativeTextViewCoordinator {
|
||||
func updateCodeBlockSelection(textView: NSTextView, parsed: ParsedDocument? = nil) {
|
||||
guard let textContainer = textView.textContainer else {
|
||||
onCodeBlockSelectionChange?([])
|
||||
fireCodeBlockSelectionChange([])
|
||||
return
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ extension NativeTextViewCoordinator {
|
||||
// no per-call full-token filter.
|
||||
cachedCodeBlockTokens = parsed.codeBlockTokensWithIndices
|
||||
} else if cachedCodeBlockTokens.isEmpty {
|
||||
onCodeBlockSelectionChange?([])
|
||||
fireCodeBlockSelectionChange([])
|
||||
return
|
||||
}
|
||||
|
||||
@@ -91,6 +91,6 @@ extension NativeTextViewCoordinator {
|
||||
)
|
||||
}
|
||||
|
||||
onCodeBlockSelectionChange?(selections)
|
||||
fireCodeBlockSelectionChange(selections)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ import AppKit
|
||||
extension NativeTextViewCoordinator {
|
||||
func updateCommentAnchorRects(textView: NSTextView) {
|
||||
guard !commentAnchorQueries.isEmpty else {
|
||||
onCommentAnchorRectsChange?([])
|
||||
fireCommentAnchorRectsChange([])
|
||||
return
|
||||
}
|
||||
let nsText = textView.string as NSString
|
||||
@@ -28,6 +28,6 @@ extension NativeTextViewCoordinator {
|
||||
let rect = textView.viewRect(forCharacterRange: found, using: layoutBridge) else { continue }
|
||||
results.append(CommentAnchorRect(id: query.id, rect: rect))
|
||||
}
|
||||
onCommentAnchorRectsChange?(results)
|
||||
fireCommentAnchorRectsChange(results)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -362,7 +362,7 @@ extension NativeTextViewCoordinator {
|
||||
// selection at all.
|
||||
if !isRebuildingDocument {
|
||||
let selRange = tv.selectedRange()
|
||||
onSelectedTextChange?(selRange.length > 0 ? (tv.string as NSString).substring(with: selRange) : nil)
|
||||
fireSelectedTextChange(selRange.length > 0 ? (tv.string as NSString).substring(with: selRange) : nil)
|
||||
}
|
||||
// Raw mode: plain source — no reveal, snap-back, or inline previews.
|
||||
if configuration.rawSourceMode { return }
|
||||
|
||||
+20
@@ -87,6 +87,26 @@ public final class NativeTextViewCoordinator: NSObject, NSTextViewDelegate {
|
||||
var onSelectedTextChange: ((String?) -> Void)?
|
||||
var commentAnchorQueries: [CommentAnchorQuery] = []
|
||||
var onCommentAnchorRectsChange: (([CommentAnchorRect]) -> Void)?
|
||||
|
||||
/// These three callbacks can fire from inside `NativeTextViewWrapper.updateNSView`
|
||||
/// itself (a programmatic edit re-enters `textViewDidChangeSelection`/
|
||||
/// `textDidChange` synchronously — see `isRebuildingDocument` above), which is a
|
||||
/// SwiftUI view update already in progress on the call stack. Calling straight into
|
||||
/// an embedder's `@State` setter there trips "Modifying state during view update" —
|
||||
/// deferring one runloop tick is enough to land outside it, same as how
|
||||
/// `NativeTextViewWrapper` already clears its `pending*` bindings.
|
||||
func fireCodeBlockSelectionChange(_ selections: [CodeBlockSelection]) {
|
||||
DispatchQueue.main.async { [onCodeBlockSelectionChange] in onCodeBlockSelectionChange?(selections) }
|
||||
}
|
||||
|
||||
func fireSelectedTextChange(_ text: String?) {
|
||||
DispatchQueue.main.async { [onSelectedTextChange] in onSelectedTextChange?(text) }
|
||||
}
|
||||
|
||||
func fireCommentAnchorRectsChange(_ rects: [CommentAnchorRect]) {
|
||||
DispatchQueue.main.async { [onCommentAnchorRectsChange] in onCommentAnchorRectsChange?(rects) }
|
||||
}
|
||||
|
||||
var didInitialFormatting: Bool = false
|
||||
/// One-shot guard so `updateCodeBlockSelection` only forces a full-document layout once per document.
|
||||
var didEnsureLayoutForCurrentDocument: Bool = false
|
||||
|
||||
Reference in New Issue
Block a user