/* * Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Data.Caching.ObjectCache * File: CacheEntry.cs * * CacheEntry.cs is part of VNLib.Data.Caching.ObjectCache which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Data.Caching.ObjectCache is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * VNLib.Data.Caching.ObjectCache is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ using System; using System.Buffers; using System.Diagnostics; using System.Buffers.Binary; using System.Runtime.CompilerServices; using VNLib.Utils.Memory; namespace VNLib.Data.Caching { /// /// A structure that represents an item in cache. It contains the binary content /// of a cache entry by its internal memory handle /// public readonly record struct CacheEntry : IDisposable { private const int TIME_SEGMENT_SIZE = sizeof(long); private const int LENGTH_SEGMENT_SIZE = sizeof(int); private const int DATA_SEGMENT_START = TIME_SEGMENT_SIZE + LENGTH_SEGMENT_SIZE; private readonly ICacheEntryMemoryManager _manager; private readonly object _handle; /// /// Creates a new and copies the initial data to the internal buffer /// /// The initial data to store /// The heap to allocate the buffer from /// The newly initialized and ready to use /// public static CacheEntry Create(ReadOnlySpan data, ICacheEntryMemoryManager dataManager) { _ = dataManager ?? throw new ArgumentNullException(nameof(dataManager)); //Calc buffer size uint bufferSize = GetRequiredHandleSize(data.Length); object handle = dataManager.AllocHandle(bufferSize); //Create new entry from handle CacheEntry entry = new(dataManager, handle); entry.SetLength((uint)data.Length); //Get the data segment Span segment = entry.GetDataSegment(); Debug.Assert(segment.Length == data.Length); //Copy data segment data.CopyTo(segment); return entry; } /// /// Creates a new from an existing handle /// /// The cache data handle to create the entry around /// The cache entry memory manager the handle blongs to /// The re-constructed entry /// /// public static CacheEntry FromExistingHandle(object handle, ICacheEntryMemoryManager manager) { _ = handle ?? throw new ArgumentNullException(nameof(handle)); _ = manager ?? throw new ArgumentNullException(nameof(manager)); //validate handle size it at least the minimum size if (manager.GetHandleSize(handle) < DATA_SEGMENT_START) { throw new ArgumentException("Memory segment is too small to be a valid cache entry"); } return new(manager, handle); } private static uint GetRequiredHandleSize(int size) { //Caculate the minimum handle size to store all required information, rounded to nearest page return (uint)MemoryUtil.NearestPage(size + DATA_SEGMENT_START); } private CacheEntry(ICacheEntryMemoryManager manager, object handle) { _manager = manager; _handle = handle; } /// public readonly void Dispose() => _manager?.FreeHandle(_handle); private readonly Span GetTimeSegment() => _manager.GetSpan(_handle, 0, TIME_SEGMENT_SIZE); private readonly Span GetLengthSegment() => _manager.GetSpan(_handle, TIME_SEGMENT_SIZE, LENGTH_SEGMENT_SIZE); /// /// Gets the size of the block of memory held by the underlying handle /// /// The size of the block held by the current entry /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly nuint GetMemoryUsage() => _manager.GetHandleSize(_handle); /// /// Gets the last set time /// /// The last date stored /// public readonly DateTime GetTime() { //Get the time segment and write the value in big endian ReadOnlySpan segment = GetTimeSegment(); long ticks = BinaryPrimitives.ReadInt64BigEndian(segment); //ticks back to return new(ticks); } /// /// Sets the last modified time /// /// The new time to set the handle to /// public readonly void SetTime(DateTime time) { //Get native ticks value long timeData = time.Ticks; //Get the time segment and write the value in big endian Span segment = GetTimeSegment(); BinaryPrimitives.WriteInt64BigEndian(segment, timeData); } /// /// Gets the length of the data segment /// /// The length of the data segment /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly uint GetLength() { //Get the length segment ReadOnlySpan segment = GetLengthSegment(); //Recover the integer return BinaryPrimitives.ReadUInt32BigEndian(segment); } private readonly void SetLength(uint length) { //Get the length segment Span segment = GetLengthSegment(); //Update the length value BinaryPrimitives.WriteUInt32BigEndian(segment, length); } /// /// Gets the stored data segment /// /// The data segment /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Span GetDataSegment() { //Get the actual length of the segment uint length = GetLength(); //Get the segment from its begining offset and return _manager.GetSpan(_handle, DATA_SEGMENT_START, length); } /// /// Writes the specified segment to the internal buffer and resizes the buffer if necessary. /// This operation overwrites any previously stored data /// /// The data segment to store /// public readonly void UpdateData(ReadOnlySpan data) { //Calc required buffer size uint bufferSize = GetRequiredHandleSize(data.Length); //Resize buffer if necessary if(_manager.GetHandleSize(_handle) < bufferSize) { //resize handle _manager.ResizeHandle(_handle, bufferSize); } //Reset data length SetLength((uint)data.Length); //Get the data segment Span segment = GetDataSegment(); //Test segment length is equivalent to the requested data length Debug.Assert(segment.Length == data.Length); //Copy data segment data.CopyTo(segment); } /// /// Gets a offset to the start of the /// internal data segment, and avoids calling the fixed keyword. /// The handle must be disposed/released to avoid memeory leaks. /// /// /// WARNING: You must respect the return value so /// as not to overrun the valid data segment. /// /// A handle that points to the begining of the data segment [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MemoryHandle UnsafeGetDataSegmentHandle() { //Get the handle offset to the data segment start, the caller must know when the data segment ends return _manager.PinHandle(_handle, DATA_SEGMENT_START); } /// /// Gets the internal memory handle and manager its associated with /// /// The opaque memory handle /// The associated memory manager public readonly void GetInternalHandle(out object handle, out ICacheEntryMemoryManager manager) { handle = _handle; manager = _manager; } } }