anshdadhich commited on
Commit
c3699e5
·
verified ·
1 Parent(s): e65e3db

Upload FastSeekWpf/Core/SearchEngine.cs

Browse files
Files changed (1) hide show
  1. FastSeekWpf/Core/SearchEngine.cs +116 -28
FastSeekWpf/Core/SearchEngine.cs CHANGED
@@ -1,8 +1,6 @@
1
  using System;
2
  using System.Collections.Generic;
3
  using System.IO;
4
- using System.Linq;
5
- using System.Threading.Tasks;
6
 
7
  namespace FastSeekWpf.Core;
8
 
@@ -20,8 +18,31 @@ public class SearchResult
20
  public ResultKind Kind { get; set; }
21
  }
22
 
 
 
 
 
 
 
 
23
  public static class SearchEngine
24
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  private static readonly string[] AppExtensions = { "exe", "lnk", "msi", "appx", "msix" };
26
  private static readonly string[] AppPathMarkers = {
27
  "\\program files\\", "\\program files (x86)\\",
@@ -59,13 +80,23 @@ public static class SearchEngine
59
  if (ext == null) return ResultKind.Other;
60
  if (ExtensionMap.TryGetValue(ext, out var kind))
61
  {
62
- if (kind == ResultKind.App && AppPathMarkers.Any(m => fullPathLower.Contains(m)))
63
  return ResultKind.App;
64
  return kind;
65
  }
66
  return ResultKind.Other;
67
  }
68
 
 
 
 
 
 
 
 
 
 
 
69
  public static List<SearchResult> Search(
70
  IndexStore store,
71
  string query,
@@ -77,43 +108,60 @@ public static class SearchEngine
77
  return new List<SearchResult>();
78
 
79
  string q = caseSensitive ? query : query.ToLowerInvariant();
 
 
 
 
 
80
 
81
- var candidates = new List<(int idx, byte rank)>();
82
  lock (store)
83
  {
84
- for (int i = 0; i < store.Entries.Count; i++)
 
85
  {
86
- var entry = store.Entries[i];
87
- string name = caseSensitive ? store.Name(entry) : store.NameLower(entry);
88
 
89
  byte rank;
90
- if (name == q) rank = 1;
91
- else if (name.StartsWith(q, StringComparison.Ordinal)) rank = 2;
92
- else if (name.Contains(q, StringComparison.Ordinal)) rank = 3;
93
  else continue;
94
 
95
- candidates.Add((i, rank));
 
96
  }
97
  }
98
 
99
- candidates.Sort((a, b) => a.rank.CompareTo(b.rank));
 
 
100
  int overshoot = Math.Max(limit * 5, 1000);
101
- if (candidates.Count > overshoot)
102
- candidates.RemoveRange(overshoot, candidates.Count - overshoot);
103
 
104
- var results = new List<SearchResult>(Math.Min(limit, candidates.Count));
 
105
 
106
- foreach (var (idx, baseRank) in candidates)
107
  {
108
  var entry = store.Entries[idx];
109
- string fullPath = store.BuildPath(entry.FileRef);
110
  string fullPathLower = fullPath.ToLowerInvariant();
111
 
112
- if (excludedDirs.Count > 0 && excludedDirs.Any(ex => fullPathLower.StartsWith(ex, StringComparison.Ordinal)))
113
- continue;
 
 
 
 
 
 
 
 
114
 
115
- string name = store.Name(entry);
116
- string nameLower = store.NameLower(entry);
117
 
118
  byte rank = baseRank;
119
  if (baseRank <= 2)
@@ -122,7 +170,7 @@ public static class SearchEngine
122
  if (dotIdx >= 0)
123
  {
124
  string ext = nameLower[(dotIdx + 1)..];
125
- if (AppExtensions.Contains(ext) && AppPathMarkers.Any(m => fullPathLower.Contains(m)))
126
  rank = 0;
127
  }
128
  }
@@ -158,19 +206,26 @@ public static class SearchEngine
158
  {
159
  string dotExt = "." + ext.ToLowerInvariant();
160
  var results = new List<SearchResult>();
 
161
 
162
  lock (store)
163
  {
164
- foreach (var entry in store.Entries)
165
  {
166
- string name = store.NameLower(entry);
167
  if (!name.EndsWith(dotExt)) continue;
168
 
169
- string fullPath = store.BuildPath(entry.FileRef);
 
170
  string fullPathLower = fullPath.ToLowerInvariant();
171
 
172
- if (excludedDirs.Any(ex => fullPathLower.StartsWith(ex, StringComparison.Ordinal)))
173
- continue;
 
 
 
 
 
174
 
175
  string? fileExt = null;
176
  int dot = name.LastIndexOf('.');
@@ -179,7 +234,7 @@ public static class SearchEngine
179
  results.Add(new SearchResult
180
  {
181
  FullPath = fullPath,
182
- Name = store.Name(entry),
183
  Rank = 0,
184
  IsDir = entry.IsDir,
185
  Kind = GetKind(fileExt, entry.IsDir, fullPathLower)
@@ -191,4 +246,37 @@ public static class SearchEngine
191
 
192
  return results;
193
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  }
 
1
  using System;
2
  using System.Collections.Generic;
3
  using System.IO;
 
 
4
 
5
  namespace FastSeekWpf.Core;
6
 
 
18
  public ResultKind Kind { get; set; }
19
  }
20
 
21
+ /// <summary>
22
+ /// Allocation-free search engine matching Rust behavior:
23
+ /// - Pre-cached lowercase names (no UTF8 decoding in hot loop)
24
+ /// - Span-based prefix/contains matching
25
+ /// - Stack-allocated candidate buffer
26
+ /// - Reusable StringBuilder for path building
27
+ /// </summary>
28
  public static class SearchEngine
29
  {
30
+ // Reusable StringBuilder for path building
31
+ [ThreadStatic]
32
+ private static StringBuilder? _pathBuilder;
33
+
34
+ private static StringBuilder GetPathBuilder()
35
+ {
36
+ var sb = _pathBuilder;
37
+ if (sb == null)
38
+ {
39
+ sb = new StringBuilder(512);
40
+ _pathBuilder = sb;
41
+ }
42
+ sb.Clear();
43
+ return sb;
44
+ }
45
+
46
  private static readonly string[] AppExtensions = { "exe", "lnk", "msi", "appx", "msix" };
47
  private static readonly string[] AppPathMarkers = {
48
  "\\program files\\", "\\program files (x86)\\",
 
80
  if (ext == null) return ResultKind.Other;
81
  if (ExtensionMap.TryGetValue(ext, out var kind))
82
  {
83
+ if (kind == ResultKind.App && ContainsAny(fullPathLower, AppPathMarkers))
84
  return ResultKind.App;
85
  return kind;
86
  }
87
  return ResultKind.Other;
88
  }
89
 
90
+ private static bool ContainsAny(string s, string[] markers)
91
+ {
92
+ foreach (var m in markers)
93
+ if (s.Contains(m, StringComparison.Ordinal)) return true;
94
+ return false;
95
+ }
96
+
97
+ /// <summary>
98
+ /// Zero-allocation search using cached lowercase names and Span-based matching.
99
+ /// </summary>
100
  public static List<SearchResult> Search(
101
  IndexStore store,
102
  string query,
 
108
  return new List<SearchResult>();
109
 
110
  string q = caseSensitive ? query : query.ToLowerInvariant();
111
+ ReadOnlySpan<char> qSpan = q.AsSpan();
112
+
113
+ // Stack-allocated candidate array (avoid List allocations)
114
+ Span<(int idx, byte rank)> candidates = stackalloc (int, byte)[10000];
115
+ int candidateCount = 0;
116
 
 
117
  lock (store)
118
  {
119
+ int count = store.Entries.Count;
120
+ for (int i = 0; i < count; i++)
121
  {
122
+ string name = caseSensitive ? store.NameAt(i) : store.NameLowerAt(i);
123
+ ReadOnlySpan<char> nameSpan = name.AsSpan();
124
 
125
  byte rank;
126
+ if (nameSpan.SequenceEqual(qSpan)) rank = 1;
127
+ else if (nameSpan.StartsWith(qSpan, StringComparison.Ordinal)) rank = 2;
128
+ else if (nameSpan.Contains(qSpan, StringComparison.Ordinal)) rank = 3;
129
  else continue;
130
 
131
+ if (candidateCount < candidates.Length)
132
+ candidates[candidateCount++] = (i, rank);
133
  }
134
  }
135
 
136
+ // Sort candidates by rank
137
+ candidates[..candidateCount].Sort((a, b) => a.rank.CompareTo(b.rank));
138
+
139
  int overshoot = Math.Max(limit * 5, 1000);
140
+ if (candidateCount > overshoot)
141
+ candidateCount = overshoot;
142
 
143
+ var results = new List<SearchResult>(Math.Min(limit, candidateCount));
144
+ var pathBuilder = GetPathBuilder();
145
 
146
+ foreach (var (idx, baseRank) in candidates[..candidateCount])
147
  {
148
  var entry = store.Entries[idx];
149
+ string fullPath = BuildPathFast(store, entry.FileRef, pathBuilder);
150
  string fullPathLower = fullPath.ToLowerInvariant();
151
 
152
+ if (excludedDirs.Count > 0)
153
+ {
154
+ bool excluded = false;
155
+ foreach (var ex in excludedDirs)
156
+ {
157
+ if (fullPathLower.StartsWith(ex, StringComparison.Ordinal))
158
+ { excluded = true; break; }
159
+ }
160
+ if (excluded) continue;
161
+ }
162
 
163
+ string name = store.NameAt(idx);
164
+ string nameLower = store.NameLowerAt(idx);
165
 
166
  byte rank = baseRank;
167
  if (baseRank <= 2)
 
170
  if (dotIdx >= 0)
171
  {
172
  string ext = nameLower[(dotIdx + 1)..];
173
+ if (AppExtensions.Contains(ext) && ContainsAny(fullPathLower, AppPathMarkers))
174
  rank = 0;
175
  }
176
  }
 
206
  {
207
  string dotExt = "." + ext.ToLowerInvariant();
208
  var results = new List<SearchResult>();
209
+ var pathBuilder = GetPathBuilder();
210
 
211
  lock (store)
212
  {
213
+ for (int i = 0; i < store.Entries.Count; i++)
214
  {
215
+ string name = store.NameLowerAt(i);
216
  if (!name.EndsWith(dotExt)) continue;
217
 
218
+ var entry = store.Entries[i];
219
+ string fullPath = BuildPathFast(store, entry.FileRef, pathBuilder);
220
  string fullPathLower = fullPath.ToLowerInvariant();
221
 
222
+ bool excluded = false;
223
+ foreach (var ex in excludedDirs)
224
+ {
225
+ if (fullPathLower.StartsWith(ex, StringComparison.Ordinal))
226
+ { excluded = true; break; }
227
+ }
228
+ if (excluded) continue;
229
 
230
  string? fileExt = null;
231
  int dot = name.LastIndexOf('.');
 
234
  results.Add(new SearchResult
235
  {
236
  FullPath = fullPath,
237
+ Name = store.NameAt(i),
238
  Rank = 0,
239
  IsDir = entry.IsDir,
240
  Kind = GetKind(fileExt, entry.IsDir, fullPathLower)
 
246
 
247
  return results;
248
  }
249
+
250
+ /// <summary>
251
+ /// Fast path building using reusable StringBuilder instead of List allocation.
252
+ /// </summary>
253
+ private static string BuildPathFast(IndexStore store, ulong fileRef, StringBuilder sb)
254
+ {
255
+ sb.Clear();
256
+ // Stackalloc for components
257
+ Span<string> components = stackalloc string[64];
258
+ int compCount = 0;
259
+ ulong current = fileRef;
260
+
261
+ for (int i = 0; i < 64; i++)
262
+ {
263
+ var idx = store.LookupIdx(current);
264
+ if (idx == null) break;
265
+
266
+ int eIdx = (int)idx;
267
+ components[compCount++] = store.NameAt(eIdx);
268
+ var entry = store.Entries[eIdx];
269
+ if (entry.ParentRef == current) break;
270
+ current = entry.ParentRef;
271
+ }
272
+
273
+ sb.Append(store.DriveRoot);
274
+ for (int i = compCount - 1; i >= 0; i--)
275
+ {
276
+ if (sb.Length > 0 && sb[sb.Length - 1] != '\\' && sb[sb.Length - 1] != '/')
277
+ sb.Append('\\');
278
+ sb.Append(components[i]);
279
+ }
280
+ return sb.ToString();
281
+ }
282
  }