Skip to content

Commit

Permalink
Merge pull request #83 from aarondandy/utc-ticks
Browse files Browse the repository at this point in the history
Implements what is hopefully a safer time limit check
  • Loading branch information
aarondandy authored Nov 23, 2023
2 parents 4e8fc98 + a09ab0c commit a2ec6af
Show file tree
Hide file tree
Showing 15 changed files with 117 additions and 126 deletions.
84 changes: 38 additions & 46 deletions WeCantSpell.Hunspell/OperationLimiters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,88 +3,60 @@

namespace WeCantSpell.Hunspell;

struct OperationTimedLimiter
ref struct OperationTimedLimiter
{
public OperationTimedLimiter(TimeSpan timeLimit, CancellationToken cancellationToken)
: this((int)timeLimit.TotalMilliseconds, cancellationToken)
{
}

public OperationTimedLimiter(int timeLimitMs, CancellationToken cancellationToken)
{
_startedAtMs = Environment.TickCount;
_timeLimitMs = timeLimitMs;
_expiresAtMs = _startedAtMs + timeLimitMs;
_timer = new ExpirationTimer(timeLimit);
_cancellationToken = cancellationToken;
_hasTriggeredCancellation = false;
}

private int _expiresAtMs;
private int _startedAtMs;
private readonly int _timeLimitMs;
private readonly ExpirationTimer _timer;
private readonly CancellationToken _cancellationToken;
private bool _hasTriggeredCancellation;

public bool HasBeenCanceled => _hasTriggeredCancellation || _cancellationToken.IsCancellationRequested;
public readonly bool HasBeenCanceled => _hasTriggeredCancellation || _cancellationToken.IsCancellationRequested;

public bool QueryForCancellation()
{
if (!_hasTriggeredCancellation)
{
if (_cancellationToken.IsCancellationRequested || _expiresAtMs <= Environment.TickCount)
if (_cancellationToken.IsCancellationRequested || _timer.CheckForExpiration())
{
_hasTriggeredCancellation = true;
}
}

return _hasTriggeredCancellation;
}

public void Reset()
{
_hasTriggeredCancellation = false;
_startedAtMs = Environment.TickCount;
_expiresAtMs = _startedAtMs + _timeLimitMs;
}
}

sealed class OperationTimedCountLimiter
ref struct OperationTimedCountLimiter
{
/// <summary>
/// This is the number of operations that are added to a timer if it runs out of operations
/// before the time limit has expired.
/// This is the number of operations that are added to a limiter if it runs out of operations before the time limit has expired.
/// </summary>
private const int MaxPlusTimer = 100;

public OperationTimedCountLimiter(TimeSpan timeLimit, int countLimit, CancellationToken cancellationToken)
: this((int)timeLimit.TotalMilliseconds, countLimit, cancellationToken)
{
}

public OperationTimedCountLimiter(int timeLimitMs, int countLimit, CancellationToken cancellationToken)
{
#if DEBUG
if (countLimit < 0) throw new ArgumentOutOfRangeException(nameof(countLimit));
#endif

_startedAtMs = Environment.TickCount;
_timeLimitMs = timeLimitMs;
_expiresAtMs = _startedAtMs + timeLimitMs;
_countLimit = countLimit;
_counter = countLimit;
_timer = new ExpirationTimer(timeLimit);
_cancellationToken = cancellationToken;
_counter = countLimit;
_hasTriggeredCancellation = false;
}

private readonly int _timeLimitMs;
private readonly int _countLimit;
private int _counter;
private int _expiresAtMs;
private int _startedAtMs;
private readonly ExpirationTimer _timer;
private readonly CancellationToken _cancellationToken;
private int _counter;
private bool _hasTriggeredCancellation;

public bool HasBeenCanceled => _hasTriggeredCancellation || _cancellationToken.IsCancellationRequested;
public readonly bool HasBeenCanceled => _hasTriggeredCancellation || _cancellationToken.IsCancellationRequested;

public bool QueryForCancellation()
{
Expand All @@ -98,7 +70,7 @@ public bool QueryForCancellation()
{
_counter--;
}
else if (_expiresAtMs > Environment.TickCount)
else if (_timer.CheckForExpiration())
{
_counter = MaxPlusTimer;
}
Expand All @@ -111,12 +83,32 @@ public bool QueryForCancellation()

return _hasTriggeredCancellation;
}
}

readonly struct ExpirationTimer
{
private const long DisabledSentinelValue = long.MinValue;

private static long GetCurrentTicks() => DateTime.UtcNow.Ticks;

public void Reset()
internal ExpirationTimer(TimeSpan timeLimit)
{
_counter = _countLimit;
_hasTriggeredCancellation = false;
_startedAtMs = Environment.TickCount;
_expiresAtMs = _startedAtMs + _timeLimitMs;
var limitTicks = timeLimit.Ticks;
if (limitTicks < 0)
{
_expiresAt = DisabledSentinelValue;
}
else
{
_expiresAt = GetCurrentTicks() + limitTicks;
if (_expiresAt < DateTime.MinValue.Ticks || _expiresAt > DateTime.MaxValue.Ticks)
{
_expiresAt = DisabledSentinelValue;
}
}
}

private readonly long _expiresAt;

public readonly bool CheckForExpiration() => _expiresAt != DisabledSentinelValue && _expiresAt <= GetCurrentTicks();
}
28 changes: 13 additions & 15 deletions WeCantSpell.Hunspell/WordList.QuerySuggest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -622,8 +622,6 @@ internal bool Suggest(List<string> slst, string word, ref bool onlyCompoundSug)
word = word.GetReversed();
}

var opLimiter = new OperationTimedLimiter(Options.TimeLimitCompoundSuggest, _query.CancellationToken);

// three loops:
// - the first without compounding,
// - the second one with 2-word compounding,
Expand All @@ -640,7 +638,7 @@ internal bool Suggest(List<string> slst, string word, ref bool onlyCompoundSug)
for (state.CpdSuggest = 0; state.CpdSuggest < 3 && !noCompoundTwoWords; state.CpdSuggest++)
{
// initialize both in non-compound and compound cycles
opLimiter.Reset();
var opLimiter = new OperationTimedLimiter(Options.TimeLimitCompoundSuggest, _query.CancellationToken);

// limit compound suggestion
if (state.CpdSuggest > 0)
Expand Down Expand Up @@ -886,7 +884,7 @@ private void BadChar(ref SuggestState state)
}

candidate[i] = tryString[j];
TestSug(state.SuggestionList, candidate, ref state, timer);
TestSug(state.SuggestionList, candidate, ref state, ref timer);
candidate[i] = tmpc;

if (timer.HasBeenCanceled)
Expand Down Expand Up @@ -967,7 +965,7 @@ private void ForgotChar(ref SuggestState state)
word.CopyTo(candidate);
candidate[word.Length] = tryChar;

TestSug(state.SuggestionList, candidate, ref state, timer);
TestSug(state.SuggestionList, candidate, ref state, ref timer);

if (timer.HasBeenCanceled)
{
Expand All @@ -979,7 +977,7 @@ private void ForgotChar(ref SuggestState state)
candidate[index] = tryChar;
word.Slice(index).CopyTo(candidate.Slice(index + 1));

TestSug(state.SuggestionList, candidate, ref state, timer);
TestSug(state.SuggestionList, candidate, ref state, ref timer);

if (timer.HasBeenCanceled)
{
Expand Down Expand Up @@ -1148,10 +1146,10 @@ private void MapChars(List<string> wlst, string word, in SuggestState state)

var candidate = string.Empty;
var timer = new OperationTimedCountLimiter(Options.TimeLimitSuggestStep, Options.MinTimer, _query.CancellationToken);
MapRelated(word, ref candidate, wn: 0, wlst, in state, timer, depth: 0);
MapRelated(word, ref candidate, wn: 0, wlst, in state, ref timer, depth: 0);
}

private void MapRelated(string word, ref string candidate, int wn, List<string> wlst, in SuggestState state, OperationTimedCountLimiter timer, int depth)
private void MapRelated(string word, ref string candidate, int wn, List<string> wlst, in SuggestState state, ref OperationTimedCountLimiter timer, int depth)
{
if (word.Length == wn)
{
Expand All @@ -1160,7 +1158,7 @@ private void MapRelated(string word, ref string candidate, int wn, List<string>
&&
!wlst.Contains(candidate)
&&
CheckWord(candidate, state.CpdSuggest, timer) != 0
CheckWord(candidate, state.CpdSuggest, ref timer) != 0
)
{
wlst.Add(candidate);
Expand All @@ -1186,7 +1184,7 @@ private void MapRelated(string word, ref string candidate, int wn, List<string>
foreach (var otherMapEntryValue in mapEntry.GetInternalArray())
{
candidate = candidatePrefix + otherMapEntryValue;
MapRelated(word, ref candidate, wn + mapEntryValue.Length, wlst, in state, timer, depth + 1);
MapRelated(word, ref candidate, wn + mapEntryValue.Length, wlst, in state, ref timer, depth + 1);

if (timer.HasBeenCanceled)
{
Expand All @@ -1200,15 +1198,15 @@ private void MapRelated(string word, ref string candidate, int wn, List<string>
if (!inMap)
{
candidate += word[wn];
MapRelated(word, ref candidate, wn + 1, wlst, in state, timer, depth + 1);
MapRelated(word, ref candidate, wn + 1, wlst, in state, ref timer, depth + 1);
}
}

private void TestSug(List<string> wlst, ReadOnlySpan<char> candidate, ref SuggestState state, OperationTimedCountLimiter timer)
private void TestSug(List<string> wlst, ReadOnlySpan<char> candidate, ref SuggestState state, ref OperationTimedCountLimiter timer)
{
if (wlst.Count < MaxSuggestions && !wlst.Contains(candidate))
{
var result = CheckWord(candidate, state.CpdSuggest, timer);
var result = CheckWord(candidate, state.CpdSuggest, ref timer);
if (result != 0)
{
// compound word in the dictionary
Expand Down Expand Up @@ -1258,7 +1256,7 @@ private void TestSug(List<string> wlst, ReadOnlySpan<char> candidate, ref Sugges
}
}

private byte CheckWord(string word, byte cpdSuggest, OperationTimedCountLimiter timer)
private byte CheckWord(string word, byte cpdSuggest, ref OperationTimedCountLimiter timer)
{
if (timer.QueryForCancellation())
{
Expand All @@ -1268,7 +1266,7 @@ private byte CheckWord(string word, byte cpdSuggest, OperationTimedCountLimiter
return CheckWord(word, cpdSuggest);
}

private byte CheckWord(ReadOnlySpan<char> word, byte cpdSuggest, OperationTimedCountLimiter timer)
private byte CheckWord(ReadOnlySpan<char> word, byte cpdSuggest, ref OperationTimedCountLimiter timer)
{
if (timer.QueryForCancellation())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ MaxWarmupIterationCount=5 MinIterationCount=1 MinWarmupIterationCount=1
```
| Method | Mean | Error | StdDev | Min | Max | Median | Ratio |
|--------------------------- |----------:|----------:|----------:|----------:|----------:|----------:|------:|
| &#39;Check words: WeCantSpell&#39; | 18.338 ms | 0.2788 ms | 0.0432 ms | 18.287 ms | 18.392 ms | 18.337 ms | 1.00 |
| &#39;Check words: NHunspell&#39; | 6.060 ms | 0.1201 ms | 0.0794 ms | 5.973 ms | 6.175 ms | 6.049 ms | 0.33 |
| &#39;Check words: WeCantSpell&#39; | 18.805 ms | 0.1595 ms | 0.0247 ms | 18.782 ms | 18.830 ms | 18.804 ms | 1.00 |
| &#39;Check words: NHunspell&#39; | 6.115 ms | 0.0697 ms | 0.0038 ms | 6.112 ms | 6.119 ms | 6.112 ms | 0.33 |
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,LargeAddressAware,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,Mean,Error,StdDev,Min,Max,Median,Ratio
'Check words: WeCantSpell',Job-TPWOKF,False,Default,Default,Default,1,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET Framework 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,Default,1.0000 s,Default,20,5,Default,1,1,Default,16,Default,18.338 ms,0.2788 ms,0.0432 ms,18.287 ms,18.392 ms,18.337 ms,1.00
'Check words: NHunspell',Job-TPWOKF,False,Default,Default,Default,1,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET Framework 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,Default,1.0000 s,Default,20,5,Default,1,1,Default,16,Default,6.060 ms,0.1201 ms,0.0794 ms,5.973 ms,6.175 ms,6.049 ms,0.33
'Check words: WeCantSpell',Job-TPWOKF,False,Default,Default,Default,1,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET Framework 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,Default,1.0000 s,Default,20,5,Default,1,1,Default,16,Default,18.805 ms,0.1595 ms,0.0247 ms,18.782 ms,18.830 ms,18.804 ms,1.00
'Check words: NHunspell',Job-TPWOKF,False,Default,Default,Default,1,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET Framework 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,Default,1.0000 s,Default,20,5,Default,1,1,Default,16,Default,6.115 ms,0.0697 ms,0.0038 ms,6.112 ms,6.119 ms,6.112 ms,0.33
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang='en'>
<head>
<meta charset='utf-8' />
<title>WeCantSpell.Hunspell.Benchmarks.NHunspell.Suites.CheckEnUsSuite-20231123-122835</title>
<title>WeCantSpell.Hunspell.Benchmarks.NHunspell.Suites.CheckEnUsSuite-20231123-134440</title>

<style type="text/css">
table { border-collapse: collapse; display: block; width: 100%; overflow: auto; }
Expand All @@ -25,8 +25,8 @@
<table>
<thead><tr><th>Method </th><th>Mean</th><th>Error</th><th>StdDev</th><th>Min</th><th>Max</th><th>Median</th><th>Ratio</th>
</tr>
</thead><tbody><tr><td>&#39;Check words: WeCantSpell&#39;</td><td>18.338 ms</td><td>0.2788 ms</td><td>0.0432 ms</td><td>18.287 ms</td><td>18.392 ms</td><td>18.337 ms</td><td>1.00</td>
</tr><tr><td>&#39;Check words: NHunspell&#39;</td><td>6.060 ms</td><td>0.1201 ms</td><td>0.0794 ms</td><td>5.973 ms</td><td>6.175 ms</td><td>6.049 ms</td><td>0.33</td>
</thead><tbody><tr><td>&#39;Check words: WeCantSpell&#39;</td><td>18.805 ms</td><td>0.1595 ms</td><td>0.0247 ms</td><td>18.782 ms</td><td>18.830 ms</td><td>18.804 ms</td><td>1.00</td>
</tr><tr><td>&#39;Check words: NHunspell&#39;</td><td>6.115 ms</td><td>0.0697 ms</td><td>0.0038 ms</td><td>6.112 ms</td><td>6.119 ms</td><td>6.112 ms</td><td>0.33</td>
</tr></tbody></table>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ MinInvokeCount=1 IterationTime=1.0000 s MaxIterationCount=20
MaxWarmupIterationCount=5 MinIterationCount=1 MinWarmupIterationCount=1
```
| Method | Mean | Error | StdDev | Min | Max | Median | Ratio | RatioSD |
|----------------------------- |-----------:|---------:|---------:|-----------:|-----------:|-----------:|------:|--------:|
| &#39;Suggest words: WeCantSpell&#39; | 759.0 ms | 7.36 ms | 1.14 ms | 758.1 ms | 760.7 ms | 758.7 ms | 1.00 | 0.00 |
| &#39;Suggest words: NHunspell&#39; | 1,895.0 ms | 32.72 ms | 17.11 ms | 1,873.2 ms | 1,911.9 ms | 1,899.5 ms | 2.50 | 0.02 |
| Method | Mean | Error | StdDev | Min | Max | Median | Ratio |
|----------------------------- |-----------:|---------:|--------:|-----------:|-----------:|-----------:|------:|
| &#39;Suggest words: WeCantSpell&#39; | 750.4 ms | 9.09 ms | 2.36 ms | 748.3 ms | 753.4 ms | 749.4 ms | 1.00 |
| &#39;Suggest words: NHunspell&#39; | 1,913.3 ms | 29.95 ms | 1.64 ms | 1,911.7 ms | 1,915.0 ms | 1,913.1 ms | 2.55 |
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,LargeAddressAware,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,Mean,Error,StdDev,Min,Max,Median,Ratio,RatioSD
'Suggest words: WeCantSpell',Job-TPWOKF,False,Default,Default,Default,1,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET Framework 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,Default,1.0000 s,Default,20,5,Default,1,1,Default,16,Default,759.0 ms,7.36 ms,1.14 ms,758.1 ms,760.7 ms,758.7 ms,1.00,0.00
'Suggest words: NHunspell',Job-TPWOKF,False,Default,Default,Default,1,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET Framework 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,Default,1.0000 s,Default,20,5,Default,1,1,Default,16,Default,"1,895.0 ms",32.72 ms,17.11 ms,"1,873.2 ms","1,911.9 ms","1,899.5 ms",2.50,0.02
Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,LargeAddressAware,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,Mean,Error,StdDev,Min,Max,Median,Ratio
'Suggest words: WeCantSpell',Job-TPWOKF,False,Default,Default,Default,1,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET Framework 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,Default,1.0000 s,Default,20,5,Default,1,1,Default,16,Default,750.4 ms,9.09 ms,2.36 ms,748.3 ms,753.4 ms,749.4 ms,1.00
'Suggest words: NHunspell',Job-TPWOKF,False,Default,Default,Default,1,Default,Default,1111111111111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET Framework 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,Default,1.0000 s,Default,20,5,Default,1,1,Default,16,Default,"1,913.3 ms",29.95 ms,1.64 ms,"1,911.7 ms","1,915.0 ms","1,913.1 ms",2.55
Loading

0 comments on commit a2ec6af

Please sign in to comment.