Skip to content
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

Re-design for ToObservableChangeSet() #771

Merged
merged 5 commits into from
Dec 6, 2023

Conversation

JakenVeina
Copy link
Collaborator

@JakenVeina JakenVeina commented Dec 3, 2023

Re-designed the .ToObservableChangeSet() operator for both caches and lists, for better independence, proper error handling, and improved performance. Resolves #635.

I worry I went a little overboard with the complexity, so I would definitely appreciate a couple reviews on this.

The good news is the complexity isn't without benefit, according to the benchmarks I wrote previously:

ToObservableChangeSet_Cache (Before)

| Method                     | itemCount | sizeLimit | Mean          | Error       | StdDev      | Median        | Gen0      | Gen1    | Allocated  |
|--------------------------- |---------- |---------- |--------------:|------------:|------------:|--------------:|----------:|--------:|-----------:|
| AddsUpdatesAndFinalization | 0         | -1        |      2.036 us |   0.0396 us |   0.0501 us |      2.015 us |    0.6104 |       - |    2.51 KB |
| AddsUpdatesAndFinalization | 0         | 0         |      1.991 us |   0.0127 us |   0.0106 us |      1.991 us |    0.6104 |       - |    2.51 KB |
| AddsUpdatesAndFinalization | 0         | 1         |      5.630 us |   0.0610 us |   0.0476 us |      5.614 us |    1.4267 |       - |    5.84 KB |
| AddsUpdatesAndFinalization | 1         | 1         |      7.303 us |   0.1317 us |   0.1167 us |      7.278 us |    1.8158 |       - |    7.44 KB |
| AddsUpdatesAndFinalization | 10        | -1        |      5.446 us |   0.0308 us |   0.0273 us |      5.439 us |    1.7319 |       - |     7.1 KB |
| AddsUpdatesAndFinalization | 10        | 1         |     80.155 us |   0.1413 us |   0.1104 us |     80.168 us |    7.4463 |       - |   30.31 KB |
| AddsUpdatesAndFinalization | 10        | 5         |     16.161 us |   0.3204 us |   0.5939 us |     15.945 us |    4.6082 |       - |   18.89 KB |
| AddsUpdatesAndFinalization | 10        | 10        |     16.592 us |   0.3907 us |   1.1211 us |     16.100 us |    4.6082 |       - |   18.89 KB |
| AddsUpdatesAndFinalization | 100       | -1        |     33.464 us |   0.2536 us |   0.1980 us |     33.429 us |   12.0239 |       - |   49.13 KB |
| AddsUpdatesAndFinalization | 100       | 10        |    810.529 us |  11.1528 us |  10.9536 us |    811.366 us |   83.0078 |       - |  338.29 KB |
| AddsUpdatesAndFinalization | 100       | 50        |    101.668 us |   1.9407 us |   2.9636 us |    100.624 us |   34.0576 |  0.2441 |  139.48 KB |
| AddsUpdatesAndFinalization | 100       | 100       |    105.010 us |   2.1004 us |   6.0263 us |    102.394 us |   34.0576 |  0.2441 |  139.48 KB |
| AddsUpdatesAndFinalization | 1000      | -1        |    319.496 us |   3.9503 us |   3.5019 us |    318.270 us |  108.8867 |       - |   446.1 KB |
| AddsUpdatesAndFinalization | 1000      | 100       | 11,868.026 us | 224.6409 us | 230.6897 us | 11,895.656 us | 2046.8750 | 15.6250 | 8168.74 KB |
| AddsUpdatesAndFinalization | 1000      | 500       |    924.165 us |  17.7331 us |  17.4163 us |    918.035 us |  308.5938 |  0.9766 | 1262.84 KB |
| AddsUpdatesAndFinalization | 1000      | 1000      |    937.450 us |  14.7725 us |  13.8182 us |    937.429 us |  308.5938 |  1.9531 | 1262.85 KB |

ToObservableChangeSet_Cache (After)

| Method                     | itemCount | sizeLimit | Mean         | Error       | StdDev       | Median       | Gen0    | Gen1   | Allocated |
|--------------------------- |---------- |---------- |-------------:|------------:|-------------:|-------------:|--------:|-------:|----------:|
| AddsUpdatesAndFinalization | 0         | -1        |     644.1 ns |     5.68 ns |      4.74 ns |     643.5 ns |  0.3405 |      - |   1.39 KB |
| AddsUpdatesAndFinalization | 0         | 0         |     648.7 ns |     2.76 ns |      2.44 ns |     649.3 ns |  0.3557 |      - |   1.45 KB |
| AddsUpdatesAndFinalization | 0         | 1         |     724.6 ns |     9.34 ns |      8.28 ns |     727.7 ns |  0.4072 |      - |   1.66 KB |
| AddsUpdatesAndFinalization | 1         | 1         |   1,038.8 ns |    14.46 ns |     11.29 ns |   1,036.5 ns |  0.4578 |      - |   1.88 KB |
| AddsUpdatesAndFinalization | 10        | -1        |   2,996.8 ns |    35.32 ns |     29.49 ns |   2,989.6 ns |  0.7172 |      - |   2.94 KB |
| AddsUpdatesAndFinalization | 10        | 1         |   3,694.9 ns |    39.52 ns |     35.04 ns |   3,693.8 ns |  0.8392 |      - |   3.43 KB |
| AddsUpdatesAndFinalization | 10        | 5         |   3,366.2 ns |    50.23 ns |     41.95 ns |   3,353.1 ns |  0.8850 |      - |   3.64 KB |
| AddsUpdatesAndFinalization | 10        | 10        |   3,298.6 ns |    38.84 ns |     34.43 ns |   3,281.5 ns |  1.0300 |      - |   4.21 KB |
| AddsUpdatesAndFinalization | 100       | -1        |  22,892.2 ns |   246.48 ns |    205.82 ns |  22,903.7 ns |  4.5776 |      - |  18.79 KB |
| AddsUpdatesAndFinalization | 100       | 10        |  31,186.4 ns |   609.49 ns |    792.51 ns |  30,959.0 ns |  4.8828 |      - |  20.06 KB |
| AddsUpdatesAndFinalization | 100       | 50        |  27,228.6 ns |   736.77 ns |  2,172.38 ns |  26,546.7 ns |  6.2866 |      - |  25.79 KB |
| AddsUpdatesAndFinalization | 100       | 100       |  25,842.7 ns |   516.26 ns |    756.72 ns |  25,779.1 ns |  6.9275 |      - |  28.42 KB |
| AddsUpdatesAndFinalization | 1000      | -1        | 219,860.6 ns | 2,138.89 ns |  1,786.07 ns | 219,290.6 ns | 32.9590 | 0.2441 | 135.16 KB |
| AddsUpdatesAndFinalization | 1000      | 100       | 304,533.5 ns | 6,081.91 ns | 11,571.46 ns | 302,311.4 ns | 45.4102 |      - | 186.45 KB |
| AddsUpdatesAndFinalization | 1000      | 500       | 284,497.8 ns | 6,103.96 ns | 17,901.87 ns | 284,901.1 ns | 58.5938 | 0.4883 | 240.85 KB |
| AddsUpdatesAndFinalization | 1000      | 1000      | 262,644.1 ns | 5,225.24 ns | 13,765.36 ns | 256,853.2 ns | 66.4063 |      - | 272.36 KB |

ToObservableChangeSet_List (Before)

| Method                     | itemCount | sizeLimit | Mean       | Error     | StdDev    | Median     | Gen0     | Allocated |
|--------------------------- |---------- |---------- |-----------:|----------:|----------:|-----------:|---------:|----------:|
| AddsUpdatesAndFinalization | 0         | -1        |   1.251 us | 0.0110 us | 0.0098 us |   1.246 us |   0.4826 |   1.98 KB |
| AddsUpdatesAndFinalization | 0         | 0         |   1.271 us | 0.0245 us | 0.0430 us |   1.258 us |   0.4826 |   1.98 KB |
| AddsUpdatesAndFinalization | 0         | 1         |   1.234 us | 0.0132 us | 0.0110 us |   1.231 us |   0.4826 |   1.98 KB |
| AddsUpdatesAndFinalization | 1         | 1         |   1.614 us | 0.0301 us | 0.0566 us |   1.594 us |   0.5894 |   2.41 KB |
| AddsUpdatesAndFinalization | 10        | -1        |   4.140 us | 0.0951 us | 0.2804 us |   4.306 us |   1.5030 |   6.14 KB |
| AddsUpdatesAndFinalization | 10        | 1         |   4.815 us | 0.0951 us | 0.1640 us |   4.774 us |   1.9989 |   8.18 KB |
| AddsUpdatesAndFinalization | 10        | 5         |   4.590 us | 0.0670 us | 0.0523 us |   4.583 us |   1.7776 |   7.27 KB |
| AddsUpdatesAndFinalization | 10        | 10        |   3.841 us | 0.0660 us | 0.0811 us |   3.820 us |   1.5030 |   6.14 KB |
| AddsUpdatesAndFinalization | 100       | -1        |  25.062 us | 0.1847 us | 0.1542 us |  25.051 us |  10.4980 |  42.95 KB |
| AddsUpdatesAndFinalization | 100       | 10        |  35.845 us | 0.6984 us | 0.8833 us |  35.535 us |  15.5640 |   63.8 KB |
| AddsUpdatesAndFinalization | 100       | 50        |  31.572 us | 0.6178 us | 0.8661 us |  31.202 us |  13.3057 |  54.53 KB |
| AddsUpdatesAndFinalization | 100       | 100       |  25.368 us | 0.5032 us | 0.4942 us |  25.149 us |  10.4980 |  42.95 KB |
| AddsUpdatesAndFinalization | 1000      | -1        | 239.723 us | 3.5975 us | 3.1891 us | 239.451 us |  99.6094 | 408.61 KB |
| AddsUpdatesAndFinalization | 1000      | 100       | 345.873 us | 5.4681 us | 4.8473 us | 344.242 us | 151.3672 | 619.52 KB |
| AddsUpdatesAndFinalization | 1000      | 500       | 301.902 us | 3.5411 us | 3.1391 us | 300.495 us | 128.4180 | 525.68 KB |
| AddsUpdatesAndFinalization | 1000      | 1000      | 240.624 us | 2.6498 us | 2.6025 us | 239.769 us |  99.6094 | 408.61 KB |

ToObservableChangeSet_List (After)

| Method                     | itemCount | sizeLimit | Mean         | Error       | StdDev       | Median       | Gen0    | Allocated |
|--------------------------- |---------- |---------- |-------------:|------------:|-------------:|-------------:|--------:|----------:|
| AddsUpdatesAndFinalization | 0         | -1        |     623.4 ns |     9.80 ns |      8.69 ns |     622.2 ns |  0.3223 |   1.32 KB |
| AddsUpdatesAndFinalization | 0         | 0         |     650.0 ns |    11.67 ns |     30.32 ns |     639.9 ns |  0.3309 |   1.35 KB |
| AddsUpdatesAndFinalization | 0         | 1         |     688.4 ns |    15.82 ns |     45.38 ns |     669.8 ns |  0.3386 |   1.38 KB |
| AddsUpdatesAndFinalization | 1         | 1         |     798.6 ns |     9.56 ns |      7.98 ns |     799.7 ns |  0.3729 |   1.52 KB |
| AddsUpdatesAndFinalization | 10        | -1        |   1,907.0 ns |    34.82 ns |     32.57 ns |   1,901.5 ns |  0.6084 |   2.49 KB |
| AddsUpdatesAndFinalization | 10        | 1         |   2,260.1 ns |    21.61 ns |     19.16 ns |   2,256.5 ns |  0.8354 |   3.42 KB |
| AddsUpdatesAndFinalization | 10        | 5         |   2,284.9 ns |    36.09 ns |     32.00 ns |   2,280.9 ns |  0.7706 |   3.16 KB |
| AddsUpdatesAndFinalization | 10        | 10        |   2,131.4 ns |    25.93 ns |     24.25 ns |   2,134.0 ns |  0.6905 |   2.82 KB |
| AddsUpdatesAndFinalization | 100       | -1        |  12,564.9 ns |   242.45 ns |    226.78 ns |  12,515.7 ns |  3.1891 |  13.04 KB |
| AddsUpdatesAndFinalization | 100       | 10        |  20,089.4 ns |   465.09 ns |  1,349.32 ns |  19,689.4 ns |  5.3101 |   21.8 KB |
| AddsUpdatesAndFinalization | 100       | 50        |  16,454.8 ns |   231.03 ns |    204.80 ns |  16,408.3 ns |  4.6692 |  19.15 KB |
| AddsUpdatesAndFinalization | 100       | 100       |  14,790.8 ns |   293.89 ns |    632.64 ns |  14,580.3 ns |  3.8605 |  15.83 KB |
| AddsUpdatesAndFinalization | 1000      | -1        | 119,876.9 ns | 1,871.73 ns |  1,461.32 ns | 119,863.6 ns | 28.8086 | 118.51 KB |
| AddsUpdatesAndFinalization | 1000      | 100       | 204,914.5 ns | 6,514.90 ns | 19,209.33 ns | 202,398.3 ns | 50.2930 | 205.67 KB |
| AddsUpdatesAndFinalization | 1000      | 500       | 187,847.0 ns | 6,420.04 ns | 18,929.64 ns | 182,274.3 ns | 43.7012 | 179.11 KB |
| AddsUpdatesAndFinalization | 1000      | 1000      | 144,470.2 ns | 3,321.84 ns |  9,149.32 ns | 140,413.5 ns | 35.6445 | 145.91 KB |

TL;DR, this shows a solid 50% reduction in runtime and memory usage, for the list operator, and 70% reduction in runtime with a 50% reduction in memory usage, for the cache operator.

Although, the values in the "Allocations" column are suspicious, to me. It says we're allocating 1.39kB for constructing a stream that we then never emit any items into? I dunno how that possibly makes sense, but I'm assuming the numbers are still relevant, in comparison to what they were before.

@glennawatson
Copy link
Member

btw @JakenVeina you are a maintainer now. So you should be able to create branches directly on this repo.

that will allow the test coverage to work.

I need to work out a way of having the test coverage to work on forks.

@JakenVeina
Copy link
Collaborator Author

JakenVeina commented Dec 3, 2023

So you should be able to create branches directly on this repo.

Duly noted, thanks.

Would running coverage on dev branches just be for convenience, or would it actually support the main branch or PRs, in some fashion? Like, I believe I saw recently that coverage runs only periodically on the main branch, and results are cached for comparison against PRs, and that's how the bot reports changes in coverage. Is that accurate? I have my own tooling in VS for running coverage locally, during development, before submitting PRs.

@glennawatson
Copy link
Member

Mostly would allow others to see your code coverage. Swapping from codecov to sonarcloud, sonarcloud uses a secured token to upload results for the coverage. Codecov would upload without the token but the results were all over the place.

@JakenVeina
Copy link
Collaborator Author

sonarcloud uses a secured token to upload results for the coverage.

Looks like the token is actually missing?

The format of the analysis property sonar.token= is invalid

That same error is the one that was failing in the README PR yesterday.

@glennawatson
Copy link
Member

Only missing when running on a fork. If you allowed GitHub action secrets to run on forks it'd be a massive security flaw since people can get access to your tokens.

@JakenVeina
Copy link
Collaborator Author

Ohhhhh, okay, I see what you mean. Somehow I thought it wouldn't count as running on a fork, within the PR build.

@glennawatson
Copy link
Member

I'm trying a new feature that runs results locally after a fork build has finished. Will see how it works.

ChrisPulman
ChrisPulman previously approved these changes Dec 3, 2023
Copy link
Member

@ChrisPulman ChrisPulman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All looks good from my point of view, it's an addition to the existing API and provides fixes to existing issues, thank you.

_source = Observable.Create<IEnumerable<TObject>>(observer =>
{
// Reusable buffer, to avoid allocating per-item
var buffer = new TObject[1];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smart optimisation

private bool _hasSourceCompleted;
private ScheduledExpiration? _scheduledExpiration;

private struct ItemState
Copy link
Collaborator

@RolandPheasant RolandPheasant Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't a struct always be immutable, and preferably read only? It would be interesting to change these as such and run the benchmarks again,

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also a structure should implement equality especially if contained within a collection / dictionary. Otherwise reflection would be used for operations that check equality. Records would come to the rescue here, but alas the 46 framework target prevents that.

My main concern is with ItemState being contained in a dictionary.

Or alternatively, can you be sure that my concerns are ill founded in this case?

Copy link
Collaborator Author

@JakenVeina JakenVeina Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do generally use readonly structs, and definitely records, but since the tests need to run on at least .NET 6, I wasn't able to use all the features I usually do, like required and init. Since I made a PR for those, I was gonna do another pass over this, anyway, depending on who gets merged first.

The pitfall with equality of structs is when using them as a TKey within keyed collections, E.G. Dictionary, usually in the interest of creating a composite key. Like you say, they'll work but really poorly.

As a rudimentary proof, you can do what I just did and toss IEquatable<T> on all those structs and override .Equals() and .GetHashCode() just to call the base methods. Breakpoints show that none of those methods ever get called in any of the tests.

Copy link
Collaborator

@RolandPheasant RolandPheasant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've started a discussion regarding the use of structures and I am interested in your thoughts.

I grabbed the branch locally and whilst complicated, there's a large number of new and existing tests to cover us. The reduction in allocations is impressive,

observer: observer,
scheduler: _scheduler));

private readonly Func<TObject, TimeSpan?>? _expireAfter;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer the fields before the constructor. That way I can see them without have to scroll through a long section of code to see the local state. The same applies to the Subscription class and the equivalent list implementation.

Copy link
Member

@ChrisPulman ChrisPulman Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will create a PR to tighten up the requirements in the editorConfig file, hopefully this will aid the code to follow the expected format.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me personally, I feel the exact opposite. State is the least-likely thing I wanna look at when inspecting a class, I'm probably more interested in seeing its public surface, at a glance.

I'm happy to follow whatever style rules we want to have, though, so long as they're either documented, or enforced by tooling. I'll swap this up.

private bool _hasSourceCompleted;
private ScheduledExpiration? _scheduledExpiration;

private struct ItemState
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also a structure should implement equality especially if contained within a collection / dictionary. Otherwise reflection would be used for operations that check equality. Records would come to the rescue here, but alas the 46 framework target prevents that.

My main concern is with ItemState being contained in a dictionary.

Or alternatively, can you be sure that my concerns are ill founded in this case?

@@ -77,7 +77,7 @@ private IObservable<IChangeSet<TDestination>> RunImpl()
case ListChangeReason.Add:
{
var change = item.Item;
if (change.CurrentIndex < 0 | change.CurrentIndex >= transformed.Count)
if (change.CurrentIndex < 0 || change.CurrentIndex >= transformed.Count)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this the other day, Thanks for fixing,

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spotted that one too, and fixed it along with some formatting.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a moment, I thought that was something I'd written.

@ChrisPulman ChrisPulman dismissed their stale review December 3, 2023 15:45

Further Discussions underway, awaiting outcome.

@glennawatson
Copy link
Member

I've started a discussion regarding the use of structures and I am interested in your thoughts.

I grabbed the branch locally and whilst complicated, there's a large number of new and existing tests to cover us. The reduction in allocations is impressive,

Use 'record struct' instead of struct if immutable. They include the ability to mutate into a new immutable value with the 'with' operator.

@glennawatson
Copy link
Member

Not to mention records and 'record struct' are much more concise by default.

You can use them just like normal structs though.

Eg

public record MyRecord(string Hello);

And

public record MyRecord
{
public string Hello { get; init: }
}

Are the same. The with operator allows you to modify the record/record struct with just selected properties changed in a new immutable value.

var newValue = oldValue with { MyPropery = 123 };

Will create a new value keeping existing property values and only MyProperty as 123

@RolandPheasant
Copy link
Collaborator

@glennawatson regarding records, I have been using them for years. However what I did not know until our conversation on slack was that it only required a polyfill to make them work on Net462.

@JakenVeina has already enabled it here #772, which I just merged,

We're in a happy record enabled space now

@JakenVeina
Copy link
Collaborator Author

We're in a happy record enabled space now

I'm actually not sure if records are good-to-go now, they're quite a bit more involved than just the required and init keywords, which I also don't think are are a requirement for records. I think there may be some other BCL stuff that goes into records, not just codegen. I'll look into it, though, we definitely want to have the option of using records internally, if we can polyfill it.

@RolandPheasant RolandPheasant self-requested a review December 4, 2023 23:04
Copy link
Collaborator

@RolandPheasant RolandPheasant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JakenVeina your call with checking out the structures / records etc. However if you want to merge I've approved, so feel free to do so.

Also I can confirm that we can use records now, I added one into my PR yesterday and it works well.

@glennawatson
Copy link
Member

I wouldn't mind using this PR to see if I can get that coverage tester working on this branch before merge if possible

@JakenVeina
Copy link
Collaborator Author

@RolandPheasant I actually did that yesterday, and then apparently never checked it in.

@glennawatson go for it.

@JakenVeina
Copy link
Collaborator Author

@glennawatson I went ahead and created a dedicated dummy branch for testing the code coverage workflow, so this PR can move forward.

@glennawatson glennawatson merged commit f492247 into reactivemarbles:main Dec 6, 2023
1 check passed
ChrisPulman added a commit that referenced this pull request Dec 6, 2023
src\DynamicData.Tests\Cache\TransformFixtureParallel.cs line 95 - Test Needs fixing or updated Functionality needs checking
RolandPheasant pushed a commit that referenced this pull request Dec 7, 2023
* Strengthen EditorConfig to help enforce coding standards

Update code to match coding standards

* Update ObservableSpy.cs

Fix release version

* Further code fixes after SonarCloud tests

* Fix issues picked up by SonarCloud

* Update new code

* introduce ThrowArgumentNullExceptionIfNull

Reduce code bloat by using extension method for ArgumentNullException's. This was chosen due to current target frameworks. If in the future netstandard2.1 + compliant frameworks are used we can switch methods.

* Update API checks

* Update remaining ArgumentNullException to use extension method

* Fix code after merge

* Update code after merge

* The PR #771 introduced inconsistent results

src\DynamicData.Tests\Cache\TransformFixtureParallel.cs line 95 - Test Needs fixing or updated Functionality needs checking

* Skip removed to ensure fix is applied before merge

Test - src\DynamicData.Tests\Cache\TransformFixtureParallel.cs SameKeyChanges on line 95 needs attention

* Fixed intermittent test failures introduced by 6395e7d

---------

Co-authored-by: JakenVeina <[email protected]>
Copy link

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Dec 21, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: OnCompleted never fires v7.9.*
4 participants