Skip to content

Commit cef059b

Browse files
committed
Reduce remaining UI stalls
- Add lightweight git status reloads for index polling - Avoid blocking main thread on shell env load - Fix single-file diff to not compute full-repo diff - Truncate huge diffs and reduce diff reload churn
1 parent c5a0b99 commit cef059b

8 files changed

Lines changed: 105 additions & 28 deletions

File tree

aizen/Services/Git/Domain/GitStatusService.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ struct DetailedGitStatus {
2121

2222
actor GitStatusService {
2323

24-
func getDetailedStatus(at path: String) async throws -> DetailedGitStatus {
24+
func getDetailedStatus(
25+
at path: String,
26+
includeUntracked: Bool = true,
27+
includeDiffStats: Bool = true
28+
) async throws -> DetailedGitStatus {
2529
// Run libgit2 operations on background thread to avoid blocking
2630
return try await Task.detached(priority: .utility) {
2731
let repo = try Libgit2Repository(path: path)
28-
let status = try repo.status()
32+
let status = try repo.status(includeUntracked: includeUntracked)
2933

3034
// Get current branch name
3135
let currentBranch = try? repo.currentBranchName()
@@ -35,8 +39,13 @@ actor GitStatusService {
3539
let aheadBy = aheadBehind.ahead
3640
let behindBy = aheadBehind.behind
3741

38-
// Calculate additions/deletions from diff
39-
let diffStats = try repo.diffStats()
42+
// Calculate additions/deletions from diff (can be expensive on large repos)
43+
let diffStats: Libgit2DiffStats
44+
if includeDiffStats {
45+
diffStats = (try? repo.diffStats()) ?? Libgit2DiffStats(filesChanged: 0, insertions: 0, deletions: 0)
46+
} else {
47+
diffStats = Libgit2DiffStats(filesChanged: 0, insertions: 0, deletions: 0)
48+
}
4049

4150
// Map entries to file paths
4251
let stagedFiles = status.staged.map { $0.path }
@@ -59,7 +68,7 @@ actor GitStatusService {
5968
}
6069

6170
func getCurrentBranch(at path: String) async throws -> String {
62-
return try await Task.detached {
71+
return try await Task.detached(priority: .utility) {
6372
let repo = try Libgit2Repository(path: path)
6473
guard let branch = try repo.currentBranchName() else {
6574
throw Libgit2Error.referenceNotFound("HEAD")

aizen/Services/Git/GitRepositoryService.swift

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class GitRepositoryService: ObservableObject {
2727
private var isStatusReloadPending = false
2828
private let statusReloadDebounceInterval: TimeInterval = 0.3
2929
private var inFlightStatusTask: Task<Void, Never>?
30+
private var pendingReloadIsLightweight = true
3031

3132
init(worktreePath: String) {
3233
self.worktreePath = worktreePath
@@ -328,16 +329,16 @@ class GitRepositoryService: ObservableObject {
328329

329330
// MARK: - Status Loading
330331

331-
func reloadStatus() {
332+
func reloadStatus(lightweight: Bool = false) {
332333
Task { @MainActor [weak self] in
333-
self?.reloadStatusDebouncedOnMain()
334+
self?.reloadStatusDebouncedOnMain(lightweight: lightweight)
334335
}
335336
}
336337

337338
/// Force immediate status reload without debouncing (for use after operations)
338339
private func reloadStatusImmediate() {
339340
Task { @MainActor [weak self] in
340-
await self?.reloadStatusNowOnMain()
341+
await self?.reloadStatusNowOnMain(lightweight: false)
341342
}
342343
}
343344

@@ -347,7 +348,7 @@ class GitRepositoryService: ObservableObject {
347348
guard newPath != self.worktreePath else { return }
348349
self.worktreePath = newPath
349350
self.currentStatus = .empty
350-
self.reloadStatusDebouncedOnMain()
351+
self.reloadStatusDebouncedOnMain(lightweight: true)
351352
}
352353
}
353354

@@ -359,7 +360,7 @@ class GitRepositoryService: ObservableObject {
359360
await MainActor.run { original?() }
360361
return
361362
}
362-
await self.reloadStatusNow()
363+
await self.reloadStatusNow(lightweight: false)
363364
await MainActor.run {
364365
original?()
365366
}
@@ -390,12 +391,16 @@ class GitRepositoryService: ObservableObject {
390391
}
391392

392393
private func reloadStatusInternal() async {
393-
await reloadStatusNow()
394+
await reloadStatusNow(lightweight: false)
394395
}
395396

396397
nonisolated
397-
private func loadGitStatus(at path: String) async throws -> GitStatus {
398-
let detailedStatus = try await statusService.getDetailedStatus(at: path)
398+
private func loadGitStatus(at path: String, lightweight: Bool) async throws -> GitStatus {
399+
let detailedStatus = try await statusService.getDetailedStatus(
400+
at: path,
401+
includeUntracked: !lightweight,
402+
includeDiffStats: !lightweight
403+
)
399404

400405
return GitStatus(
401406
stagedFiles: detailedStatus.stagedFiles,
@@ -410,18 +415,19 @@ class GitRepositoryService: ObservableObject {
410415
)
411416
}
412417

413-
private func reloadStatusNow() async {
418+
private func reloadStatusNow(lightweight: Bool) async {
414419
let task = await MainActor.run { () -> Task<Void, Never>? in
415420
statusReloadTask?.cancel()
416421
isStatusReloadPending = false
422+
pendingReloadIsLightweight = true
417423

418424
let path = worktreePath
419425

420426
inFlightStatusTask?.cancel()
421427
let task = Task(priority: .utility) { [weak self] in
422428
guard let self else { return }
423429
do {
424-
let status = try await self.loadGitStatus(at: path)
430+
let status = try await self.loadGitStatus(at: path, lightweight: lightweight)
425431
guard !Task.isCancelled else { return }
426432
await MainActor.run {
427433
// Guard against path changes while loading (e.g. worktree switch).
@@ -439,11 +445,17 @@ class GitRepositoryService: ObservableObject {
439445
}
440446

441447
@MainActor
442-
private func reloadStatusDebouncedOnMain() {
448+
private func reloadStatusDebouncedOnMain(lightweight: Bool) {
443449
if isStatusReloadPending {
450+
// Upgrade pending lightweight reload to full if requested.
451+
if pendingReloadIsLightweight && !lightweight {
452+
pendingReloadIsLightweight = false
453+
}
444454
return
445455
}
456+
446457
isStatusReloadPending = true
458+
pendingReloadIsLightweight = lightweight
447459

448460
statusReloadTask?.cancel()
449461
statusReloadTask = Task { [weak self] in
@@ -458,12 +470,13 @@ class GitRepositoryService: ObservableObject {
458470
await MainActor.run {
459471
self.isStatusReloadPending = false
460472
}
461-
await self.reloadStatusNow()
473+
let doLightweight = await MainActor.run { self.pendingReloadIsLightweight }
474+
await self.reloadStatusNow(lightweight: doLightweight)
462475
}
463476
}
464477

465478
@MainActor
466-
private func reloadStatusNowOnMain() async {
467-
await reloadStatusNow()
479+
private func reloadStatusNowOnMain(lightweight: Bool) async {
480+
await reloadStatusNow(lightweight: lightweight)
468481
}
469482
}

aizen/Services/Git/Libgit2/Libgit2Diff.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,8 @@ extension Libgit2Repository {
443443
/// Format libgit2 diff as unified diff string
444444
private func formatDiffAsUnified(_ diff: OpaquePointer) -> String {
445445
var output = ""
446+
let maxBytes = 4_000_000
447+
var bytesWritten = 0
446448

447449
let numDeltas = git_diff_num_deltas(diff)
448450
for i in 0..<numDeltas {
@@ -457,7 +459,21 @@ extension Libgit2Repository {
457459
defer { git_buf_dispose(&buf) }
458460

459461
if git_patch_to_buf(&buf, p) == 0, let ptr = buf.ptr {
460-
output += String(cString: ptr)
462+
let remaining = maxBytes - bytesWritten
463+
if remaining <= 0 {
464+
output += "\n... diff truncated (too large) ...\n"
465+
break
466+
}
467+
468+
let take = min(Int(buf.size), remaining)
469+
let raw = UnsafeRawBufferPointer(start: ptr, count: take)
470+
output += String(decoding: raw, as: UTF8.self)
471+
bytesWritten += take
472+
473+
if take < Int(buf.size) {
474+
output += "\n... diff truncated (too large) ...\n"
475+
break
476+
}
461477
}
462478
}
463479

aizen/Utilities/ShellEnvironment.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ enum ShellEnvironment {
2323
return cached
2424
}
2525

26+
// Never block the UI thread on a login-shell spawn.
27+
// Return a best-effort environment immediately and warm the cache asynchronously.
28+
if Thread.isMainThread {
29+
DispatchQueue.global(qos: .utility).async {
30+
_ = loadUserShellEnvironment()
31+
}
32+
return ProcessInfo.processInfo.environment
33+
}
34+
2635
let env = loadEnvironmentFromShell()
2736
cachedEnvironment = env
2837
return env

aizen/Views/Worktree/Components/Git/DiffView.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,10 +373,21 @@ struct DiffView: NSViewRepresentable {
373373

374374
private static func parseDiffOutput(diffOutput: String, showFileHeaders: Bool) -> ParsedDiffMetadata {
375375
var rawLines: [String] = []
376-
rawLines.reserveCapacity(max(128, diffOutput.count / 48))
377-
diffOutput.enumerateLines { line, _ in
376+
let maxRawLines = 200_000
377+
rawLines.reserveCapacity(min(max(128, diffOutput.count / 48), maxRawLines))
378+
var didTruncate = false
379+
380+
diffOutput.enumerateLines { line, stop in
381+
if rawLines.count >= maxRawLines {
382+
didTruncate = true
383+
stop = true
384+
return
385+
}
378386
rawLines.append(line)
379387
}
388+
if didTruncate {
389+
rawLines.append("@@ ... diff view truncated (too many lines) ... @@")
390+
}
380391

381392
var rowKinds: [RowKind] = []
382393
rowKinds.reserveCapacity(rawLines.count)

aizen/Views/Worktree/Components/Git/GitDiffViewModel.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,22 @@ class GitDiffViewModel: ObservableObject {
211211
/// Get unified diff output using libgit2
212212
private func runGitDiff(_ args: [String]) async -> String? {
213213
let path = repoPath
214-
return await Task.detached {
214+
215+
// For single-file diffs, use the git CLI so we don't compute a full-repo diff.
216+
if args.contains("--") {
217+
do {
218+
let result = try await ProcessExecutor.shared.executeWithOutput(
219+
executable: "/usr/bin/git",
220+
arguments: args,
221+
workingDirectory: path
222+
)
223+
return result.succeeded ? result.stdout : nil
224+
} catch {
225+
return nil
226+
}
227+
}
228+
229+
return await Task.detached(priority: .utility) {
215230
do {
216231
let repo = try Libgit2Repository(path: path)
217232
// Check if this is a HEAD diff or unstaged diff

aizen/Views/Worktree/Components/Git/GitPanelWindowContent.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@ struct GitPanelWindowContent: View {
101101
gitIndexWatchToken = nil
102102
}
103103
.onChange(of: gitStatus) { _ in
104-
updateChangedFilesCache()
105-
reloadDiffDebounced()
104+
let changed = updateChangedFilesCache()
105+
if changed {
106+
reloadDiffDebounced()
107+
}
106108
}
107109
.onChange(of: selectedHistoryCommit) { commit in
108110
Task {
@@ -384,7 +386,7 @@ struct GitPanelWindowContent: View {
384386
Task {
385387
let token = await GitIndexWatchCenter.shared.addSubscriber(worktreePath: worktreePath) { [weak gitRepositoryService] in
386388
Task { @MainActor in
387-
gitRepositoryService?.reloadStatus()
389+
gitRepositoryService?.reloadStatus(lightweight: true)
388390
}
389391
}
390392
await MainActor.run {
@@ -393,7 +395,7 @@ struct GitPanelWindowContent: View {
393395
}
394396
}
395397

396-
private func updateChangedFilesCache() {
398+
private func updateChangedFilesCache() -> Bool {
397399
var files = Set<String>()
398400
files.formUnion(gitStatus.stagedFiles)
399401
files.formUnion(gitStatus.modifiedFiles)
@@ -403,7 +405,9 @@ struct GitPanelWindowContent: View {
403405
let sortedFiles = files.sorted()
404406
if sortedFiles != cachedChangedFiles {
405407
cachedChangedFiles = sortedFiles
408+
return true
406409
}
410+
return false
407411
}
408412

409413
private func validateCommentsAgainstDiff() {

aizen/Views/Worktree/WorktreeDetailView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ struct WorktreeDetailView: View {
556556

557557
let token = await GitIndexWatchCenter.shared.addSubscriber(worktreePath: worktreePath) { [weak gitRepositoryService] in
558558
Task { @MainActor in
559-
gitRepositoryService?.reloadStatus()
559+
gitRepositoryService?.reloadStatus(lightweight: true)
560560
}
561561
}
562562
gitIndexWatchToken = token

0 commit comments

Comments
 (0)