213 lines
7.9 KiB
C#
213 lines
7.9 KiB
C#
using System.Collections.Concurrent;
|
||
using System.Runtime.CompilerServices;
|
||
|
||
namespace FluidExplorer.Services.Search.Usn;
|
||
|
||
/// <summary>
|
||
/// 索引的紧凑存储:结构体数组(Struct-of-Arrays)而不是对象数组。
|
||
///
|
||
/// 每条记录只占 45 字节(8+8+8+8+8+4+1),100 万条约 45MB,
|
||
/// 再加上名字池(约 2 字节/字符)就构成整个索引;绝无 per-entry 的对象头与 string。
|
||
///
|
||
/// 并发模型(读多写极少):
|
||
/// * <see cref="Count"/> 用 volatile 发布:写入方先写满数据,最后自增 Count;读方只需读一次 Count 再顺序访问。
|
||
/// * 结构只增不减(删除只打墓碑标志位),因此已经发布的 [0, Count) 区间永远有效。
|
||
/// * 容量不足时 <see cref="Grow"/> 采用 copy-on-write,整体替换数组;老快照对正在查询的线程依然合法。
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <summary>
|
||
/// 名字池与数组快照绑定在一起:查询只需抓取一次 store 引用就能得到一致的 (名字池, 数组, 条数)。
|
||
/// 关键:扩容(<see cref="Grow"/>)必须<b>复用同一个名字池</b> —— NameRef 里存的是池内偏移,
|
||
/// 换池等于让所有老记录的名字全部失效。
|
||
/// </summary>
|
||
internal readonly NamePool Names;
|
||
|
||
internal readonly int Capacity;
|
||
|
||
/// <summary>已发布条数。写入方必须在写完所有字段之后再自增它。</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>扩容(copy-on-write)。返回新快照,旧快照仍然可被并发查询安全使用。</summary>
|
||
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;
|
||
|
||
/// <summary>读名字(零分配)。</summary>
|
||
internal ReadOnlySpan<char> GetName(int i)
|
||
{
|
||
long nameRef = Volatile.Read(ref NameRef[i]);
|
||
return Names.Get(NamePool.UnpackOffset(nameRef), NamePool.UnpackLength(nameRef));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// FRN(低 48 位记录号)→ 索引下标的映射。
|
||
///
|
||
/// 策略:构建期用普通 Dictionary 最快;构建结束后调用 <see cref="Optimize"/>,
|
||
/// 如果记录号足够密集(maxRecord <= 8 * count),就换成“记录号直接寻址”的 int[],
|
||
/// 100 万文件只占几 MB(而 Dictionary 要 30~40MB)。
|
||
/// 稀疏卷或监听期新增的越界记录号退回到 <see cref="ConcurrentDictionary{TKey,TValue}"/> 侧表。
|
||
/// </summary>
|
||
internal sealed class FrnMap
|
||
{
|
||
private const int NoIndex = 0;
|
||
private const int DenseSlack = 8;
|
||
|
||
private Dictionary<ulong, int>? _buildMap;
|
||
private int[]? _dense; // 记录号 → 下标+1;0 表示不存在
|
||
private ConcurrentDictionary<ulong, int>? _sparse;
|
||
|
||
internal FrnMap(int estimatedCount)
|
||
{
|
||
_buildMap = new Dictionary<ulong, int>(Math.Max(16, estimatedCount));
|
||
}
|
||
|
||
internal int Count => _buildMap?.Count ?? _sparse?.Count ?? _dense?.Length ?? 0;
|
||
|
||
/// <summary>构建期写入(单线程,无锁,最快)。</summary>
|
||
internal void AddBuild(ulong frn, int index)
|
||
{
|
||
_buildMap![UsnNative.NormalizeFrn(frn)] = index;
|
||
}
|
||
|
||
/// <summary>构建完成后调用:把构建期的 Dictionary 压缩成密集数组或稀疏侧表。</summary>
|
||
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<ulong, int>();
|
||
foreach (var (key, value) in map)
|
||
{
|
||
sparse[key] = value;
|
||
}
|
||
Volatile.Write(ref _sparse, sparse);
|
||
}
|
||
|
||
/// <summary>监听期写入(可能并发于查询,必须线程安全)。</summary>
|
||
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<ulong, int>();
|
||
sparse = Interlocked.CompareExchange(ref _sparse, created, null) ?? created;
|
||
}
|
||
sparse[record] = index;
|
||
}
|
||
|
||
/// <summary>批量覆盖(重建索引时用,单线程)。</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>NormizeFrn 的别名,便于调用点表达意图。</summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
internal static ulong Normalize(ulong frn) => UsnNative.NormalizeFrn(frn);
|
||
}
|