Skip to content

Glob Filter re-parses the filter string for every item: ~9x slower and ~8x more allocation than the matching itself (2.4 KB per item for "*.cs") #190

Open

Description

@matt-edmondson

What's wrong

Filter (TextFilter/TextFilter.cs:187-192) calls IsMatch once per item. For a glob filter that reaches DoesMatchGlob (:337), which on every call:

  • runs ExtractGlobFilterTokens(filter) (:297-321): a split, a GroupBy, a HashSet per group and a ToDictionary, although the filter is the same for every item;
  • builds a HashSet<string> of the text's words (:343), although it is only used for Any/All;
  • calls ResolveGlob twice per optional token, each time building a fresh "s:" + pattern cache-key string (:117).

Only the DotNet.Glob match and the masking (IsGlobMatch, :448) actually depend on the item.

Measurement (main @ 5f420d9, Release, net10.0, warm)

100,000 items shaped like src/Module3/File123.cs some description text:

time allocated / item
TextFilter.Filter(items, "*.cs") 223 ms (597 ms cold) 2,437 B
same split + mask + Glob.IsMatch done by hand 24 ms 311 B
Filter(items, @"\.cs$", TextFilterType.Regex) ~91 ms not measured

Per call, from a breakdown across 100k calls:

  • ExtractGlobFilterTokens("*.cs") alone: 60 ms, ~1,070 B
  • ExtractTextTokens: 69 ms, ~670 B
  • the whole DoesMatchGlob: 183 ms, 2,450 B

Why it matters

The glob path is the default TextFilterType, and the library's typical use is a search box re-filtered on every keystroke. Most of the per-item cost, and most of the GC pressure, comes from parsing a string that never changes during the call. On large lists that is visible input lag.

Suggested fix

  1. Parse the filter once per Filter/Rank call into a private compiled form: the excluded, required and optional token lists, each holding its resolved Glob[].
  2. Use that form in a private matcher shared by Filter and DoesMatchGlob. DoesMatchGlob's public signature and behaviour stay unchanged.
  3. Optionally, cache the compiled filter by (filter, caseSensitivity) next to GlobCache, so single-item IsMatch callers benefit too.
  4. Use a string[] or span-based enumeration for the text's words instead of a HashSet.
  5. Build each token's cache key once, not once per lookup.

Acceptance:

  • The existing tests pass unchanged.
  • A glob Filter over 100k items allocates roughly what the hand-written split + Glob.IsMatch baseline does, with no filter-parse allocation per item, as measured with a BenchmarkDotNet or allocation-count test.

Activity

  1. matt-edmondson commented on Nov 6, 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