namespace FluidExplorer.Services.Search.Usn; /// /// 紧凑文件名池 —— 索引“快且省内存”的关键之一。 /// /// 所有文件名以 UTF-16 连续存放在若干 定长块(每块 1<<20 个字符 = 2MB)里, /// 整个索引里不会为文件名产生任何 string 对象;查询时直接在这块内存上取 ReadOnlySpan<char>。 /// /// 偏移编码:(chunkIndex << 20) | offsetInChunk,单个 int 即可寻址 2G 字符。 /// 因为块大小固定为 1<<20,而 NTFS 单个文件名最长 255 字符, /// 所以一个名字永远不会跨越两个块 —— 取值时无需拼接。 /// /// 线程安全:块数组一次性预分配(只写指针不扩容),块本身写入后用 发布。 /// 查询线程只会读到“已经完整写入”的块;配合索引条数的发布顺序(先写数据、后写 Count),读侧无需加锁。 /// internal sealed class NamePool { internal const int ChunkShift = 20; internal const int ChunkSize = 1 << ChunkShift; internal const int ChunkMask = ChunkSize - 1; /// NameRef 只用 8 位存长度,NTFS 文件名最长 255 字符,正好用满。 internal const int MaxNameLength = 255; private const int MaxChunks = 2048; // 2048 * 1M = 2G 字符上限 private readonly char[][] _chunks = new char[MaxChunks][]; private int _chunkCount; private int _fill; // 当前块已使用字符数 private int _publishedChars; internal NamePool() { AddChunk(); } /// 池中已发布的字符总数(约等于所有名字长度之和)。 internal int TotalChars => _publishedChars; /// 写入一个名字,返回它的池内偏移。空名字返回 0(配合 NameRef 的长度字段仍然无歧义)。 internal int Add(ReadOnlySpan name) { if (name.Length == 0) return 0; if (name.Length > ChunkSize) name = name[..ChunkSize]; if (_fill + name.Length > ChunkSize) AddChunk(); int chunkIndex = _chunkCount - 1; int offset = (chunkIndex << ChunkShift) | _fill; name.CopyTo(_chunks[chunkIndex].AsSpan(_fill)); _fill += name.Length; _publishedChars = (chunkIndex << ChunkShift) + _fill; return offset; } /// 取回名字。参数非法时返回空 span(绝不抛异常,读侧可能并发读到正在更新的旧值)。 internal ReadOnlySpan Get(int offset, int length) { if (length <= 0 || offset < 0) return default; int chunkIndex = offset >> ChunkShift; int inChunk = offset & ChunkMask; if (chunkIndex >= _chunkCount) return default; var chunk = Volatile.Read(ref _chunks[chunkIndex]); if (chunk is null || inChunk + length > ChunkSize) return default; return chunk.AsSpan(inChunk, length); } private void AddChunk() { if (_chunkCount >= MaxChunks) throw new InvalidOperationException("文件名池已达到容量上限(2G 字符)。"); var chunk = new char[ChunkSize]; Volatile.Write(ref _chunks[_chunkCount], chunk); _chunkCount++; _fill = 0; } // ---------------------------------------------------------------- NameRef 打包 /// 把 (偏移, 长度) 打包进一个 long:低 8 位是长度,高 56 位是偏移。 internal static long Pack(int offset, int length) => ((long)offset << 8) | (uint)(length & 0xFF); internal static int UnpackOffset(long nameRef) => (int)(nameRef >> 8); internal static int UnpackLength(long nameRef) => (int)(nameRef & 0xFF); }