Files
fluid-explorer/Services/Search/Usn/IndexStore.cs
T

213 lines
7.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 &lt;= 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);
}