native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #160 from onevcat/contrib/parallel-loading

Parallelize repository startup loading

authored by

khoi and committed by
GitHub
4343783e 5e705cf7

+179 -6
+40 -4
supacode/Features/Repositories/Reducer/RepositoriesFeature.swift
··· 2575 2575 .cancellable(id: CancelID.load, cancelInFlight: true) 2576 2576 } 2577 2577 2578 + private struct WorktreesFetchResult: Sendable { 2579 + let root: URL 2580 + let worktrees: [Worktree]? 2581 + let errorMessage: String? 2582 + } 2583 + 2578 2584 private func loadRepositoriesData(_ roots: [URL]) async -> ([Repository], [LoadFailure]) { 2585 + let fetchResults = await withTaskGroup(of: WorktreesFetchResult.self) { group in 2586 + for root in roots { 2587 + let gitClient = self.gitClient 2588 + group.addTask { 2589 + do { 2590 + let worktrees = try await gitClient.worktrees(root) 2591 + return WorktreesFetchResult(root: root, worktrees: worktrees, errorMessage: nil) 2592 + } catch { 2593 + return WorktreesFetchResult( 2594 + root: root, 2595 + worktrees: nil, 2596 + errorMessage: error.localizedDescription 2597 + ) 2598 + } 2599 + } 2600 + } 2601 + 2602 + var resultsByRootID: [Repository.ID: WorktreesFetchResult] = [:] 2603 + for await result in group { 2604 + let rootID = result.root.standardizedFileURL.path(percentEncoded: false) 2605 + resultsByRootID[rootID] = result 2606 + } 2607 + return resultsByRootID 2608 + } 2609 + 2579 2610 var loaded: [Repository] = [] 2580 2611 var failures: [LoadFailure] = [] 2581 2612 for root in roots { 2582 2613 let normalizedRoot = root.standardizedFileURL 2583 2614 let rootID = normalizedRoot.path(percentEncoded: false) 2584 - do { 2585 - let worktrees = try await gitClient.worktrees(root) 2615 + guard let result = fetchResults[rootID] else { continue } 2616 + if let worktrees = result.worktrees { 2586 2617 let name = Repository.name(for: normalizedRoot) 2587 2618 let repository = Repository( 2588 2619 id: rootID, ··· 2591 2622 worktrees: IdentifiedArray(uniqueElements: worktrees) 2592 2623 ) 2593 2624 loaded.append(repository) 2594 - } catch { 2595 - failures.append(LoadFailure(rootID: rootID, message: error.localizedDescription)) 2625 + } else { 2626 + failures.append( 2627 + LoadFailure( 2628 + rootID: rootID, 2629 + message: result.errorMessage ?? "Unknown error" 2630 + ) 2631 + ) 2596 2632 } 2597 2633 } 2598 2634 return (loaded, failures)
+139 -2
supacodeTests/RepositoriesFeatureTests.swift
··· 2447 2447 ) 2448 2448 } 2449 2449 2450 - private func makeRepository(id: String, worktrees: [Worktree]) -> Repository { 2450 + private func makeRepository( 2451 + id: String, 2452 + name: String = "repo", 2453 + worktrees: [Worktree] 2454 + ) -> Repository { 2451 2455 Repository( 2452 2456 id: id, 2453 2457 rootURL: URL(fileURLWithPath: id), 2454 - name: "repo", 2458 + name: name, 2455 2459 worktrees: IdentifiedArray(uniqueElements: worktrees) 2456 2460 ) 2457 2461 } ··· 2461 2465 state.repositories = IdentifiedArray(uniqueElements: repositories) 2462 2466 state.repositoryRoots = repositories.map(\.rootURL) 2463 2467 return state 2468 + } 2469 + 2470 + @Test func loadPersistedRepositoriesStartsFetchesConcurrentlyAndPreservesRootOrder() async { 2471 + let testID = UUID().uuidString 2472 + let repoRootA = "/tmp/\(testID)-repo-a" 2473 + let repoRootB = "/tmp/\(testID)-repo-b" 2474 + let worktreeA = makeWorktree(id: "\(repoRootA)/main", name: "main", repoRoot: repoRootA) 2475 + let worktreeB = makeWorktree(id: "\(repoRootB)/main", name: "main", repoRoot: repoRootB) 2476 + let repoA = makeRepository( 2477 + id: repoRootA, 2478 + name: URL(fileURLWithPath: repoRootA).lastPathComponent, 2479 + worktrees: [worktreeA] 2480 + ) 2481 + let repoB = makeRepository( 2482 + id: repoRootB, 2483 + name: URL(fileURLWithPath: repoRootB).lastPathComponent, 2484 + worktrees: [worktreeB] 2485 + ) 2486 + let gate = AsyncGate() 2487 + let startedRoots = LockIsolated<Set<String>>([]) 2488 + 2489 + let store = TestStore(initialState: RepositoriesFeature.State()) { 2490 + RepositoriesFeature() 2491 + } withDependencies: { 2492 + $0.repositoryPersistence.loadRoots = { [repoRootA, repoRootB] } 2493 + $0.gitClient.worktrees = { root in 2494 + let path = root.path(percentEncoded: false) 2495 + startedRoots.withValue { $0.insert(path) } 2496 + if path == repoRootA { 2497 + await gate.wait() 2498 + return [worktreeA] 2499 + } 2500 + if path == repoRootB { 2501 + return [worktreeB] 2502 + } 2503 + Issue.record("Unexpected root: \(path)") 2504 + return [] 2505 + } 2506 + } 2507 + 2508 + await store.send(.loadPersistedRepositories) 2509 + 2510 + var secondFetchStarted = false 2511 + for _ in 0..<100 { 2512 + if startedRoots.value.contains(repoRootB) { 2513 + secondFetchStarted = true 2514 + break 2515 + } 2516 + await Task.yield() 2517 + } 2518 + #expect(secondFetchStarted) 2519 + 2520 + await gate.resume() 2521 + 2522 + await store.receive(\.repositoriesLoaded) { 2523 + $0.repositories = [repoA, repoB] 2524 + $0.repositoryRoots = [repoRootA, repoRootB].map { URL(fileURLWithPath: $0) } 2525 + $0.isInitialLoadComplete = true 2526 + } 2527 + await store.receive(\.delegate.repositoriesChanged) 2528 + await store.finish() 2529 + } 2530 + 2531 + @Test func loadPersistedRepositoriesRestoresLastFocusedSelectionAfterFullLoad() async { 2532 + let testID = UUID().uuidString 2533 + let repoRootA = "/tmp/\(testID)-repo-a" 2534 + let repoRootB = "/tmp/\(testID)-repo-b" 2535 + let worktreeA = makeWorktree(id: "\(repoRootA)/main", name: "main", repoRoot: repoRootA) 2536 + let worktreeB = makeWorktree(id: "\(repoRootB)/main", name: "main", repoRoot: repoRootB) 2537 + let repoA = makeRepository( 2538 + id: repoRootA, 2539 + name: URL(fileURLWithPath: repoRootA).lastPathComponent, 2540 + worktrees: [worktreeA] 2541 + ) 2542 + let repoB = makeRepository( 2543 + id: repoRootB, 2544 + name: URL(fileURLWithPath: repoRootB).lastPathComponent, 2545 + worktrees: [worktreeB] 2546 + ) 2547 + 2548 + var state = RepositoriesFeature.State() 2549 + state.lastFocusedWorktreeID = worktreeB.id 2550 + state.shouldRestoreLastFocusedWorktree = true 2551 + 2552 + let store = TestStore(initialState: state) { 2553 + RepositoriesFeature() 2554 + } withDependencies: { 2555 + $0.repositoryPersistence.loadRoots = { [repoRootA, repoRootB] } 2556 + $0.gitClient.worktrees = { root in 2557 + switch root.path(percentEncoded: false) { 2558 + case repoRootA: 2559 + return [worktreeA] 2560 + case repoRootB: 2561 + return [worktreeB] 2562 + default: 2563 + Issue.record("Unexpected root: \(root.path(percentEncoded: false))") 2564 + return [] 2565 + } 2566 + } 2567 + } 2568 + 2569 + await store.send(.loadPersistedRepositories) 2570 + await store.receive(\.repositoriesLoaded) { 2571 + $0.repositories = [repoA, repoB] 2572 + $0.repositoryRoots = [repoRootA, repoRootB].map { URL(fileURLWithPath: $0) } 2573 + $0.selection = .worktree(worktreeB.id) 2574 + $0.shouldRestoreLastFocusedWorktree = false 2575 + $0.isInitialLoadComplete = true 2576 + } 2577 + await store.receive(\.delegate.repositoriesChanged) 2578 + await store.receive(\.delegate.selectedWorktreeChanged) 2579 + await store.finish() 2580 + } 2581 + 2582 + private actor AsyncGate { 2583 + var continuation: CheckedContinuation<Void, Never>? 2584 + var isOpen = false 2585 + 2586 + func wait() async { 2587 + guard !isOpen else { return } 2588 + await withCheckedContinuation { continuation in 2589 + self.continuation = continuation 2590 + } 2591 + } 2592 + 2593 + func resume() { 2594 + if let continuation { 2595 + continuation.resume() 2596 + self.continuation = nil 2597 + } else { 2598 + isOpen = true 2599 + } 2600 + } 2464 2601 } 2465 2602 }