• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orion-ecs / keen-eye / 20249440466

15 Dec 2025 10:22PM UTC coverage: 91.745% (-0.008%) from 91.753%
20249440466

Pull #489

github

web-flow
Merge 40848a9e0 into 763efa493
Pull Request #489: ci: Remove Stop hook from settings.json

1812 of 1964 branches covered (92.26%)

Branch coverage included in aggregate %.

9924 of 10828 relevant lines covered (91.65%)

1.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.52
/src/KeenEyes.Core/Queries/QueryManager.cs
1
using System.Collections.Concurrent;
2

3
namespace KeenEyes;
4

5
/// <summary>
6
/// Thread-safe container for cached archetype matches using copy-on-write semantics.
7
/// </summary>
8
/// <remarks>
9
/// <para>
10
/// This class provides lock-free reads and synchronized writes for archetype caching.
11
/// Reads return a volatile reference to an immutable array snapshot, while writes
12
/// atomically replace the entire array under a lock.
13
/// </para>
14
/// <para>
15
/// The copy-on-write pattern is optimal for read-heavy workloads where archetype
16
/// creation (writes) is rare compared to query execution (reads).
17
/// </para>
18
/// </remarks>
19
internal sealed class ArchetypeCache
20
{
21
    private volatile Archetype[] archetypes = [];
7✔
22
    private volatile bool isPopulated;
23
    private readonly Lock writeLock = new();
7✔
24

25
    /// <summary>
26
    /// Gets a value indicating whether the cache has been populated.
27
    /// </summary>
28
    public bool IsPopulated => isPopulated;
4✔
29

30
    /// <summary>
31
    /// Gets the cached archetypes as a read-only list.
32
    /// </summary>
33
    /// <remarks>
34
    /// This property is lock-free and returns a snapshot of the cached archetypes.
35
    /// The returned array is safe to iterate even while other threads add archetypes.
36
    /// </remarks>
37
    public IReadOnlyList<Archetype> Archetypes => archetypes;
7✔
38

39
    /// <summary>
40
    /// Adds an archetype to the cache using copy-on-write semantics.
41
    /// </summary>
42
    /// <param name="archetype">The archetype to add.</param>
43
    /// <remarks>
44
    /// This method is idempotent - adding an archetype that already exists is a no-op.
45
    /// </remarks>
46
    public void Add(Archetype archetype)
47
    {
2✔
48
        lock (writeLock)
49
        {
50
            var current = archetypes;
2✔
51

52
            // Check if archetype already exists (deduplication)
53
            foreach (var existing in current)
2✔
54
            {
55
                if (ReferenceEquals(existing, archetype))
2✔
56
                {
57
                    return;
×
58
                }
59
            }
60

61
            var newArray = new Archetype[current.Length + 1];
2✔
62
            Array.Copy(current, newArray, current.Length);
2✔
63
            newArray[current.Length] = archetype;
2✔
64
            archetypes = newArray;
2✔
65
        }
2✔
66
    }
2✔
67

68
    /// <summary>
69
    /// Sets all archetypes in the cache, replacing any existing entries.
70
    /// </summary>
71
    /// <param name="items">The archetypes to cache.</param>
72
    public void SetAll(List<Archetype> items)
73
    {
×
74
        lock (writeLock)
75
        {
76
            archetypes = [.. items];
×
77
            isPopulated = true;
×
78
        }
×
79
    }
×
80

81
    /// <summary>
82
    /// Populates the cache with matching archetypes if not already populated.
83
    /// Thread-safe: only the first caller populates, others wait.
84
    /// </summary>
85
    /// <param name="allArchetypes">All archetypes to search through.</param>
86
    /// <param name="descriptor">The query descriptor to match against.</param>
87
    /// <remarks>
88
    /// This method ensures that only one thread populates the cache.
89
    /// Any archetypes added via <see cref="Add"/> during population will be
90
    /// included in the final result because we use a HashSet to deduplicate.
91
    /// </remarks>
92
    public void PopulateIfEmpty(IReadOnlyList<Archetype> allArchetypes, QueryDescriptor descriptor)
93
    {
94
        // Fast path: already populated
95
        if (isPopulated)
7✔
96
        {
97
            return;
×
98
        }
99

100
        lock (writeLock)
101
        {
102
            // Double-check after acquiring lock
103
            if (isPopulated)
7✔
104
            {
105
                return;
×
106
            }
107

108
            // Find all matching archetypes
109
            var matching = new List<Archetype>();
7✔
110
            foreach (var archetype in allArchetypes)
7✔
111
            {
112
                if (descriptor.Matches(archetype))
6✔
113
                {
114
                    matching.Add(archetype);
6✔
115
                }
116
            }
117

118
            // Merge with any archetypes added by OnArchetypeCreated during our iteration
119
            // archetypes might have entries from concurrent Add() calls
120
            var currentArchetypes = archetypes;
7✔
121
            if (currentArchetypes.Length > 0)
7✔
122
            {
123
                var combined = new HashSet<Archetype>(matching);
×
124
                foreach (var arch in currentArchetypes)
×
125
                {
126
                    combined.Add(arch);
×
127
                }
128

129
                archetypes = [.. combined];
×
130
            }
131
            else
132
            {
133
                archetypes = [.. matching];
7✔
134
            }
135

136
            isPopulated = true;
7✔
137
        }
7✔
138
    }
7✔
139
}
140

141
/// <summary>
142
/// Manages query caching and archetype matching for efficient query execution.
143
/// Caches archetype matches per query descriptor and invalidates on archetype changes.
144
/// </summary>
145
/// <remarks>
146
/// <para>
147
/// The QueryManager maintains a cache of archetype matches for each unique query.
148
/// On first execution, a query computes which archetypes match and caches the result.
149
/// Subsequent queries with the same descriptor return cached results in O(1) time.
150
/// </para>
151
/// <para>
152
/// When a new archetype is created (due to entity component changes), the cache
153
/// is invalidated for queries that could match the new archetype. This uses
154
/// an incremental invalidation strategy to minimize overhead.
155
/// </para>
156
/// <para>
157
/// This implementation is thread-safe for concurrent query execution. The cache uses
158
/// <see cref="ConcurrentDictionary{TKey, TValue}"/> for thread-safe entry management
159
/// and <see cref="ArchetypeCache"/> for lock-free archetype reads with synchronized writes.
160
/// </para>
161
/// </remarks>
162
public sealed class QueryManager
163
{
164
    private readonly ArchetypeManager archetypeManager;
165
    private readonly ConcurrentDictionary<QueryDescriptor, ArchetypeCache> cache = [];
8✔
166
    private long cacheHits;
167
    private long cacheMisses;
168
    private int cacheVersion;
169

170
    /// <summary>
171
    /// Gets the number of queries currently cached.
172
    /// </summary>
173
    public int CachedQueryCount => cache.Count;
2✔
174

175
    /// <summary>
176
    /// Gets the number of cache hits.
177
    /// </summary>
178
    public long CacheHits => Interlocked.Read(ref cacheHits);
2✔
179

180
    /// <summary>
181
    /// Gets the number of cache misses.
182
    /// </summary>
183
    public long CacheMisses => Interlocked.Read(ref cacheMisses);
2✔
184

185
    /// <summary>
186
    /// Gets the cache hit rate as a percentage.
187
    /// </summary>
188
    public double HitRate
189
    {
190
        get
191
        {
192
            var total = CacheHits + CacheMisses;
1✔
193
            if (total == 0)
1✔
194
            {
195
                return 0.0;
1✔
196
            }
197

198
            return (double)CacheHits / total * 100.0;
1✔
199
        }
200
    }
201

202
    /// <summary>
203
    /// Creates a new QueryManager for the specified archetype manager.
204
    /// </summary>
205
    /// <param name="archetypeManager">The archetype manager to query.</param>
206
    public QueryManager(ArchetypeManager archetypeManager)
8✔
207
    {
208
        this.archetypeManager = archetypeManager;
8✔
209

210
        // Subscribe to archetype creation for cache invalidation
211
        archetypeManager.ArchetypeCreated += OnArchetypeCreated;
8✔
212
    }
8✔
213

214
    /// <summary>
215
    /// Gets the archetypes matching the specified query description.
216
    /// Uses cached results when available.
217
    /// </summary>
218
    /// <param name="description">The query description.</param>
219
    /// <returns>A list of matching archetypes.</returns>
220
    public IReadOnlyList<Archetype> GetMatchingArchetypes(QueryDescription description)
221
    {
222
        var descriptor = QueryDescriptor.FromDescription(description);
7✔
223
        return GetMatchingArchetypes(descriptor);
7✔
224
    }
225

226
    /// <summary>
227
    /// Gets the archetypes matching the specified query descriptor.
228
    /// Uses cached results when available.
229
    /// </summary>
230
    /// <param name="descriptor">The query descriptor.</param>
231
    /// <returns>A list of matching archetypes.</returns>
232
    /// <remarks>
233
    /// This method is thread-safe. Concurrent calls with the same descriptor
234
    /// may both compute matching archetypes if called simultaneously before
235
    /// the cache is populated, but the result will be consistent.
236
    /// </remarks>
237
    public IReadOnlyList<Archetype> GetMatchingArchetypes(QueryDescriptor descriptor)
238
    {
239
        // Check if we have a fully populated cache entry
240
        if (cache.TryGetValue(descriptor, out var cached) && cached.IsPopulated)
7✔
241
        {
242
            Interlocked.Increment(ref cacheHits);
4✔
243
            return cached.Archetypes;
4✔
244
        }
245

246
        Interlocked.Increment(ref cacheMisses);
7✔
247

248
        // Create cache entry first so OnArchetypeCreated can add to it during iteration
249
        // This prevents a race where archetypes created during iteration are missed
250
        var archetypeCache = cache.GetOrAdd(descriptor, _ => new ArchetypeCache());
7✔
251

252
        // Populate the cache with matching archetypes
253
        // OnArchetypeCreated will add any new archetypes created during this iteration
254
        archetypeCache.PopulateIfEmpty(archetypeManager.Archetypes, descriptor);
7✔
255

256
        return archetypeCache.Archetypes;
7✔
257
    }
258

259
    /// <summary>
260
    /// Invalidates the entire cache.
261
    /// </summary>
262
    /// <remarks>
263
    /// This method is thread-safe. New queries will recompute matching archetypes
264
    /// after invalidation.
265
    /// </remarks>
266
    public void InvalidateCache()
267
    {
268
        cache.Clear();
2✔
269
        Interlocked.Increment(ref cacheVersion);
2✔
270
    }
2✔
271

272
    /// <summary>
273
    /// Invalidates a specific query from the cache.
274
    /// </summary>
275
    /// <param name="descriptor">The query descriptor to invalidate.</param>
276
    /// <remarks>
277
    /// This method is thread-safe. The next query with this descriptor will
278
    /// recompute matching archetypes.
279
    /// </remarks>
280
    public void InvalidateQuery(QueryDescriptor descriptor)
281
    {
282
        cache.TryRemove(descriptor, out _);
1✔
283
    }
1✔
284

285
    /// <summary>
286
    /// Clears all statistics.
287
    /// </summary>
288
    public void ResetStatistics()
289
    {
290
        Interlocked.Exchange(ref cacheHits, 0);
1✔
291
        Interlocked.Exchange(ref cacheMisses, 0);
1✔
292
    }
1✔
293

294
    private void OnArchetypeCreated(Archetype archetype)
295
    {
296
        // Incremental invalidation: only update queries that could match the new archetype
297
        // This is thread-safe: ConcurrentDictionary allows iteration while other threads modify it,
298
        // and ArchetypeCache.Add is thread-safe using copy-on-write semantics.
299
        foreach (var (descriptor, archetypeCache) in cache)
8✔
300
        {
301
            // If the new archetype matches this query, incrementally add it
302
            if (descriptor.Matches(archetype))
2✔
303
            {
304
                archetypeCache.Add(archetype);
2✔
305
            }
306
        }
307
    }
8✔
308
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc