using System.Collections.Concurrent; using System.Runtime.CompilerServices; namespace FluidExplorer.Services.Search.Usn; /// /// 索引的紧凑存储:结构体数组(Struct-of-Arrays)而不是对象数组。 /// /// 每条记录只占 45 字节(8+8+8+8+8+4+1),100 万条约 45MB, /// 再加上名字池(约 2 字节/字符)就构成整个索引;绝无 per-entry 的对象头与 string。 /// /// 并发模型(读多写极少): /// * 用 volatile 发布:写入方先写满数据,最后自增 Count;读方只需读一次 Count 再顺序访问。 /// * 结构只增不减(删除只打墓碑标志位),因此已经发布的 [0, Count) 区间永远有效。 /// * 容量不足时 采用 copy-on-write,整体替换数组;老快照对正在查询的线程依然合法。 /// internal sealed class IndexStore { internal const byte FlagDeleted = 0x01; internal const byte FlagDirectory = 0x02; internal const byte FlagSizeKnown = 0x04; internal readonly ulong[] Frn; // 归一化 FRN(低 48 位记录号) internal readonly ulong[] ParentFrn; // 归一化父目录 FRN internal readonly long[] NameRef; // NamePool.Pack(偏移, 长度) internal readonly long[] Size; // -1 = 未知(USN 降级模式) internal readonly long[] ModifiedTicks; // UTC ticks;0 = 未知 internal readonly uint[] Attributes; // FILE_ATTRIBUTE_* internal readonly byte[] Flags; /// /// 名字池与数组快照绑定在一起:查询只需抓取一次 store 引用就能得到一致的 (名字池, 数组, 条数)。 /// 关键:扩容()必须复用同一个名字池 —— NameRef 里存的是池内偏移, /// 换池等于让所有老记录的名字全部失效。 /// internal readonly NamePool Names; internal readonly int Capacity; /// 已发布条数。写入方必须在写完所有字段之后再自增它。 internal volatile int Count; internal IndexStore(int capacity) : this(capacity, new NamePool()) { } private IndexStore(int capacity, NamePool names) { if (capacity < 16) capacity = 16; Capacity = capacity; Names = names; Frn = new ulong[capacity]; ParentFrn = new ulong[capacity]; NameRef = new long[capacity]; Size = new long[capacity]; ModifiedTicks = new long[capacity]; Attributes = new uint[capacity]; Flags = new byte[capacity]; } private IndexStore(IndexStore old, int capacity) : this(capacity, old.Names) { int n = old.Count; Array.Copy(old.Frn, Frn, n); Array.Copy(old.ParentFrn, ParentFrn, n); Array.Copy(old.NameRef, NameRef, n); Array.Copy(old.Size, Size, n); Array.Copy(old.ModifiedTicks, ModifiedTicks, n); Array.Copy(old.Attributes, Attributes, n); Array.Copy(old.Flags, Flags, n); Count = n; } /// 扩容(copy-on-write)。返回新快照,旧快照仍然可被并发查询安全使用。 internal static IndexStore Grow(IndexStore old, int minCapacity) { int capacity = old.Capacity; while (capacity < minCapacity) capacity = capacity < 1024 ? capacity * 2 : capacity + (capacity >> 1); return new IndexStore(old, capacity); } internal bool IsDeleted(int i) => (Flags[i] & FlagDeleted) != 0; internal bool IsDirectory(int i) => (Flags[i] & FlagDirectory) != 0; /// 读名字(零分配)。 internal ReadOnlySpan GetName(int i) { long nameRef = Volatile.Read(ref NameRef[i]); return Names.Get(NamePool.UnpackOffset(nameRef), NamePool.UnpackLength(nameRef)); } } /// /// FRN(低 48 位记录号)→ 索引下标的映射。 /// /// 策略:构建期用普通 Dictionary 最快;构建结束后调用 , /// 如果记录号足够密集(maxRecord <= 8 * count),就换成“记录号直接寻址”的 int[], /// 100 万文件只占几 MB(而 Dictionary 要 30~40MB)。 /// 稀疏卷或监听期新增的越界记录号退回到 侧表。 /// internal sealed class FrnMap { private const int NoIndex = 0; private const int DenseSlack = 8; private Dictionary? _buildMap; private int[]? _dense; // 记录号 → 下标+1;0 表示不存在 private ConcurrentDictionary? _sparse; internal FrnMap(int estimatedCount) { _buildMap = new Dictionary(Math.Max(16, estimatedCount)); } internal int Count => _buildMap?.Count ?? _sparse?.Count ?? _dense?.Length ?? 0; /// 构建期写入(单线程,无锁,最快)。 internal void AddBuild(ulong frn, int index) { _buildMap![UsnNative.NormalizeFrn(frn)] = index; } /// 构建完成后调用:把构建期的 Dictionary 压缩成密集数组或稀疏侧表。 internal void Optimize() { var map = _buildMap ?? throw new InvalidOperationException("FrnMap 已经压缩过。"); _buildMap = null; ulong maxRecord = 0; foreach (var key in map.Keys) { if (key > maxRecord) maxRecord = key; } // 密集数组代价 = (maxRecord+1)*4 字节;只要不超过 8 个记录号/条目的开销就值得。 if (map.Count > 0 && maxRecord <= (ulong)map.Count * DenseSlack) { var dense = new int[maxRecord + 1]; foreach (var (key, value) in map) { dense[key] = value + 1; } Volatile.Write(ref _dense, dense); return; } var sparse = new ConcurrentDictionary(); foreach (var (key, value) in map) { sparse[key] = value; } Volatile.Write(ref _sparse, sparse); } /// 监听期写入(可能并发于查询,必须线程安全)。 internal void Set(ulong frn, int index) { ulong record = UsnNative.NormalizeFrn(frn); var dense = Volatile.Read(ref _dense); if (dense is not null && record < (ulong)dense.Length) { Volatile.Write(ref dense[record], index + 1); return; } var sparse = _sparse; if (sparse is null) { var created = new ConcurrentDictionary(); sparse = Interlocked.CompareExchange(ref _sparse, created, null) ?? created; } sparse[record] = index; } /// 批量覆盖(重建索引时用,单线程)。 internal void SetUnsafe(ulong frn, int index) { ulong record = UsnNative.NormalizeFrn(frn); var dense = _dense; if (dense is not null && record < (ulong)dense.Length) { dense[record] = index + 1; return; } _sparse![record] = index; } internal bool TryGet(ulong frn, out int index) { ulong record = UsnNative.NormalizeFrn(frn); var dense = Volatile.Read(ref _dense); if (dense is not null && record < (ulong)dense.Length) { int slot = Volatile.Read(ref dense[record]); if (slot == NoIndex) { index = -1; return false; } index = slot - 1; return true; } var sparse = Volatile.Read(ref _sparse); if (sparse is not null && sparse.TryGetValue(record, out index)) return true; index = -1; return false; } /// NormizeFrn 的别名,便于调用点表达意图。 [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static ulong Normalize(ulong frn) => UsnNative.NormalizeFrn(frn); }