164 lines
6.7 KiB
C#
164 lines
6.7 KiB
C#
using System;
|
|
using EmbedIO.Internal;
|
|
|
|
namespace EmbedIO.Files.Internal
|
|
{
|
|
internal sealed class FileCacheItem
|
|
{
|
|
#pragma warning disable SA1401 // Field should be private - performance is a stronger concern here.
|
|
// These fields create a sort of linked list of items
|
|
// inside the cache's dictionary.
|
|
// Their purpose is to keep track of items
|
|
// in order from least to most recently used.
|
|
internal string? PreviousKey;
|
|
internal string? NextKey;
|
|
internal long LastUsedAt;
|
|
#pragma warning restore SA1401
|
|
|
|
// Size of a pointer in bytes
|
|
private static readonly long SizeOfPointer = Environment.Is64BitProcess ? 8 : 4;
|
|
|
|
// Size of a WeakReference<T> in bytes
|
|
private static readonly long SizeOfWeakReference = Environment.Is64BitProcess ? 16 : 32;
|
|
|
|
// Educated guess about the size of an Item in memory (see comments on constructor).
|
|
// 3 * SizeOfPointer + total size of fields, rounded up to a multiple of 16.
|
|
//
|
|
// Computed as follows:
|
|
//
|
|
// * for 32-bit:
|
|
// - initialize count to 3 (number of "hidden" pointers that compose the object header)
|
|
// - for every field / auto property, in order of declaration:
|
|
// - increment count by 1 for reference types, 2 for long and DateTime
|
|
// (as of time of writing there are no fields of other types here)
|
|
// - increment again by 1 if this field "weighs" 1 and the next one "weighs" 2
|
|
// (padding for field alignment)
|
|
// - multiply count by 4 (size of a pointer)
|
|
// - if the result is not a multiple of 16, round it up to next multiple of 16
|
|
//
|
|
// * for 64-bit:
|
|
// - initialize count to 3 (number of "hidden" pointers that compose the object header)
|
|
// - for every field / auto property, in order of declaration, increment count by 1
|
|
// (at the time of writing there are no fields here that need padding on 64-bit)
|
|
// - multiply count by 8 (size of a pointer)
|
|
// - if the result is not a multiple of 16, round it up to next multiple of 16
|
|
private static readonly long SizeOfItem = Environment.Is64BitProcess ? 96 : 128;
|
|
|
|
private readonly object _syncRoot = new object();
|
|
|
|
// Used to update total size of section.
|
|
// Weak reference avoids circularity.
|
|
private readonly WeakReference<FileCache.Section> _section;
|
|
|
|
// There are only 3 possible compression methods,
|
|
// hence a dictionary (or two dictionaries) would be overkill.
|
|
private byte[]? _uncompressedContent;
|
|
private byte[]? _gzippedContent;
|
|
private byte[]? _deflatedContent;
|
|
|
|
internal FileCacheItem(FileCache.Section section, DateTime lastModifiedUtc, long length)
|
|
{
|
|
_section = new WeakReference<FileCache.Section>(section);
|
|
|
|
LastModifiedUtc = lastModifiedUtc;
|
|
Length = length;
|
|
|
|
// There is no way to know the actual size of an object at runtime.
|
|
// This method makes some educated guesses, based on the following
|
|
// article (among others):
|
|
// https://codingsight.com/precise-computation-of-clr-object-size/
|
|
// PreviousKey and NextKey values aren't counted in
|
|
// because they are just references to existing strings.
|
|
SizeInCache = SizeOfItem + SizeOfWeakReference;
|
|
}
|
|
|
|
public DateTime LastModifiedUtc { get; }
|
|
|
|
public long Length { get; }
|
|
|
|
// This is the (approximate) in-memory size of this object.
|
|
// It is NOT the length of the cache resource!
|
|
public long SizeInCache { get; private set; }
|
|
|
|
public byte[]? GetContent(CompressionMethod compressionMethod)
|
|
{
|
|
// If there are both entity tag and content, use them.
|
|
switch (compressionMethod)
|
|
{
|
|
case CompressionMethod.Deflate:
|
|
if (_deflatedContent != null) return _deflatedContent;
|
|
break;
|
|
case CompressionMethod.Gzip:
|
|
if (_gzippedContent != null) return _gzippedContent;
|
|
break;
|
|
default:
|
|
if (_uncompressedContent != null) return _uncompressedContent;
|
|
break;
|
|
}
|
|
|
|
// Try to convert existing content, if any.
|
|
byte[]? content;
|
|
if (_uncompressedContent != null)
|
|
{
|
|
content = CompressionUtility.ConvertCompression(_uncompressedContent, CompressionMethod.None, compressionMethod);
|
|
}
|
|
else if (_gzippedContent != null)
|
|
{
|
|
content = CompressionUtility.ConvertCompression(_gzippedContent, CompressionMethod.Gzip, compressionMethod);
|
|
}
|
|
else if (_deflatedContent != null)
|
|
{
|
|
content = CompressionUtility.ConvertCompression(_deflatedContent, CompressionMethod.Deflate, compressionMethod);
|
|
}
|
|
else
|
|
{
|
|
// No content whatsoever.
|
|
return null;
|
|
}
|
|
|
|
return SetContent(compressionMethod, content);
|
|
}
|
|
|
|
public byte[]? SetContent(CompressionMethod compressionMethod, byte[]? content)
|
|
{
|
|
// This is the bare minimum locking we need
|
|
// to ensure we don't mess sizes up.
|
|
byte[]? oldContent;
|
|
lock (_syncRoot)
|
|
{
|
|
switch (compressionMethod)
|
|
{
|
|
case CompressionMethod.Deflate:
|
|
oldContent = _deflatedContent;
|
|
_deflatedContent = content;
|
|
break;
|
|
case CompressionMethod.Gzip:
|
|
oldContent = _gzippedContent;
|
|
_gzippedContent = content;
|
|
break;
|
|
default:
|
|
oldContent = _uncompressedContent;
|
|
_uncompressedContent = content;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var sizeDelta = GetSizeOf(content) - GetSizeOf(oldContent);
|
|
SizeInCache += sizeDelta;
|
|
if (_section.TryGetTarget(out var section))
|
|
section.UpdateTotalSize(sizeDelta);
|
|
|
|
return content;
|
|
}
|
|
|
|
// Round up to a multiple of 16
|
|
private static long RoundUpTo16(long n)
|
|
{
|
|
var remainder = n % 16;
|
|
return remainder > 0 ? n + (16 - remainder) : n;
|
|
}
|
|
|
|
// The size of a byte array is 3 * SizeOfPointer + 1 (size of byte) * Length
|
|
private static long GetSizeOf(byte[]? arr) => arr == null ? 0 : RoundUpTo16(3 * SizeOfPointer) + arr.Length;
|
|
}
|
|
} |