State manager decay (#1128)

* Finally use exponential decay algorithm for greenzone
  * Works in both directions (unlike fceux taseditor)
* Stop using last accesses states for anything
* When dropping states per project save, account for state gap better
* Use _states.Keys[i] instead of _states.ElementAt{i).Key in speedy parts, because it's apparently tons faster
* Use StateIsMarker() where it's supposed to be used
* Markers are still left untouched
* Branch states are still dead (probably forever)
This commit is contained in:
feos 2018-03-08 12:24:35 +03:00 committed by GitHub
parent 9804a0901a
commit bf8c21663d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 211 additions and 119 deletions

View File

@ -184,6 +184,7 @@
</Compile>
<Compile Include="movie\bk2\StringLogs.cs" />
<Compile Include="movie\import\PXMImport.cs" />
<Compile Include="movie\tasproj\StateManagerDecay.cs" />
<Compile Include="movie\tasproj\StateManagerState.cs" />
<Compile Include="movie\tasproj\TasBranch.cs" />
<Compile Include="movie\tasproj\TasMovie.History.cs" />
@ -330,4 +331,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>

View File

@ -0,0 +1,167 @@
/****************************************************************************************
Algorithm by r57shell & feos, 2018
_zeros is the key to GREENZONE DECAY PATTERN.
In a 16 element example, we evaluate these bitwise numbers to count zeros on the right.
First element is always assumed to be 16, which has all 4 bits set to 0. Each right zero
means that we lower the priority of a state that goes at that index. Priority changes
depending on current frame and amount of states. States with biggest priority get erased
first. With a 4-bit battern and no initial gap between states, total frame coverage is
about 5 times state count. Initial state gap can screw up our patterns, so do all
calculations like gap isn't there, and take it back into account afterwards.
_zeros values are essentialy the values of rshiftby here:
bitwise view frame rshiftby priority
00010000 0 4 1
00000001 1 0 15
00000010 2 1 7
00000011 3 0 13
00000100 4 2 3
00000101 5 0 11
00000110 6 1 5
00000111 7 0 9
00001000 8 3 1
00001001 9 0 7
00001010 10 1 3
00001011 11 0 5
00001100 12 2 1
00001101 13 0 3
00001110 14 1 1
00001111 15 0 1
*****************************************************************************************/
using System.Collections.Generic;
namespace BizHawk.Client.Common
{
internal class StateManagerDecay
{
private TasStateManager _tsm; // access tsm methods to make life easier
private List<int> _zeros; // amount of least significant zeros in bitwise view (also max pattern step)
private int _bits; // size of _zeros is 2 raised to the power of _bits
private int _mask; // for remainder calculation using bitwise instead of division
private int _base; // repeat count (like fceux's capacity). only used by aligned formula
private int _capacity; // total amount of savestates
private int _step; // initial memory state gap
private bool _align; // extra care about fine alignment. TODO: do we want it?
public StateManagerDecay(TasStateManager tsm)
{
_tsm = tsm;
_align = false;
}
public void Trigger(int decayStates)
{
for (; decayStates > 0 && _tsm.StateCount > 1;)
{
int baseStateIndex = _tsm.GetStateIndexByFrame(Global.Emulator.Frame);
int baseStateFrame = _tsm.GetStateFrameByIndex(baseStateIndex) / _step;
int forwardPriority = -1000000;
int backwardPriority = -1000000;
int forwardFrame = -1;
int backwardFrame = -1;
for (int currentStateIndex = 1; currentStateIndex < baseStateIndex; currentStateIndex++)
{
int currentFrame = _tsm.GetStateFrameByIndex(currentStateIndex) / _step;
if (_tsm.StateIsMarker(currentFrame * _step))
{
continue;
}
int zeroCount = _zeros[currentFrame & _mask];
int priority = ((baseStateFrame - currentFrame) >> zeroCount);
if (_align)
{
priority -= ((_base * ((1 << zeroCount) * 2 - 1)) >> zeroCount);
}
if (priority > forwardPriority)
{
forwardPriority = priority;
forwardFrame = currentFrame;
}
}
for (int currentStateIndex = _tsm.StateCount - 1; currentStateIndex > baseStateIndex; currentStateIndex--)
{
int currentFrame = _tsm.GetStateFrameByIndex(currentStateIndex) / _step;
if (_tsm.StateIsMarker(currentFrame * _step))
{
continue;
}
int zeroCount = _zeros[currentFrame & _mask];
int priority = ((currentFrame - baseStateFrame) >> zeroCount);
if (_align)
{
priority -= ((_base * ((1 << zeroCount) * 2 - 1)) >> zeroCount);
}
if (priority > backwardPriority)
{
backwardPriority = priority;
backwardFrame = currentFrame;
}
}
if (forwardFrame > -1 && backwardFrame > -1)
{
if (baseStateFrame - forwardFrame > backwardFrame - baseStateFrame)
{
_tsm.RemoveState(forwardFrame * _step);
}
else
{
_tsm.RemoveState(backwardFrame * _step);
}
decayStates--;
}
else if (forwardFrame > -1)
{
_tsm.RemoveState(forwardFrame * _step);
decayStates--;
}
else if (backwardFrame > -1)
{
_tsm.RemoveState(backwardFrame * _step);
decayStates--;
}
}
}
public void UpdateSettings(int capacity, int step, int bits)
{
_capacity = capacity;
_step = step;
_bits = bits;
_mask = (1 << _bits) - 1;
_base = (_capacity + _bits / 2) / (_bits + 1);
_zeros = new List<int>();
_zeros.Add(_bits);
for (int i = 1; i < (1 << _bits); i++)
{
_zeros.Add(0);
for (int j = i; j > 0; j >>= 1)
{
if ((j & 1) > 0)
{
break;
}
_zeros[i]++;
}
}
}
}
}

View File

@ -26,7 +26,7 @@ namespace BizHawk.Client.Common
{
InvalidateCallback?.Invoke(index);
}
internal NDBDatabase NdbDatabase { get; set; }
private Guid _guid = Guid.NewGuid();
private SortedList<int, StateManagerState> _states = new SortedList<int, StateManagerState>();
@ -40,12 +40,10 @@ namespace BizHawk.Client.Common
}
}
private long _stateCleanupTime;
private readonly long _stateCleanupPeriod = 10000;
private bool _isMountedForWrite;
private readonly TasMovie _movie;
private StateManagerDecay _decay;
private ulong _expectedStateSize;
private int _stateFrequency;
private readonly int _minFrequency = 1;
@ -53,7 +51,6 @@ namespace BizHawk.Client.Common
private int _maxStates => (int)(Settings.Cap / _expectedStateSize) +
(int)((ulong)Settings.DiskCapacitymb * 1024 * 1024 / _expectedStateSize);
private int _fileStateGap => 1 << Settings.FileStateGap;
private int _greenzoneDecayCall = 0;
public TasStateManager(TasMovie movie)
{
@ -65,7 +62,7 @@ namespace BizHawk.Client.Common
SetState(0, _movie.BinarySavestate);
}
_stateCleanupTime = DateTime.Now.Ticks + _stateCleanupPeriod;
_decay = new StateManagerDecay(this);
}
public void Dispose()
@ -79,6 +76,8 @@ namespace BizHawk.Client.Common
_stateFrequency = NumberExtensions.Clamp(
((int)_expectedStateSize / Settings.MemStateGapDivider / 1024),
_minFrequency, _maxFrequency);
_decay.UpdateSettings(_maxStates, _stateFrequency, 4);
}
/// <summary>
@ -110,7 +109,7 @@ namespace BizHawk.Client.Common
NdbDatabase = new NDBDatabase(StatePath, Settings.DiskCapacitymb * 1024 * 1024, (int)_expectedStateSize);
}
public TasStateManagerSettings Settings { get; set; }
/// <summary>
@ -156,8 +155,8 @@ namespace BizHawk.Client.Common
public void Capture(bool force = false)
{
bool shouldCapture;
int frame = Global.Emulator.Frame;
if (_movie.StartsFromSavestate && frame == 0) // Never capture frame 0 on savestate anchored movies since we have it anyway
{
shouldCapture = false;
@ -170,7 +169,7 @@ namespace BizHawk.Client.Common
{
shouldCapture = true;
}
else if (_movie.Markers.IsMarker(frame + 1))
else if (StateIsMarker(frame))
{
shouldCapture = true; // Markers shoudl always get priority
}
@ -253,7 +252,7 @@ namespace BizHawk.Client.Common
return anyInvalidated;
}
private bool StateIsMarker(int frame)
public bool StateIsMarker(int frame)
{
if (frame == -1)
{
@ -263,8 +262,8 @@ namespace BizHawk.Client.Common
return _movie.Markers.IsMarker(frame + 1);
}
private void RemoveState(int frame)
{
public void RemoveState(int frame)
{
int index = _states.IndexOfKey(frame);
if (frame < 1 || index < 1)
@ -292,89 +291,9 @@ namespace BizHawk.Client.Common
/// </summary>
public void LimitStateCount()
{
if (Used + _expectedStateSize > Settings.Cap || DiskUsed > (ulong)Settings.DiskCapacitymb * 1024 * 1024)
if (StateCount + 1 > _maxStates || DiskUsed > (ulong)Settings.DiskCapacitymb * 1024 * 1024)
{
// feos: this GREENZONE DECAY algo is critically important (and crazy), so I'll explain it fully here
// we force decay gap between memory-based states that increases for every new region
// regions start from the state right above the current frame (or right below for forward decay)
// we use powers of 2 to determine decay gap size and region length
// amount of regions and their lengths depend on how many powers of 2 we want to use
// we use 5 powers of 2, from 0 to 4. decay gap goes 0, 1, 3, 7, 15 (in reality, not perfectly so)
// 1 decay gap unit is 1 frame * minimal state frequency
// first region has no decay gaps, the length of that region in fceux is called "greenzone capacity"
// every next region is twice longer than its predecessor, but it has the same amount of states (approximately)
// states beyond last region are erased, except for state at frame 0
// algo works in both directions, alternating between them on every call
// it removes as many states is its pattern needs, which allows for cooldown before cap is about to get hit again
// todo: this is still imperfect, even though probably usable already
_greenzoneDecayCall++;
int regionStates = _maxStates / 5;
int baseIndex = GetStateIndexByFrame(Global.Emulator.Frame);
int direction = 1; // negative for forward decay
if (_greenzoneDecayCall % 2 == 0)
{
baseIndex++;
direction = -1;
}
int lastStateFrame = -1;
for (int mult = 2, currentStateIndex = baseIndex - regionStates * direction; mult <= 16; mult *= 2)
{
int gap = _stateFrequency * mult;
int regionFrames = regionStates * gap;
for (; ; currentStateIndex -= direction)
{
// are we out of states yet?
if (direction > 0 && currentStateIndex <= 1 ||
direction < 0 && currentStateIndex >= _states.Count - 1)
return;
int nextStateIndex = currentStateIndex - direction;
NumberExtensions.Clamp(nextStateIndex, 1, _states.Count - 1);
int currentStateFrame = GetStateFrameByIndex(currentStateIndex);
int nextStateFrame = GetStateFrameByIndex(nextStateIndex);
int frameDiff = Math.Max(currentStateFrame, nextStateFrame) - Math.Min(currentStateFrame, nextStateFrame);
lastStateFrame = currentStateFrame;
if (frameDiff < gap)
{
RemoveState(nextStateFrame);
// when going forward, we don't remove the state before current
// but current changes anyway, so compensate for that here
if (direction < 0)
currentStateIndex--;
}
else
{
regionFrames -= frameDiff;
if (regionFrames <= 0)
break;
}
}
}
// finish off whatever we've missed
if (lastStateFrame > -1)
{
List<KeyValuePair<int, StateManagerState>> leftoverStates;
if (direction > 0)
leftoverStates = _states.Where(s => s.Key > 0 && s.Key < lastStateFrame).ToList();
else
leftoverStates = _states.Where(s => s.Key > lastStateFrame && s.Key < LastEmulatedFrame).ToList();
foreach (var state in leftoverStates)
{
RemoveState(state.Key);
}
}
_decay.Trigger(StateCount + 1 - _maxStates);
}
}
@ -387,21 +306,22 @@ namespace BizHawk.Client.Common
// still leave marker states
for (int i = 1; i < _states.Count; i++)
{
if (_movie.Markers.IsMarker(_states.ElementAt(i).Key + 1)
|| _states.ElementAt(i).Key % _fileStateGap == 0)
int frame = GetStateFrameByIndex(i);
if (StateIsMarker(frame) || frame % _fileStateGap < _stateFrequency)
{
continue;
}
ret.Add(i);
if (_states.ElementAt(i).Value.IsOnDisk)
if (_states.Values[i].IsOnDisk)
{
saveUsed -= _expectedStateSize;
}
else
{
saveUsed -= (ulong)_states.ElementAt(i).Value.Length;
saveUsed -= (ulong)_states.Values[i].Length;
}
}
@ -412,13 +332,12 @@ namespace BizHawk.Client.Common
{
do
{
index++;
if (index >= _states.Count)
if (++index >= _states.Count)
{
break;
}
}
while (_movie.Markers.IsMarker(_states.ElementAt(index).Key + 1));
while (StateIsMarker(GetStateFrameByIndex(index)));
if (index >= _states.Count)
{
@ -427,13 +346,13 @@ namespace BizHawk.Client.Common
ret.Add(index);
if (_states.ElementAt(index).Value.IsOnDisk)
if (_states.Values[index].IsOnDisk)
{
saveUsed -= _expectedStateSize;
}
else
{
saveUsed -= (ulong)_states.ElementAt(index).Value.Length;
saveUsed -= (ulong)_states.Values[index].Length;
}
}
@ -441,19 +360,18 @@ namespace BizHawk.Client.Common
index = 0;
while (saveUsed > (ulong)Settings.DiskSaveCapacitymb * 1024 * 1024)
{
index++;
if (!ret.Contains(index))
if (!ret.Contains(++index))
{
ret.Add(index);
}
if (_states.ElementAt(index).Value.IsOnDisk)
if (_states.Values[index].IsOnDisk)
{
saveUsed -= _expectedStateSize;
}
else
{
saveUsed -= (ulong)_states.ElementAt(index).Value.Length;
saveUsed -= (ulong)_states.Values[index].Length;
}
}
@ -472,18 +390,24 @@ namespace BizHawk.Client.Common
}
}
// Map:
// 4 bytes - total savestate count
// [Foreach state]
// 4 bytes - frame
// 4 bytes - length of savestate
// 0 - n savestate
public void Save(BinaryWriter bw)
{
List<int> noSave = ExcludeStates();
bw.Write(_states.Count - noSave.Count);
for (int i = 0; i < _states.Count; i++)
{
if (noSave.Contains(i))
{
continue;
}
KeyValuePair<int, StateManagerState> kvp = _states.ElementAt(i);
bw.Write(kvp.Key);
bw.Write(kvp.Value.Length);
@ -491,18 +415,14 @@ namespace BizHawk.Client.Common
}
}
// Map:
// 4 bytes - total savestate count
// [Foreach state]
// 4 bytes - frame
// 4 bytes - length of savestate
// 0 - n savestate
public void Load(BinaryReader br)
{
_states.Clear();
try
{
int nstates = br.ReadInt32();
for (int i = 0; i < nstates; i++)
{
int frame = br.ReadInt32();
@ -543,7 +463,9 @@ namespace BizHawk.Client.Common
/// <returns></returns>
public int GetStateFrameByIndex(int index)
{
return _states.ElementAt(index).Key;
// feos: this is called super often by decay
// this method is hundred times faster than _states.ElementAt(index).Key
return _states.Keys[index];
}
private ulong _used;
@ -606,7 +528,7 @@ namespace BizHawk.Client.Common
}
}
public int LastEmulatedFrame
public int LastStatedFrame
{
get
{

View File

@ -764,7 +764,7 @@ namespace BizHawk.Client.EmuHawk
GoToFrame(0);
int lastState = 0;
int goToFrame = CurrentTasMovie.TasStateManager.LastEmulatedFrame;
int goToFrame = CurrentTasMovie.TasStateManager.LastStatedFrame;
do
{
Mainform.FrameAdvance();

View File

@ -807,6 +807,7 @@ namespace BizHawk.Client.EmuHawk
TasView.Refresh();
//SetSplicer();
CurrentTasMovie.FlushInputCache();
CurrentTasMovie.UseInputCache = false;
@ -939,6 +940,7 @@ namespace BizHawk.Client.EmuHawk
SplicerStatusLabel.Text =
"Selected: " + TasView.SelectedRows.Count() + " frame" +
(TasView.SelectedRows.Count() == 1 ? "" : "s") +
//", State count: " + CurrentTasMovie.TasStateManager.StateCount.ToString() +
", Clipboard: " + (_tasClipboard.Any() ? _tasClipboard.Count + " frame" +
(_tasClipboard.Count == 1 ? "" : "s") : "empty");
}