TasStateManager uses a new data structure to write all states to one auto-deleting file per TasStateManager instance. TasStateManager is now IDisposable; this needs to be followed rigorously (I didn't do that).
This commit is contained in:
parent
078bd102e8
commit
5bf21e391c
|
@ -10,19 +10,24 @@ using BizHawk.Emulation.Common.IEmulatorExtensions;
|
|||
|
||||
namespace BizHawk.Client.Common
|
||||
{
|
||||
private class tsmState
|
||||
class tsmState : IDisposable
|
||||
{
|
||||
static int state_id = 0;
|
||||
TasStateManager _manager;
|
||||
|
||||
byte[] _state;
|
||||
int frame;
|
||||
int ID;
|
||||
|
||||
public tsmState(byte[] state)
|
||||
public tsmState(TasStateManager manager, byte[] state)
|
||||
{
|
||||
_manager = manager;
|
||||
_state = state;
|
||||
ID = state_id;
|
||||
state_id++;
|
||||
|
||||
//I still think this is a bad idea. IDs may need scavenging somehow
|
||||
if (state_id > int.MaxValue - 100)
|
||||
throw new InvalidOperationException();
|
||||
ID = System.Threading.Interlocked.Increment(ref state_id);
|
||||
}
|
||||
|
||||
public byte[] State
|
||||
|
@ -32,8 +37,7 @@ namespace BizHawk.Client.Common
|
|||
if (_state != null)
|
||||
return _state;
|
||||
|
||||
string path = Path.Combine(TasStateManager.statePath, ID.ToString());
|
||||
return File.ReadAllBytes(path);
|
||||
return _manager.ndbdatabase.FetchAll(ID.ToString());
|
||||
}
|
||||
set
|
||||
{
|
||||
|
@ -43,8 +47,8 @@ namespace BizHawk.Client.Common
|
|||
return;
|
||||
}
|
||||
|
||||
string path = Path.Combine(TasStateManager.statePath, ID.ToString());
|
||||
File.WriteAllBytes(path, value);
|
||||
_state = value;
|
||||
MoveToDisk();
|
||||
}
|
||||
}
|
||||
public int Length { get { return State.Length; } }
|
||||
|
@ -55,8 +59,7 @@ namespace BizHawk.Client.Common
|
|||
if (IsOnDisk)
|
||||
return;
|
||||
|
||||
string path = Path.Combine(TasStateManager.statePath, ID.ToString());
|
||||
File.WriteAllBytes(path, _state);
|
||||
_manager.ndbdatabase.Store(ID.ToString(), _state, 0, _state.Length);
|
||||
_state = null;
|
||||
}
|
||||
public void MoveToRAM()
|
||||
|
@ -64,17 +67,16 @@ namespace BizHawk.Client.Common
|
|||
if (!IsOnDisk)
|
||||
return;
|
||||
|
||||
string path = Path.Combine(TasStateManager.statePath, ID.ToString());
|
||||
_state = File.ReadAllBytes(path);
|
||||
File.Delete(path);
|
||||
var key = ID.ToString();
|
||||
var ret = _manager.ndbdatabase.FetchAll(key);
|
||||
_manager.ndbdatabase.Release(key);
|
||||
}
|
||||
public void DeleteFile()
|
||||
public void Dispose()
|
||||
{
|
||||
if (!IsOnDisk)
|
||||
return;
|
||||
|
||||
string path = Path.Combine(TasStateManager.statePath, ID.ToString());
|
||||
File.Delete(path);
|
||||
_manager.ndbdatabase.Release(ID.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +84,7 @@ namespace BizHawk.Client.Common
|
|||
/// Captures savestates and manages the logic of adding, retrieving,
|
||||
/// invalidating/clearing of states. Also does memory management and limiting of states
|
||||
/// </summary>
|
||||
public class TasStateManager
|
||||
public class TasStateManager : IDisposable
|
||||
{
|
||||
// TODO: pass this in, and find a solution to a stale reference (this is instantiated BEFORE a new core instance is made, making this one stale if it is simply set in the constructor
|
||||
private IStatable Core
|
||||
|
@ -103,7 +105,8 @@ namespace BizHawk.Client.Common
|
|||
}
|
||||
}
|
||||
|
||||
static private Guid guid = Guid.NewGuid();
|
||||
internal NDBDatabase ndbdatabase;
|
||||
private Guid guid = Guid.NewGuid();
|
||||
private SortedList<int, tsmState> States = new SortedList<int, tsmState>();
|
||||
private SortedList<int, SortedList<int, tsmState>> BranchStates = new SortedList<int, SortedList<int, tsmState>>();
|
||||
private int branches = 0;
|
||||
|
@ -140,7 +143,7 @@ namespace BizHawk.Client.Common
|
|||
return -2;
|
||||
}
|
||||
|
||||
public static string statePath
|
||||
public string statePath
|
||||
{
|
||||
get
|
||||
{
|
||||
|
@ -187,6 +190,14 @@ namespace BizHawk.Client.Common
|
|||
accessed = new List<int>();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (ndbdatabase != null)
|
||||
ndbdatabase.Dispose();
|
||||
|
||||
//States and BranchStates don't need cleaning because they would only contain an ndbdatabase entry which was demolished by the above
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mounts this instance for write access. Prior to that it's read-only
|
||||
/// </summary>
|
||||
|
@ -197,12 +208,6 @@ namespace BizHawk.Client.Common
|
|||
|
||||
_isMountedForWrite = true;
|
||||
|
||||
if (Directory.Exists(statePath))
|
||||
{
|
||||
Directory.Delete(statePath, true); // To delete old files that may still exist.
|
||||
}
|
||||
Directory.CreateDirectory(statePath);
|
||||
|
||||
int limit = 0;
|
||||
|
||||
_expectedStateSize = (ulong)Core.SaveStateBinary().Length;
|
||||
|
@ -213,6 +218,10 @@ namespace BizHawk.Client.Common
|
|||
}
|
||||
|
||||
States = new SortedList<int, tsmState>(limit);
|
||||
|
||||
if(_expectedStateSize > int.MaxValue)
|
||||
throw new InvalidOperationException();
|
||||
ndbdatabase = new NDBDatabase(statePath, Settings.DiskCapacitymb * 1024 * 1024, (int)_expectedStateSize);
|
||||
}
|
||||
|
||||
public TasStateManagerSettings Settings { get; set; }
|
||||
|
@ -371,14 +380,12 @@ namespace BizHawk.Client.Common
|
|||
|
||||
private void MoveStateToDisk(int index)
|
||||
{
|
||||
DiskUsed += _expectedStateSize;
|
||||
Used -= (ulong)States[index].Length;
|
||||
States[index].MoveToDisk();
|
||||
}
|
||||
private void MoveStateToMemory(int index)
|
||||
{
|
||||
States[index].MoveToRAM();
|
||||
DiskUsed -= _expectedStateSize;
|
||||
Used += (ulong)States[index].Length;
|
||||
}
|
||||
|
||||
|
@ -394,7 +401,7 @@ namespace BizHawk.Client.Common
|
|||
else
|
||||
{
|
||||
Used += (ulong)state.Length;
|
||||
States.Add(frame, new tsmState(state));
|
||||
States.Add(frame, new tsmState(this, state));
|
||||
}
|
||||
|
||||
StateAccessed(frame);
|
||||
|
@ -403,8 +410,7 @@ namespace BizHawk.Client.Common
|
|||
{
|
||||
if (States[frame].IsOnDisk)
|
||||
{
|
||||
DiskUsed -= _expectedStateSize;
|
||||
States[frame].DeleteFile();
|
||||
States[frame].Dispose();
|
||||
}
|
||||
else
|
||||
Used -= (ulong)States[frame].Length;
|
||||
|
@ -465,8 +471,7 @@ namespace BizHawk.Client.Common
|
|||
{
|
||||
if (state.Value.IsOnDisk)
|
||||
{
|
||||
DiskUsed -= _expectedStateSize;
|
||||
state.Value.DeleteFile();
|
||||
state.Value.Dispose();
|
||||
}
|
||||
else
|
||||
Used -= (ulong)state.Value.Length;
|
||||
|
@ -490,8 +495,8 @@ namespace BizHawk.Client.Common
|
|||
States.Clear();
|
||||
accessed.Clear();
|
||||
Used = 0;
|
||||
DiskUsed = 0;
|
||||
clearDiskStates();
|
||||
ndbdatabase.Clear();
|
||||
}
|
||||
public void ClearStateHistory()
|
||||
{
|
||||
|
@ -513,17 +518,13 @@ namespace BizHawk.Client.Common
|
|||
else
|
||||
Used = 0;
|
||||
|
||||
DiskUsed = 0;
|
||||
clearDiskStates();
|
||||
}
|
||||
}
|
||||
private void clearDiskStates()
|
||||
{
|
||||
if (Directory.Exists(statePath))
|
||||
{
|
||||
Directory.Delete(statePath, true);
|
||||
Directory.CreateDirectory(statePath);
|
||||
}
|
||||
if (ndbdatabase != null)
|
||||
ndbdatabase.Clear();
|
||||
}
|
||||
|
||||
// TODO: save/load BranchStates
|
||||
|
@ -617,8 +618,11 @@ namespace BizHawk.Client.Common
|
|||
}
|
||||
private ulong DiskUsed
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get
|
||||
{
|
||||
if (ndbdatabase == null) return 0;
|
||||
else return (ulong)ndbdatabase.Consumed;
|
||||
}
|
||||
}
|
||||
|
||||
public int StateCount
|
||||
|
@ -695,7 +699,7 @@ namespace BizHawk.Client.Common
|
|||
if (stateHasDuplicate(kvp.Key, index) == -2)
|
||||
{
|
||||
if (stateList[index].IsOnDisk)
|
||||
DiskUsed -= _expectedStateSize;
|
||||
{ }
|
||||
else
|
||||
Used -= (ulong)stateList[index].Length;
|
||||
}
|
||||
|
@ -719,7 +723,7 @@ namespace BizHawk.Client.Common
|
|||
if (stateHasDuplicate(kvp.Key, index) == -2)
|
||||
{
|
||||
if (stateList[index].IsOnDisk)
|
||||
DiskUsed -= _expectedStateSize;
|
||||
{ }
|
||||
else
|
||||
Used -= (ulong)stateList[index].Length;
|
||||
}
|
||||
|
|
|
@ -524,8 +524,8 @@ namespace BizHawk.Client.EmuHawk
|
|||
Global.Config.MovieEndAction = _originalEndAction;
|
||||
GlobalWin.MainForm.SetMainformMovieInfo();
|
||||
// Do not keep TAStudio's disk save states.
|
||||
if (Directory.Exists(statesPath))
|
||||
Directory.Delete(statesPath, true);
|
||||
//if (Directory.Exists(statesPath)) Directory.Delete(statesPath, true);
|
||||
//TODO - do we need to dispose something here instead?
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
<Compile Include="IPC\SharedMemoryBlock.cs" />
|
||||
<Compile Include="Log.cs" />
|
||||
<Compile Include="MruStack.cs" />
|
||||
<Compile Include="NDBDatabase.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="QuickCollections.cs" />
|
||||
<Compile Include="Serializer.cs" />
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace BizHawk.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Non-consecutive Disk Block Database
|
||||
/// Opens a file and stores blocks in it.
|
||||
/// Blocks can be differently sized than the basic block size. Wastage will occur.
|
||||
/// TODO: Mount on memory as well?
|
||||
/// </summary>
|
||||
class NDBDatabase : IDisposable
|
||||
{
|
||||
FileStream Stream;
|
||||
int BlockSize;
|
||||
Dictionary<string, Item> Items = new Dictionary<string, Item>();
|
||||
LinkedList<Block> FreeList = new LinkedList<Block>();
|
||||
readonly long BlockCount;
|
||||
long FreeWatermark;
|
||||
|
||||
class Block
|
||||
{
|
||||
public long Number;
|
||||
}
|
||||
|
||||
class Item
|
||||
{
|
||||
public LinkedList<Block> Blocks = new LinkedList<Block>();
|
||||
public long Size;
|
||||
}
|
||||
|
||||
Block AllocBlock()
|
||||
{
|
||||
if (FreeList.Count != 0)
|
||||
{
|
||||
var blocknode = FreeList.First;
|
||||
FreeList.RemoveFirst();
|
||||
Consumed += BlockSize;
|
||||
return blocknode.Value;
|
||||
}
|
||||
|
||||
if (FreeWatermark == BlockCount)
|
||||
throw new OutOfMemoryException("NDBDatabase out of reserved space");
|
||||
|
||||
var b = new Block() { Number = FreeWatermark };
|
||||
FreeWatermark++;
|
||||
Consumed += BlockSize;
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
long GetOffsetForBlock(Block b)
|
||||
{
|
||||
return b.Number * BlockSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance around a DeleteOnClose file of the provided path
|
||||
/// </summary>
|
||||
public NDBDatabase(string path, long size, int blocksize)
|
||||
{
|
||||
Capacity = size;
|
||||
Consumed = 0;
|
||||
BlockSize = blocksize;
|
||||
BlockCount = size / BlockSize;
|
||||
Stream = new FileStream(path, FileMode.Create, System.Security.AccessControl.FileSystemRights.FullControl, FileShare.None, 4 * 1024, FileOptions.DeleteOnClose);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the state of the datastructure to its original condition
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Consumed = 0;
|
||||
FreeList.Clear();
|
||||
FreeWatermark = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stream.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Total reserved storage capacity. You may nto be able to fit that much data in here though (due to blockiness)
|
||||
/// </summary>
|
||||
public readonly long Capacity;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of bytes of storage consumed. Not necessarily equal to the total amount of data stored (due to blockiness)
|
||||
/// </summary>
|
||||
public long Consumed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of bytes of storage available. Store operations <= Remain will always succeed
|
||||
/// </summary>
|
||||
public long Remain { get { return Capacity - Consumed; } }
|
||||
|
||||
/// <summary>
|
||||
/// Stores an item with the given key
|
||||
/// </summary>
|
||||
public void Store(string name, byte[] buf, int offset, int length)
|
||||
{
|
||||
if (Items.ContainsKey(name))
|
||||
throw new InvalidOperationException(string.Format("Can't add already existing key of name {0}", name));
|
||||
|
||||
if (length > Remain)
|
||||
throw new OutOfMemoryException(string.Format("Insufficient storage reserved for {0} bytes", length));
|
||||
|
||||
long todo = length;
|
||||
int src = offset;
|
||||
Item item = new Item { Size = length };
|
||||
Items[name] = item;
|
||||
while (todo > 0)
|
||||
{
|
||||
var b = AllocBlock();
|
||||
item.Blocks.AddLast(b);
|
||||
|
||||
long tocopy = todo;
|
||||
if (tocopy > BlockSize)
|
||||
tocopy = BlockSize;
|
||||
|
||||
Stream.Position = GetOffsetForBlock(b);
|
||||
Stream.Write(buf, src, (int)tocopy);
|
||||
|
||||
todo -= tocopy;
|
||||
src += (int)tocopy;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches an item with the given key
|
||||
/// </summary>
|
||||
public byte[] FetchAll(string name)
|
||||
{
|
||||
var buf = new byte[GetSize(name)];
|
||||
Fetch(name, buf, 0);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches an item with the given key
|
||||
/// </summary>
|
||||
public void Fetch(string name, byte[] buf, int offset)
|
||||
{
|
||||
Item item;
|
||||
if (!Items.TryGetValue(name, out item))
|
||||
throw new KeyNotFoundException();
|
||||
|
||||
long todo = item.Size;
|
||||
var curr = item.Blocks.First;
|
||||
while (todo > 0)
|
||||
{
|
||||
long tocopy = todo;
|
||||
if (tocopy > BlockSize)
|
||||
tocopy = BlockSize;
|
||||
Stream.Position = GetOffsetForBlock(curr.Value);
|
||||
Stream.Read(buf, offset, (int)tocopy);
|
||||
|
||||
todo -= tocopy;
|
||||
offset += (int)tocopy;
|
||||
|
||||
curr = curr.Next;
|
||||
}
|
||||
System.Diagnostics.Debug.Assert(curr == null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the item with the given key.
|
||||
/// Removing a non-existent item is benign, I guess
|
||||
/// </summary>
|
||||
public void Release(string name)
|
||||
{
|
||||
Item item;
|
||||
if (!Items.TryGetValue(name, out item))
|
||||
return;
|
||||
Items.Remove(name);
|
||||
var blocks = item.Blocks.ToArray();
|
||||
item.Blocks.Clear();
|
||||
foreach (var block in blocks)
|
||||
FreeList.AddLast(block);
|
||||
Consumed -= blocks.Length * BlockSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size of the item with the given key
|
||||
/// </summary>
|
||||
public long GetSize(string name)
|
||||
{
|
||||
return Items[name].Size;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue