Skip to content

Commit 82ae246

Browse files
committed
perf(editor): box query text in a reference so SwiftUI compares a pointer not the whole string
1 parent 0559fc4 commit 82ae246

2 files changed

Lines changed: 47 additions & 6 deletions

File tree

TablePro/Models/Query/QueryTabState.swift

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,21 @@ struct TabTableContext: Equatable {
271271
}
272272

273273
struct TabQueryContent: Equatable {
274-
var query: String = ""
274+
/// Holds the query text behind a reference. SwiftUI's attribute-graph diff compares a value by walking its memory
275+
/// layout, so a stored `String` field made it normalize the whole multi-megabyte query (NFC) on every view update.
276+
/// Storing the text in a class makes that walk compare an 8-byte pointer instead. Value semantics are preserved
277+
/// because the setter replaces the box.
278+
private final class QueryStorage: Sendable {
279+
let text: String
280+
init(_ text: String) { self.text = text }
281+
}
282+
283+
private var queryStorage: QueryStorage
284+
285+
var query: String {
286+
get { queryStorage.text }
287+
set { queryStorage = QueryStorage(newValue) }
288+
}
275289
var queryParameters: [QueryParameter] = []
276290
var isParameterPanelVisible: Bool = false
277291
var sourceFileURL: URL?
@@ -281,6 +295,24 @@ struct TabQueryContent: Equatable {
281295

282296
static let maxPersistableQuerySize = 500_000
283297

298+
init(
299+
query: String = "",
300+
queryParameters: [QueryParameter] = [],
301+
isParameterPanelVisible: Bool = false,
302+
sourceFileURL: URL? = nil,
303+
savedFileContent: String? = nil,
304+
loadMtime: Date? = nil,
305+
externalModificationDetected: Bool = false
306+
) {
307+
self.queryStorage = QueryStorage(query)
308+
self.queryParameters = queryParameters
309+
self.isParameterPanelVisible = isParameterPanelVisible
310+
self.sourceFileURL = sourceFileURL
311+
self.savedFileContent = savedFileContent
312+
self.loadMtime = loadMtime
313+
self.externalModificationDetected = externalModificationDetected
314+
}
315+
284316
var isFileDirty: Bool {
285317
guard sourceFileURL != nil, let saved = savedFileContent else { return false }
286318
let queryNS = query as NSString
@@ -290,11 +322,10 @@ struct TabQueryContent: Equatable {
290322
}
291323

292324
static func == (lhs: TabQueryContent, rhs: TabQueryContent) -> Bool {
293-
// `query` can be multiple megabytes and is bridged from the editor's NSTextStorage. The synthesized `==`
294-
// compared it with Swift's canonical Unicode equality, which walks the whole string through NFC normalization
295-
// on every SwiftUI diff and pins the CPU while editing a large query. NSString equality returns in O(1) when
296-
// the lengths differ (every keystroke changes the length) and uses literal comparison otherwise.
297-
guard (lhs.query as NSString).isEqual(to: rhs.query),
325+
// The query can be multiple megabytes and is bridged from the editor's NSTextStorage. Same-box comparison is
326+
// O(1); otherwise use NSString literal equality, which returns in O(1) when the lengths differ (every keystroke
327+
// changes the length) and avoids Swift's canonical Unicode normalization.
328+
guard lhs.queryStorage === rhs.queryStorage || (lhs.query as NSString).isEqual(to: rhs.query),
298329
lhs.queryParameters == rhs.queryParameters,
299330
lhs.isParameterPanelVisible == rhs.isParameterPanelVisible,
300331
lhs.sourceFileURL == rhs.sourceFileURL,

TableProTests/Models/Query/TabQueryContentEqualityTests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,14 @@ struct TabQueryContentEqualityTests {
5959
b.savedFileContent = "other"
6060
#expect(a != b)
6161
}
62+
63+
@Test("Value semantics: mutating a copy does not change the original")
64+
func valueSemantics() {
65+
let a = TabQueryContent(query: "original")
66+
var b = a
67+
b.query = "changed"
68+
#expect(a.query == "original")
69+
#expect(b.query == "changed")
70+
#expect(a != b)
71+
}
6272
}

0 commit comments

Comments
 (0)