/*************************************************************************
* ModernUO *
* Copyright 2019-2026 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: FileBufferWriter.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
*************************************************************************/
using System;
using System.Buffers;
using System.IO;
using Microsoft.Win32.SafeHandles;
namespace Server;
///
/// A whose staging block drains to a file when full instead of
/// growing: the full raw write path (unrolled encoded ints, in-place strings) composes into
/// memory, and the file sees large sequential positional writes. Seeks flush the staging
/// block and move the file offset, so backwards patches (e.g. the idx entity count) become
/// small positional writes. Memory-mapped writing pays soft page faults on every composed
/// page and dirty-section teardown stalls at dispose — measured ~4x slower at snapshot sizes.
/// A single item larger than the staging block grows the block via the base resize path,
/// so oversized spans and strings remain correct.
///
public class FileBufferWriter : BufferWriter, IDisposable
{
private const int MinStagingSize = 256;
private const int MaxStagingSize = 1024 * 1024; // 1MB write granularity for large files
private readonly SafeFileHandle _handle;
private readonly byte[] _rentedStaging;
private long _fileOffset; // file position where the staging block begins
private long _fileHighWater; // logical end of file across seeks
/// Destination file; created/truncated.
///
/// Expected total file size when known. Files at or under the staging cap never
/// drain until close; larger files stream through a pooled block at the cap. The
/// block comes from ArrayPool so sequential snapshot writers recycle one buffer
/// instead of dropping a large-object allocation per file per save.
///
public FileBufferWriter(string filePath, long expectedSize = MaxStagingSize)
: base(RentStaging(expectedSize), true)
{
_rentedStaging = Buffer;
_handle = File.OpenHandle(filePath, FileMode.Create, FileAccess.Write, FileShare.None, FileOptions.SequentialScan);
}
private static byte[] RentStaging(long expectedSize) =>
ArrayPool.Shared.Rent((int)Math.Clamp(expectedSize, MinStagingSize, MaxStagingSize));
public override long Position => _fileOffset + Index;
public override void Flush()
{
if (Index > 0)
{
Drain();
}
else
{
// Nothing staged and still not enough room: a single item larger than the
// staging block. Grow the block so the base write loops always make progress.
base.Flush();
}
}
private void Drain()
{
var length = (int)Index;
RandomAccess.Write(_handle, Buffer.AsSpan(0, length), _fileOffset);
_fileOffset += length;
if (_fileOffset > _fileHighWater)
{
_fileHighWater = _fileOffset;
}
Index = 0;
}
public override long Seek(long offset, SeekOrigin origin)
{
var position = Position;
if (position > _fileHighWater)
{
_fileHighWater = position;
}
var target = origin switch
{
SeekOrigin.Current => position + offset,
SeekOrigin.End => _fileHighWater + offset,
_ => offset // Begin
};
if (target < 0)
{
throw new InvalidOperationException("Seek before start of file");
}
if (Index > 0)
{
Drain();
}
_fileOffset = target;
return target;
}
public override void Close()
{
if (!_handle.IsClosed)
{
if (Index > 0)
{
Drain();
}
_handle.Dispose();
// Safe even if an oversized item grew the staging block: growth replaced the
// base buffer with a fresh array, so the rented one is no longer referenced.
ArrayPool.Shared.Return(_rentedStaging);
}
}
public void Dispose() => Close();
}