-
-
Notifications
You must be signed in to change notification settings - Fork 183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ExpireAfter Redesign #868
Merged
Merged
ExpireAfter Redesign #868
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
89dd436
Fixed potential infinite-looping in the "SchedulerIsInaccurate" Expir…
JakenVeina 5b14497
Enhanced benchmarks for ExpireAfter operators to improve code coverag…
JakenVeina 7d2db60
Re-designed ExpireAfter operators, from scratch, eliminating a variet…
JakenVeina File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 102 additions & 30 deletions
132
src/DynamicData.Benchmarks/Cache/ExpireAfter_Cache_ForSource.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,62 +1,134 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
|
||
using BenchmarkDotNet.Attributes; | ||
|
||
using Bogus; | ||
|
||
namespace DynamicData.Benchmarks.Cache; | ||
|
||
[MemoryDiagnoser] | ||
[MarkdownExporterAttribute.GitHub] | ||
public class ExpireAfter_Cache_ForSource | ||
{ | ||
public ExpireAfter_Cache_ForSource() | ||
=> _items = Enumerable | ||
.Range(1, 1_000) | ||
{ | ||
// Not exercising Moved, since SourceCache<> doesn't support it. | ||
_changeReasons = | ||
[ | ||
ChangeReason.Add, | ||
ChangeReason.Refresh, | ||
ChangeReason.Remove, | ||
ChangeReason.Update | ||
]; | ||
|
||
// Weights are chosen to make the cache size likely to grow over time, | ||
// exerting more pressure on the system the longer the benchmark runs. | ||
// Also, to prevent bogus operations (E.G. you can't remove an item from an empty cache). | ||
_changeReasonWeightsWhenCountIs0 = | ||
[ | ||
1f, // Add | ||
0f, // Refresh | ||
0f, // Remove | ||
0f // Update | ||
]; | ||
|
||
_changeReasonWeightsOtherwise = | ||
[ | ||
0.30f, // Add | ||
0.25f, // Refresh | ||
0.20f, // Remove | ||
0.25f // Update | ||
]; | ||
|
||
_editCount = 5_000; | ||
_maxChangeCount = 20; | ||
|
||
var randomizer = new Randomizer(1234567); | ||
|
||
var minItemLifetime = TimeSpan.FromMilliseconds(1); | ||
var maxItemLifetime = TimeSpan.FromMilliseconds(10); | ||
_items = Enumerable.Range(1, _editCount * _maxChangeCount) | ||
.Select(id => new Item() | ||
{ | ||
Id = id | ||
Id = id, | ||
Lifetime = randomizer.Bool() | ||
? TimeSpan.FromTicks(randomizer.Long(minItemLifetime.Ticks, maxItemLifetime.Ticks)) | ||
: null | ||
}) | ||
.ToArray(); | ||
.ToImmutableArray(); | ||
} | ||
|
||
[Benchmark] | ||
[Arguments(1, 0)] | ||
[Arguments(1, 1)] | ||
[Arguments(10, 0)] | ||
[Arguments(10, 1)] | ||
[Arguments(10, 10)] | ||
[Arguments(100, 0)] | ||
[Arguments(100, 1)] | ||
[Arguments(100, 10)] | ||
[Arguments(100, 100)] | ||
[Arguments(1_000, 0)] | ||
[Arguments(1_000, 1)] | ||
[Arguments(1_000, 10)] | ||
[Arguments(1_000, 100)] | ||
[Arguments(1_000, 1_000)] | ||
public void AddsRemovesAndFinalization(int addCount, int removeCount) | ||
public void RandomizedEditsAndExpirations() | ||
{ | ||
using var source = new SourceCache<Item, int>(static item => item.Id); | ||
|
||
using var subscription = source | ||
.ExpireAfter( | ||
timeSelector: static _ => TimeSpan.FromMinutes(60), | ||
interval: null) | ||
timeSelector: static item => item.Lifetime, | ||
interval: null) | ||
.Subscribe(); | ||
|
||
for (var i = 0; i < addCount; ++i) | ||
source.AddOrUpdate(_items[i]); | ||
|
||
for (var i = 0; i < removeCount; ++i) | ||
source.RemoveKey(_items[i].Id); | ||
PerformRandomizedEdits(source); | ||
|
||
subscription.Dispose(); | ||
} | ||
|
||
private readonly IReadOnlyList<Item> _items; | ||
private void PerformRandomizedEdits(SourceCache<Item, int> source) | ||
{ | ||
var randomizer = new Randomizer(1234567); | ||
|
||
var nextItemIndex = 0; | ||
|
||
private sealed class Item | ||
for (var i = 0; i < _editCount; ++i) | ||
{ | ||
source.Edit(updater => | ||
{ | ||
var changeCount = randomizer.Int(1, _maxChangeCount); | ||
for (var i = 0; i < changeCount; ++i) | ||
{ | ||
var changeReason = randomizer.WeightedRandom(_changeReasons, updater.Count switch | ||
{ | ||
0 => _changeReasonWeightsWhenCountIs0, | ||
_ => _changeReasonWeightsOtherwise | ||
}); | ||
|
||
switch (changeReason) | ||
{ | ||
case ChangeReason.Add: | ||
updater.AddOrUpdate(_items[nextItemIndex++]); | ||
break; | ||
|
||
case ChangeReason.Refresh: | ||
updater.Refresh(updater.Keys.ElementAt(randomizer.Int(0, updater.Count - 1))); | ||
break; | ||
|
||
case ChangeReason.Remove: | ||
updater.RemoveKey(updater.Keys.ElementAt(randomizer.Int(0, updater.Count - 1))); | ||
break; | ||
|
||
case ChangeReason.Update: | ||
updater.AddOrUpdate(updater.Items.ElementAt(randomizer.Int(0, updater.Count - 1))); | ||
break; | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
|
||
private readonly ChangeReason[] _changeReasons; | ||
private readonly float[] _changeReasonWeightsOtherwise; | ||
private readonly float[] _changeReasonWeightsWhenCountIs0; | ||
private readonly int _editCount; | ||
private readonly ImmutableArray<Item> _items; | ||
private readonly int _maxChangeCount; | ||
|
||
private sealed record Item | ||
{ | ||
public int Id { get; init; } | ||
public required int Id { get; init; } | ||
|
||
public required TimeSpan? Lifetime { get; init; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are these? If they're important, don't just turn them off for the entire project. Add an exception for each case so that people still try to avoid them if they can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SA1512 is new lines after a comment. You can use a SuppressMessageAttribute to disable where its appropriate but generally you don't want new lines after comments anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's see...
RCS1085 is for always using auto-implemented properties, when possible. Violations are here, here, and here. In each of these, we have a complex object where there's far more value in being able to see all of its persistent state (I.E. private fields) in one place, than in reducing the file size by 2 lines. This is a common-enough scenario, in my mind, that it warrants disabling the rule.
SA1201 says "a property should not follow a method", but it ignores the fact that it's a
protected
property, and I've always encountered standard practice to be thatprotected
instance members don't come beforepublic
instance members. Violations are here, here, and here.SA1512 says "single-line comments should not be followed by blank line", with violations here, here, here, and here. Intuitively, it's standard practice to "attach" comments to the lines of code that they apply to, which I imagine is the origin of this rule, but all of these are examples of comments that DON'T apply to a specific line of code. They are actually commenting on the LACK of lines of code that one might expect to be here. Since the rule applies to a command, and you can't disable rules upon individual comments, I think the rule should be disabled entirely.
SA1513 says "Closing brace should be followed by blank line", and is violated here and here. In these cases, I'm attaching a line of code to the end of a loop, in an effort to indicate that it fundamentally is a finalization action for that loop. Not a big deal, I probably would have just fixed these two if I hadn't already disabled the rule for the 11 other violations (I'll just link this one, they're all the same), which consist of having a
break;
immediately following a bracketedcase
block. In my mind the closing bracket itself serves the purpose that a blank line normally would, for a multi-linecase
block, and inserting an additional blank line here makes it subtly harder to read.SA1515 says "Single line comments should be preceeded by blank line", and is violated here, here, here, and here. Also here and here, but those are within the
.Tests
project. In all of these cases, we're commenting on a single line withing a multi-line action, where the comment is too long to put at the end of the line, and where the multi-line action would normally have no blank lines in it. These are all valid in my mind, because of the implicit rule that comments should be directly attached to the thing they're commenting on, not potentially many lines away from it, and with so many valid use-cases being flagged, this seems like a rule that should be disabled.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got to be honest, I really dislike style enforcement.
I have been tempted to turn it off entirely in DD as it drives me insane how slow it is to run and how it tries to force me to comment code which I have not even written yet!!!! I always end up turning it off, do my code, then turn it on to see what it complains about. The worst thing is that it also slows my development down.
We're grown ups and surely we should be allowed to express our own style. There are a few things that are important like comments, but surely we could enforce that as part of the PR process.
In a better version of of the net eco-system we'd all be using automatic linting.
Perhaps we should get rid of it completely - thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fully agree. I think style-enforcement is kind of a fundamentally-flawed concept. Good styling is about promoting human-readability of code, computers are fundamentally ill-equipped to understand what is "human readable", across the board. At least, I have yet to see any system that implements rules granular enough to promote readability, instead of harm it.
I wouldn't lose any sleep, personally. But on a large collaborative project, logically, I know there's a bigger picture than just my opinions, and it would probably be a mistake to remove completely. I think we can focus on de-tuning it, and reducing the impact it has on the developer experience. There are some rules that are nearly-universal, like member ordering and naming, and we gain value by having those. I would point to whitespace rules in particular as usually being best left to a judgement call of the writer.
What's the quick-and-easy way to do this? I've just been manually tossing out
#pragma warning disable
as they pop up, and it's quite annoying.So, wait, what is linting if not this? Like, automatically fixing violations, instead of just reporting them?