You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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.
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
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[].
Use that form in a private matcher shared by Filter and DoesMatchGlob. DoesMatchGlob's public signature and behaviour stay unchanged.
Optionally, cache the compiled filter by (filter, caseSensitivity) next to GlobCache, so single-item IsMatch callers benefit too.
Use a string[] or span-based enumeration for the text's words instead of a HashSet.
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.
Priority: Medium. Results are correct today, but glob is the default TextFilterType and the typical use is re-filtering a list on every keystroke, so a ~9x per-item overhead and 2.4 KB of garbage per item shows up as input lag on large lists. The fix is contained and has clear acceptance criteria.
Area / suggested assignment: the glob matching path in TextFilter/TextFilter.cs (Filter, DoesMatchGlob, ExtractGlobFilterTokens, ResolveGlob / GlobCache).
Duplicates / in progress: not a duplicate, and no open PR covers it. Related but separate issues:
Next step: compile the filter once per Filter/Rank call into excluded/required/optional Glob[] lists and share a private matcher with DoesMatchGlob, keeping its public behaviour unchanged. Add an allocation-count test (or a BenchmarkDotNet run) against the hand-written split + Glob.IsMatch baseline as the acceptance check.
Activity
matt-edmondson commentedon Nov 6, 2026
Triage
TextFilterTypeand the typical use is re-filtering a list on every keystroke, so a ~9x per-item overhead and 2.4 KB of garbage per item shows up as input lag on large lists. The fix is contained and has clear acceptance criteria.TextFilter/TextFilter.cs(Filter,DoesMatchGlob,ExtractGlobFilterTokens,ResolveGlob/GlobCache).*-*-*-*-*-*-*xtakes ~6 s per item against an 80-dash line (56 s with one more*-): glob matching is exponential and, unlike regex, has no timeout #131 is the exponential-backtracking glob cost. That is a matching-algorithm problem; this one is per-item filter parsing.Filternot streaming.GlobOptions.Default.Evaluation.CaseInsensitive:*.jpgmatchesIMG.JPG#188) changes how globs are built and cached with respect to case sensitivity. Step 3 here, caching the compiled filter by(filter, caseSensitivity)next toGlobCache, should be built on top of Keep case-sensitive globs off DotNet.Glob's global default [patch] #189, not alongside it.mainCI is currently red, so a PR for this can't release until that is fixed.Filter/Rankcall into excluded/required/optionalGlob[]lists and share a private matcher withDoesMatchGlob, keeping its public behaviour unchanged. Add an allocation-count test (or a BenchmarkDotNet run) against the hand-written split +Glob.IsMatchbaseline as the acceptance check.Generated by Claude Code