mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-06-28 05:29:29 +00:00
Optimize GitRepositoryMonitor to reduce redundant checks
- Implement repository deduplication in SessionMonitor to check each unique directory only once - Add smart parent directory detection for common development paths (Projects, Development, etc.) - Extend cache duration for common parent directories from 30s to 5 minutes - Add negative caching for non-Git directories (10 minute cache) - Group sessions by repository before Git checks to avoid duplicate lookups This reduces Git repository checks from ~100/minute to significantly fewer by: 1. Deduplicating checks across multiple sessions in the same repository 2. Caching parent directory results longer 3. Remembering non-Git paths to avoid repeated filesystem traversals
This commit is contained in:
parent
7674ef07d3
commit
3f86ed365c
7 changed files with 169 additions and 43 deletions
|
|
@ -179,19 +179,23 @@ struct FollowModeStatus: Codable {
|
|||
///
|
||||
/// ```swift
|
||||
/// let request = CreateWorktreeRequest(
|
||||
/// repoPath: "/path/to/repo",
|
||||
/// branch: "feature/new-feature",
|
||||
/// createBranch: true,
|
||||
/// path: "/path/to/worktree",
|
||||
/// baseBranch: "main"
|
||||
/// )
|
||||
/// ```
|
||||
struct CreateWorktreeRequest: Codable {
|
||||
/// The repository path where the worktree will be created.
|
||||
let repoPath: String
|
||||
|
||||
/// The branch name for the new worktree.
|
||||
let branch: String
|
||||
|
||||
/// The file system path where the worktree will be created.
|
||||
let path: String
|
||||
|
||||
/// Whether to create the branch if it doesn't exist.
|
||||
let createBranch: Bool
|
||||
|
||||
/// The base branch to create from when `createBranch` is true.
|
||||
/// The base branch to create from when creating a new branch.
|
||||
///
|
||||
/// If nil, uses the repository's default branch.
|
||||
let baseBranch: String?
|
||||
|
|
@ -202,11 +206,11 @@ struct CreateWorktreeRequest: Codable {
|
|||
/// This allows changing the checked-out branch without creating
|
||||
/// a new worktree, useful for quick context switches.
|
||||
struct SwitchBranchRequest: Codable {
|
||||
/// The repository path where the branch switch will occur.
|
||||
let repoPath: String
|
||||
|
||||
/// The branch to switch to.
|
||||
let branch: String
|
||||
|
||||
/// Whether to create the branch if it doesn't exist.
|
||||
let createBranch: Bool
|
||||
}
|
||||
|
||||
/// Request payload for toggling follow mode.
|
||||
|
|
@ -215,19 +219,22 @@ struct SwitchBranchRequest: Codable {
|
|||
///
|
||||
/// ```swift
|
||||
/// // Enable follow mode
|
||||
/// let enableRequest = FollowModeRequest(enabled: true, targetBranch: "develop")
|
||||
/// let enableRequest = FollowModeRequest(repoPath: "/path/to/repo", branch: "develop", enable: true)
|
||||
///
|
||||
/// // Disable follow mode
|
||||
/// let disableRequest = FollowModeRequest(enabled: false, targetBranch: nil)
|
||||
/// let disableRequest = FollowModeRequest(repoPath: "/path/to/repo", branch: nil, enable: false)
|
||||
/// ```
|
||||
struct FollowModeRequest: Codable {
|
||||
/// Whether to enable or disable follow mode.
|
||||
let enabled: Bool
|
||||
|
||||
/// The repository path where follow mode will be configured.
|
||||
let repoPath: String
|
||||
|
||||
/// The branch to follow when enabling.
|
||||
///
|
||||
/// Required when `enabled` is true, ignored otherwise.
|
||||
let targetBranch: String?
|
||||
/// Required when `enable` is true, ignored otherwise.
|
||||
let branch: String?
|
||||
|
||||
/// Whether to enable or disable follow mode.
|
||||
let enable: Bool
|
||||
}
|
||||
|
||||
/// Represents a Git branch in the repository.
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ final class ConfigManager {
|
|||
private func useDefaults() {
|
||||
self.quickStartCommands = defaultCommands
|
||||
self.repositoryBasePath = FilePathConstants.defaultRepositoryBasePath
|
||||
|
||||
|
||||
// Set notification defaults to match TypeScript defaults
|
||||
self.notificationsEnabled = true
|
||||
self.notificationSessionStart = true
|
||||
|
|
@ -235,7 +235,7 @@ final class ConfigManager {
|
|||
self.notificationClaudeTurn = false
|
||||
self.notificationSoundEnabled = true
|
||||
self.notificationVibrationEnabled = true
|
||||
|
||||
|
||||
saveConfiguration()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -218,17 +218,29 @@ public final class GitRepositoryMonitor {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Check if this path was recently confirmed as non-Git
|
||||
if let nonGitCheck = nonGitPathCache[filePath],
|
||||
Date().timeIntervalSince(nonGitCheck) < 600.0
|
||||
{ // 10 minutes for non-Git paths
|
||||
logger.debug("⏭️ Skipping known non-Git path: \(filePath)")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
if let cached = getCachedRepository(for: filePath) {
|
||||
logger.debug("📦 Found cached repository for: \(filePath)")
|
||||
|
||||
// Check if this was recently checked (within 30 seconds)
|
||||
// Use longer cache duration for common parent directories
|
||||
let cacheThreshold = isCommonParentDirectory(filePath) ? 300.0 :
|
||||
recentCheckThreshold // 5 minutes vs 30 seconds
|
||||
|
||||
// Check if this was recently checked
|
||||
if let lastCheck = recentRepositoryChecks[filePath],
|
||||
Date().timeIntervalSince(lastCheck) < recentCheckThreshold
|
||||
Date().timeIntervalSince(lastCheck) < cacheThreshold
|
||||
{
|
||||
logger
|
||||
.debug(
|
||||
"⏭️ Skipping redundant check for: \(filePath) (checked \(Int(Date().timeIntervalSince(lastCheck)))s ago)"
|
||||
"⏭️ Skipping redundant check for: \(filePath) (checked \(Int(Date().timeIntervalSince(lastCheck)))s ago, threshold: \(Int(cacheThreshold))s)"
|
||||
)
|
||||
return cached
|
||||
}
|
||||
|
|
@ -248,8 +260,14 @@ public final class GitRepositoryMonitor {
|
|||
guard let repoPath = await self.findGitRoot(from: filePath) else {
|
||||
logger.info("❌ No Git root found for: \(filePath)")
|
||||
// Mark as recently checked even for non-git paths to avoid repeated checks
|
||||
// Use longer cache for common parent directories that aren't Git repos
|
||||
await MainActor.run {
|
||||
self.recentRepositoryChecks[filePath] = Date()
|
||||
// Cache the negative result for parent directories longer
|
||||
if self.isCommonParentDirectory(filePath) {
|
||||
// Store as a "non-git" marker for 10 minutes
|
||||
self.nonGitPathCache[filePath] = Date()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -305,6 +323,7 @@ public final class GitRepositoryMonitor {
|
|||
githubURLFetchesInProgress.removeAll()
|
||||
pendingRepositoryRequests.removeAll()
|
||||
recentRepositoryChecks.removeAll()
|
||||
nonGitPathCache.removeAll()
|
||||
}
|
||||
|
||||
/// Start monitoring and refreshing all cached repositories
|
||||
|
|
@ -346,7 +365,17 @@ public final class GitRepositoryMonitor {
|
|||
recentRepositoryChecks = recentRepositoryChecks.filter { _, checkDate in
|
||||
checkDate > cutoffDate
|
||||
}
|
||||
logger.debug("🧹 Cleaned up recent checks cache, \(self.recentRepositoryChecks.count) entries remaining")
|
||||
|
||||
// Also cleanup non-Git path cache (remove entries older than 10 minutes)
|
||||
let nonGitCutoff = Date().addingTimeInterval(-600.0)
|
||||
nonGitPathCache = nonGitPathCache.filter { _, checkDate in
|
||||
checkDate > nonGitCutoff
|
||||
}
|
||||
|
||||
logger
|
||||
.debug(
|
||||
"🧹 Cleaned up caches: \(self.recentRepositoryChecks.count) recent checks, \(self.nonGitPathCache.count) non-Git paths"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Private Properties
|
||||
|
|
@ -372,11 +401,39 @@ public final class GitRepositoryMonitor {
|
|||
/// Tracks recent repository checks with timestamps to skip redundant checks
|
||||
private var recentRepositoryChecks: [String: Date] = [:]
|
||||
|
||||
/// Cache for paths that are confirmed NOT to be Git repositories (to avoid repeated filesystem checks)
|
||||
private var nonGitPathCache: [String: Date] = [:]
|
||||
|
||||
/// Duration to consider a repository check as "recent" (30 seconds)
|
||||
private let recentCheckThreshold: TimeInterval = 30.0
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
/// Check if a path is a common parent directory that should have longer cache duration
|
||||
private func isCommonParentDirectory(_ path: String) -> Bool {
|
||||
let pathComponents = path.split(separator: "/").map(String.init)
|
||||
|
||||
// Check if this is a common development parent directory
|
||||
let commonDevPaths = ["Projects", "Development", "Developer", "Code", "Work", "Source", "Workspace", "Repos"]
|
||||
|
||||
// If the path ends with one of these common directory names, it's likely a parent
|
||||
if let lastComponent = pathComponents.last,
|
||||
commonDevPaths.contains(lastComponent)
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
// Also check for patterns like /Users/username/Projects
|
||||
if pathComponents.count == 3,
|
||||
pathComponents[0] == "Users",
|
||||
commonDevPaths.contains(pathComponents[2])
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func cacheRepository(_ repository: GitRepository, originalFilePath: String? = nil) {
|
||||
repositoryCache[repository.path] = repository
|
||||
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ final class NotificationService: NSObject {
|
|||
vibrationEnabled: prefs.vibrationEnabled
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/// Get notification sound based on user preferences
|
||||
private func getNotificationSound(critical: Bool = false) -> UNNotificationSound? {
|
||||
guard preferences.soundEnabled else { return nil }
|
||||
|
|
|
|||
|
|
@ -195,14 +195,9 @@ final class SessionMonitor {
|
|||
// Update WindowTracker
|
||||
WindowTracker.shared.updateFromSessions(sessionsArray)
|
||||
|
||||
// Pre-cache Git data for all sessions
|
||||
// Pre-cache Git data for all sessions (deduplicated by repository)
|
||||
if let gitMonitor = gitRepositoryMonitor {
|
||||
for session in sessionsArray where gitMonitor.getCachedRepository(for: session.workingDir) == nil {
|
||||
Task {
|
||||
// This will cache the data for immediate access later
|
||||
_ = await gitMonitor.findRepository(for: session.workingDir)
|
||||
}
|
||||
}
|
||||
await preCacheGitRepositories(for: sessionsArray, using: gitMonitor)
|
||||
}
|
||||
} catch {
|
||||
// Only update error if it's not a simple connection error
|
||||
|
|
@ -215,6 +210,60 @@ final class SessionMonitor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Pre-cache Git repositories for sessions, deduplicating by repository root
|
||||
private func preCacheGitRepositories(for sessions: [ServerSessionInfo], using gitMonitor: GitRepositoryMonitor) async {
|
||||
// Track unique directories we need to check
|
||||
var uniqueDirectoriesToCheck = Set<String>()
|
||||
|
||||
// First, collect all unique directories that don't have cached data
|
||||
for session in sessions {
|
||||
// Skip if we already have cached data for this exact path
|
||||
if gitMonitor.getCachedRepository(for: session.workingDir) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add this directory to check
|
||||
uniqueDirectoriesToCheck.insert(session.workingDir)
|
||||
|
||||
// Smart detection: Also check common parent directories
|
||||
// This helps when multiple sessions are in subdirectories of the same project
|
||||
let pathComponents = session.workingDir.split(separator: "/").map(String.init)
|
||||
|
||||
// Check if this looks like a project directory pattern
|
||||
// Common patterns: /Users/*/Projects/*, /Users/*/Development/*, etc.
|
||||
if pathComponents.count >= 4 {
|
||||
// Check if we're in a common development directory
|
||||
let commonDevPaths = ["Projects", "Development", "Developer", "Code", "Work", "Source"]
|
||||
|
||||
for (index, component) in pathComponents.enumerated() {
|
||||
if commonDevPaths.contains(component) && index < pathComponents.count - 1 {
|
||||
// This might be a parent project directory
|
||||
// Add the immediate child of the development directory
|
||||
let potentialProjectPath = "/" + pathComponents[0...index + 1].joined(separator: "/")
|
||||
|
||||
// Only add if we don't have cached data for it
|
||||
if gitMonitor.getCachedRepository(for: potentialProjectPath) == nil {
|
||||
uniqueDirectoriesToCheck.insert(potentialProjectPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now check each unique directory only once
|
||||
for directory in uniqueDirectoriesToCheck {
|
||||
Task {
|
||||
// This will cache the data for immediate access later
|
||||
_ = await gitMonitor.findRepository(for: directory)
|
||||
}
|
||||
}
|
||||
|
||||
logger
|
||||
.debug(
|
||||
"Pre-caching Git data for \(uniqueDirectoriesToCheck.count) unique directories (from \(sessions.count) sessions)"
|
||||
)
|
||||
}
|
||||
|
||||
/// Start periodic refresh of sessions
|
||||
private func startPeriodicRefresh() {
|
||||
// Clean up any existing timer
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ final class WorktreeService {
|
|||
let worktreeResponse = try await serverManager.performRequest(
|
||||
endpoint: "/api/worktrees",
|
||||
method: "GET",
|
||||
queryItems: [URLQueryItem(name: "gitRepoPath", value: gitRepoPath)],
|
||||
queryItems: [URLQueryItem(name: "repoPath", value: gitRepoPath)],
|
||||
responseType: WorktreeListResponse.self
|
||||
)
|
||||
self.worktrees = worktreeResponse.worktrees
|
||||
|
|
@ -48,17 +48,21 @@ final class WorktreeService {
|
|||
func createWorktree(
|
||||
gitRepoPath: String,
|
||||
branch: String,
|
||||
createBranch: Bool,
|
||||
worktreePath: String,
|
||||
baseBranch: String? = nil
|
||||
)
|
||||
async throws
|
||||
{
|
||||
let request = CreateWorktreeRequest(branch: branch, createBranch: createBranch, baseBranch: baseBranch)
|
||||
let request = CreateWorktreeRequest(
|
||||
repoPath: gitRepoPath,
|
||||
branch: branch,
|
||||
path: worktreePath,
|
||||
baseBranch: baseBranch
|
||||
)
|
||||
try await serverManager.performVoidRequest(
|
||||
endpoint: "/api/worktrees",
|
||||
method: "POST",
|
||||
body: request,
|
||||
queryItems: [URLQueryItem(name: "gitRepoPath", value: gitRepoPath)]
|
||||
body: request
|
||||
)
|
||||
|
||||
// Refresh the worktree list
|
||||
|
|
@ -71,7 +75,7 @@ final class WorktreeService {
|
|||
endpoint: "/api/worktrees/\(branch)",
|
||||
method: "DELETE",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "gitRepoPath", value: gitRepoPath),
|
||||
URLQueryItem(name: "repoPath", value: gitRepoPath),
|
||||
URLQueryItem(name: "force", value: String(force))
|
||||
]
|
||||
)
|
||||
|
|
@ -81,13 +85,12 @@ final class WorktreeService {
|
|||
}
|
||||
|
||||
/// Switch to a different branch
|
||||
func switchBranch(gitRepoPath: String, branch: String, createBranch: Bool = false) async throws {
|
||||
let request = SwitchBranchRequest(branch: branch, createBranch: createBranch)
|
||||
func switchBranch(gitRepoPath: String, branch: String) async throws {
|
||||
let request = SwitchBranchRequest(repoPath: gitRepoPath, branch: branch)
|
||||
try await serverManager.performVoidRequest(
|
||||
endpoint: "/api/worktrees/switch",
|
||||
method: "POST",
|
||||
body: request,
|
||||
queryItems: [URLQueryItem(name: "gitRepoPath", value: gitRepoPath)]
|
||||
body: request
|
||||
)
|
||||
|
||||
// Refresh the worktree list
|
||||
|
|
@ -96,12 +99,11 @@ final class WorktreeService {
|
|||
|
||||
/// Toggle follow mode
|
||||
func toggleFollowMode(gitRepoPath: String, enabled: Bool, targetBranch: String? = nil) async throws {
|
||||
let request = FollowModeRequest(enabled: enabled, targetBranch: targetBranch)
|
||||
let request = FollowModeRequest(repoPath: gitRepoPath, branch: targetBranch, enable: enabled)
|
||||
try await serverManager.performVoidRequest(
|
||||
endpoint: "/api/worktrees/follow",
|
||||
method: "POST",
|
||||
body: request,
|
||||
queryItems: [URLQueryItem(name: "gitRepoPath", value: gitRepoPath)]
|
||||
body: request
|
||||
)
|
||||
|
||||
// Refresh the worktree list
|
||||
|
|
|
|||
|
|
@ -209,11 +209,22 @@ struct NewSessionForm: View {
|
|||
}
|
||||
},
|
||||
onCreateWorktree: { branchName, baseBranch in
|
||||
// Generate worktree path by slugifying branch name
|
||||
let slugifiedBranch = branchName
|
||||
.replacingOccurrences(of: "/", with: "-")
|
||||
.replacingOccurrences(of: " ", with: "-")
|
||||
.lowercased()
|
||||
|
||||
// Create worktree path in a 'worktrees' subdirectory
|
||||
let repoURL = URL(fileURLWithPath: repoPath)
|
||||
let worktreesDir = repoURL.appendingPathComponent("worktrees")
|
||||
let worktreePath = worktreesDir.appendingPathComponent(slugifiedBranch).path
|
||||
|
||||
// Create the worktree
|
||||
try await service.createWorktree(
|
||||
gitRepoPath: repoPath,
|
||||
branch: branchName,
|
||||
createBranch: true,
|
||||
worktreePath: worktreePath,
|
||||
baseBranch: baseBranch
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue