using System.Buffers.Binary; using ZB.MOM.WW.CBDD.Core.Compression; using ZB.MOM.WW.CBDD.Core.Indexing; namespace ZB.MOM.WW.CBDD.Core.Storage; /// /// Aggregated page counts grouped by page type. /// public sealed class PageTypeUsageEntry { public PageType PageType { get; init; } public int PageCount { get; init; } } /// /// Per-collection page usage summary. /// public sealed class CollectionPageUsageEntry { public string CollectionName { get; init; } = string.Empty; public int TotalDistinctPages { get; init; } public int DataPages { get; init; } public int OverflowPages { get; init; } public int IndexPages { get; init; } public int OtherPages { get; init; } } /// /// Per-collection compression ratio summary. /// public sealed class CollectionCompressionRatioEntry { public string CollectionName { get; init; } = string.Empty; public long DocumentCount { get; init; } public long CompressedDocumentCount { get; init; } public long BytesBeforeCompression { get; init; } public long BytesAfterCompression { get; init; } public double CompressionRatio => BytesAfterCompression <= 0 ? 1.0 : (double)BytesBeforeCompression / BytesAfterCompression; } /// /// Summary of free-list and reclaimable tail information. /// public sealed class FreeListSummary { public uint PageCount { get; init; } public int FreePageCount { get; init; } public long FreeBytes { get; init; } public double FragmentationPercent { get; init; } public uint TailReclaimablePages { get; init; } } /// /// Single page entry in fragmentation reporting. /// public sealed class FragmentationPageEntry { public uint PageId { get; init; } public PageType PageType { get; init; } public bool IsFreePage { get; init; } public int FreeBytes { get; init; } } /// /// Detailed fragmentation map and totals. /// public sealed class FragmentationMapReport { public IReadOnlyList Pages { get; init; } = Array.Empty(); public long TotalFreeBytes { get; init; } public double FragmentationPercent { get; init; } public uint TailReclaimablePages { get; init; } } public sealed partial class StorageEngine { /// /// Gets page usage grouped by page type. /// public IReadOnlyList GetPageUsageByPageType() { var pageCount = _pageFile.NextPageId; var buffer = new byte[_pageFile.PageSize]; var counts = new Dictionary(); for (uint pageId = 0; pageId < pageCount; pageId++) { _pageFile.ReadPage(pageId, buffer); var pageType = PageHeader.ReadFrom(buffer).PageType; counts[pageType] = counts.TryGetValue(pageType, out var count) ? count + 1 : 1; } return counts .OrderBy(x => (byte)x.Key) .Select(x => new PageTypeUsageEntry { PageType = x.Key, PageCount = x.Value }) .ToArray(); } /// /// Gets per-collection page usage by resolving primary-index locations and related index roots. /// public IReadOnlyList GetPageUsageByCollection() { var metadataEntries = GetAllCollectionMetadata(); var results = new List(metadataEntries.Count); foreach (var metadata in metadataEntries) { var pageIds = new HashSet(); if (metadata.PrimaryRootPageId != 0) pageIds.Add(metadata.PrimaryRootPageId); if (metadata.SchemaRootPageId != 0) pageIds.Add(metadata.SchemaRootPageId); foreach (var indexMetadata in metadata.Indexes) { if (indexMetadata.RootPageId != 0) pageIds.Add(indexMetadata.RootPageId); } foreach (var location in EnumeratePrimaryLocations(metadata)) { pageIds.Add(location.PageId); if (TryReadFirstOverflowPage(location, out var firstOverflowPage)) { AddOverflowChainPages(pageIds, firstOverflowPage); } } int data = 0; int overflow = 0; int indexPages = 0; int other = 0; var pageBuffer = new byte[_pageFile.PageSize]; foreach (var pageId in pageIds) { if (pageId >= _pageFile.NextPageId) continue; _pageFile.ReadPage(pageId, pageBuffer); var pageType = PageHeader.ReadFrom(pageBuffer).PageType; if (pageType == PageType.Data) { data++; } else if (pageType == PageType.Overflow) { overflow++; } else if (pageType == PageType.Index || pageType == PageType.Vector || pageType == PageType.Spatial) { indexPages++; } else { other++; } } results.Add(new CollectionPageUsageEntry { CollectionName = metadata.Name, TotalDistinctPages = pageIds.Count, DataPages = data, OverflowPages = overflow, IndexPages = indexPages, OtherPages = other }); } return results; } /// /// Gets per-collection logical-vs-stored compression ratios. /// public IReadOnlyList GetCompressionRatioByCollection() { var metadataEntries = GetAllCollectionMetadata(); var results = new List(metadataEntries.Count); foreach (var metadata in metadataEntries) { long docs = 0; long compressedDocs = 0; long bytesBefore = 0; long bytesAfter = 0; foreach (var location in EnumeratePrimaryLocations(metadata)) { if (!TryReadSlotPayloadStats(location, out var isCompressed, out var originalBytes, out var storedBytes)) continue; docs++; if (isCompressed) compressedDocs++; bytesBefore += originalBytes; bytesAfter += storedBytes; } results.Add(new CollectionCompressionRatioEntry { CollectionName = metadata.Name, DocumentCount = docs, CompressedDocumentCount = compressedDocs, BytesBeforeCompression = bytesBefore, BytesAfterCompression = bytesAfter }); } return results; } /// /// Gets free-list summary for diagnostics. /// public FreeListSummary GetFreeListSummary() { var snapshot = CaptureCompactionSnapshot(); return new FreeListSummary { PageCount = snapshot.PageCount, FreePageCount = snapshot.FreePageCount, FreeBytes = snapshot.TotalFreeBytes, FragmentationPercent = snapshot.FragmentationPercent, TailReclaimablePages = snapshot.TailReclaimablePages }; } /// /// Gets detailed page-level fragmentation diagnostics. /// public FragmentationMapReport GetFragmentationMap() { var freePageSet = new HashSet(_pageFile.EnumerateFreePages(includeEmptyPages: true)); var pageCount = _pageFile.NextPageId; var buffer = new byte[_pageFile.PageSize]; var pages = new List((int)pageCount); long totalFreeBytes = 0; for (uint pageId = 0; pageId < pageCount; pageId++) { _pageFile.ReadPage(pageId, buffer); var pageHeader = PageHeader.ReadFrom(buffer); var isFreePage = freePageSet.Contains(pageId); int freeBytes = 0; if (isFreePage) { freeBytes = _pageFile.PageSize; } else if (TryReadSlottedFreeSpace(buffer, out var slottedFreeBytes)) { freeBytes = slottedFreeBytes; } totalFreeBytes += freeBytes; pages.Add(new FragmentationPageEntry { PageId = pageId, PageType = pageHeader.PageType, IsFreePage = isFreePage, FreeBytes = freeBytes }); } uint tailReclaimablePages = 0; for (var i = pageCount; i > 2; i--) { if (!freePageSet.Contains(i - 1)) break; tailReclaimablePages++; } var fileBytes = Math.Max(1L, _pageFile.FileLengthBytes); return new FragmentationMapReport { Pages = pages, TotalFreeBytes = totalFreeBytes, FragmentationPercent = (totalFreeBytes * 100d) / fileBytes, TailReclaimablePages = tailReclaimablePages }; } private IEnumerable EnumeratePrimaryLocations(CollectionMetadata metadata) { if (metadata.PrimaryRootPageId == 0) yield break; var index = new BTreeIndex(this, IndexOptions.CreateUnique("_id"), metadata.PrimaryRootPageId); foreach (var entry in index.Range(IndexKey.MinKey, IndexKey.MaxKey, IndexDirection.Forward, transactionId: 0)) { yield return entry.Location; } } private bool TryReadFirstOverflowPage(in DocumentLocation location, out uint firstOverflowPage) { firstOverflowPage = 0; var pageBuffer = new byte[_pageFile.PageSize]; _pageFile.ReadPage(location.PageId, pageBuffer); var header = SlottedPageHeader.ReadFrom(pageBuffer); if (location.SlotIndex >= header.SlotCount) return false; var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size); var slot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0) return false; if ((slot.Flags & SlotFlags.HasOverflow) == 0) return false; if (slot.Length < 8) return false; firstOverflowPage = BinaryPrimitives.ReadUInt32LittleEndian(pageBuffer.AsSpan(slot.Offset + 4, 4)); return true; } private void AddOverflowChainPages(HashSet pageIds, uint firstOverflowPage) { if (firstOverflowPage == 0) return; var buffer = new byte[_pageFile.PageSize]; var visited = new HashSet(); var current = firstOverflowPage; while (current != 0 && current < _pageFile.NextPageId && visited.Add(current)) { pageIds.Add(current); _pageFile.ReadPage(current, buffer); var header = SlottedPageHeader.ReadFrom(buffer); if (header.PageType != PageType.Overflow) break; current = header.NextOverflowPage; } } private bool TryReadSlotPayloadStats( in DocumentLocation location, out bool isCompressed, out int originalBytes, out int storedBytes) { isCompressed = false; originalBytes = 0; storedBytes = 0; var pageBuffer = new byte[_pageFile.PageSize]; _pageFile.ReadPage(location.PageId, pageBuffer); var header = SlottedPageHeader.ReadFrom(pageBuffer); if (location.SlotIndex >= header.SlotCount) return false; var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size); var slot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0) return false; var hasOverflow = (slot.Flags & SlotFlags.HasOverflow) != 0; isCompressed = (slot.Flags & SlotFlags.Compressed) != 0; if (!hasOverflow) { storedBytes = slot.Length; if (!isCompressed) { originalBytes = slot.Length; return true; } if (slot.Length < CompressedPayloadHeader.Size) return false; var compressedHeader = CompressedPayloadHeader.ReadFrom(pageBuffer.AsSpan(slot.Offset, CompressedPayloadHeader.Size)); originalBytes = compressedHeader.OriginalLength; return true; } if (slot.Length < 8) return false; var primaryPayload = pageBuffer.AsSpan(slot.Offset, slot.Length); var totalStoredBytes = BinaryPrimitives.ReadInt32LittleEndian(primaryPayload.Slice(0, 4)); if (totalStoredBytes < 0) return false; storedBytes = totalStoredBytes; if (!isCompressed) { originalBytes = totalStoredBytes; return true; } var storedPrefix = primaryPayload.Slice(8); Span headerBuffer = stackalloc byte[CompressedPayloadHeader.Size]; if (storedPrefix.Length >= CompressedPayloadHeader.Size) { storedPrefix.Slice(0, CompressedPayloadHeader.Size).CopyTo(headerBuffer); } else { storedPrefix.CopyTo(headerBuffer); var copied = storedPrefix.Length; var nextOverflow = BinaryPrimitives.ReadUInt32LittleEndian(primaryPayload.Slice(4, 4)); var overflowBuffer = new byte[_pageFile.PageSize]; while (copied < CompressedPayloadHeader.Size && nextOverflow != 0 && nextOverflow < _pageFile.NextPageId) { _pageFile.ReadPage(nextOverflow, overflowBuffer); var overflowHeader = SlottedPageHeader.ReadFrom(overflowBuffer); if (overflowHeader.PageType != PageType.Overflow) return false; var available = Math.Min(CompressedPayloadHeader.Size - copied, _pageFile.PageSize - SlottedPageHeader.Size); overflowBuffer.AsSpan(SlottedPageHeader.Size, available).CopyTo(headerBuffer.Slice(copied)); copied += available; nextOverflow = overflowHeader.NextOverflowPage; } if (copied < CompressedPayloadHeader.Size) return false; } var headerFromPayload = CompressedPayloadHeader.ReadFrom(headerBuffer); originalBytes = headerFromPayload.OriginalLength; return true; } }