native macOS codings agent orchestrator
6
fork

Configure Feed

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

Merge pull request #15 from onevcat/startup/parallel-loading

Parallelize repository startup loading

authored by

Wei Wang and committed by
GitHub
812189d5 64d09282

+179 -6
+40 -4
supacode/Features/Repositories/Reducer/RepositoriesFeature.swift
··· 2603 2603 .cancellable(id: CancelID.load, cancelInFlight: true) 2604 2604 } 2605 2605 2606 + private struct WorktreesFetchResult: Sendable { 2607 + let root: URL 2608 + let worktrees: [Worktree]? 2609 + let errorMessage: String? 2610 + } 2611 + 2606 2612 private func loadRepositoriesData(_ roots: [URL]) async -> ([Repository], [LoadFailure]) { 2613 + let fetchResults = await withTaskGroup(of: WorktreesFetchResult.self) { group in 2614 + for root in roots { 2615 + let gitClient = self.gitClient 2616 + group.addTask { 2617 + do { 2618 + let worktrees = try await gitClient.worktrees(root) 2619 + return WorktreesFetchResult(root: root, worktrees: worktrees, errorMessage: nil) 2620 + } catch { 2621 + return WorktreesFetchResult( 2622 + root: root, 2623 + worktrees: nil, 2624 + errorMessage: error.localizedDescription 2625 + ) 2626 + } 2627 + } 2628 + } 2629 + 2630 + var resultsByRootID: [Repository.ID: WorktreesFetchResult] = [:] 2631 + for await result in group { 2632 + let rootID = result.root.standardizedFileURL.path(percentEncoded: false) 2633 + resultsByRootID[rootID] = result 2634 + } 2635 + return resultsByRootID 2636 + } 2637 + 2607 2638 var loaded: [Repository] = [] 2608 2639 var failures: [LoadFailure] = [] 2609 2640 for root in roots { 2610 2641 let normalizedRoot = root.standardizedFileURL 2611 2642 let rootID = normalizedRoot.path(percentEncoded: false) 2612 - do { 2613 - let worktrees = try await gitClient.worktrees(root) 2643 + guard let result = fetchResults[rootID] else { continue } 2644 + if let worktrees = result.worktrees { 2614 2645 let name = Repository.name(for: normalizedRoot) 2615 2646 let repository = Repository( 2616 2647 id: rootID, ··· 2619 2650 worktrees: IdentifiedArray(uniqueElements: worktrees) 2620 2651 ) 2621 2652 loaded.append(repository) 2622 - } catch { 2623 - failures.append(LoadFailure(rootID: rootID, message: error.localizedDescription)) 2653 + } else { 2654 + failures.append( 2655 + LoadFailure( 2656 + rootID: rootID, 2657 + message: result.errorMessage ?? "Unknown error" 2658 + ) 2659 + ) 2624 2660 } 2625 2661 } 2626 2662 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 }