Skip to content

Repo and compare browsers stat every entry on the file system on every frame (plus O(n²) Contains lookups) #498

Open

Description

@matt-edmondson

What's wrong

Since #482/#490 ("Classify browser entries from the repository root"), the browsers decide whether an entry is a folder by asking the file system (RepoBrowsing.IsDirectory → Directory.Exists) instead of looking at how the entry is spelled. That is the correct check, but it runs again on every frame rather than once when the listing is taken:

  • Compare browser. ShowCompareBrowser (ProjectDirector/ProjectDirector.cs:2074-2080) does the following every frame:

    • builds BrowserContentsBase.Union(BrowserContentsCompare)
    • calls ListBrowserDirectories(entries, baseRoot, compareRoot)
    • then calls Except(directories)

    IsDirectory (ProjectDirector/RepoBrowsing.cs:52-53) calls Directory.Exists once per root. A plain file therefore costs a failed stat in both repositories on every frame.

  • Repo browser. ShowRepoBrowser (ProjectDirector.cs:2242-2247) does the same for the base repository.

  • Per-row lookups. Each compare-browser row also calls BrowserContentsBase.Contains(path) and BrowserContentsCompare.Contains(path) (around ProjectDirector.cs:2112-2113 and 2182-2183). These are linear scans of a Collection<T>, so each frame does O(n²) work.

The listing only changes in SwitchRepoBrowserPath and SwitchCompareBrowserPath (ProjectDirector.cs:2439-2471). Those are the only places the classification can change.

Why it matters

The browser is drawn whenever a repository is selected. Comparing a folder of 150 entries against a sibling repository costs about 300 stat calls per frame, which is about 18,000 per second at 60 fps.

Each call gets noticeably more expensive in directory-heavy trees, and on Windows with real-time antivirus scanning. The UI can then stutter just from showing a folder.

The listing also isn't refreshed after Give/Take/X (#456), so re-checking every frame doesn't even keep it current.

Suggested fix / acceptance criteria

  • Classify entries once, when the listing is taken. RepoBrowsing.List or the Switch*BrowserPath methods would produce three things, stored next to BrowserContentsBase and BrowserContentsCompare:
    • the folders
    • the files
    • which side each entry is on, as one HashSet<RelativePath> per side
  • ShowRepoBrowser and ShowCompareBrowser read only the cached sets. They make no file-system calls and no linear Contains lookups while drawing.
  • After a Give/Take/X action, take the listing again, which re-classifies it. This also fixes the stale-row half of Compare browser: Give/Take and X only respond on the first row that shows them, and a row that was acted on stays listed until you navigate away #456.
  • RepoBrowsingTests and BrowserDirectoryTests still pass.
  • A new test checks that the classification made at listing time matches IsDirectory for:
    • nested folders
    • a name that is a folder in one repository and missing in the other

Activity

  1. matt-edmondson commented on Oct 19, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions