-
Notifications
You must be signed in to change notification settings - Fork 37
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
fix: check current session's pending-write queue when recalling snapshots (e.g. diffing) #927
Conversation
…hots (e.g. diffing)
204026a
to
dbf395a
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅ @@ Coverage Diff @@
## main #927 +/- ##
==========================================
+ Coverage 97.75% 97.76% +0.01%
==========================================
Files 21 21
Lines 1604 1613 +9
==========================================
+ Hits 1568 1577 +9
Misses 36 36 |
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.
As always, thank you for Syrupy!
src/syrupy/session.py
Outdated
# find the last (i.e. most recent) write to this index/location in the queue: | ||
for queue_data, queue_test_location, queue_index in reversed(queue): | ||
if queue_index == index and queue_test_location == test_location: | ||
return queue_data |
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.
This loops is a potential performance issue, but I suspect it is okay. In particular: it scales like O(length of queue
), i.e. number of pending writes.
This function is called twice in SnapshotAssertion._assert
:
- unconditionally:
syrupy/src/syrupy/assertion.py
Line 316 in ef8189c
snapshot_data, tainted = self._recall_data(index=self.index) - conditionally, only when diffing:
syrupy/src/syrupy/assertion.py
Line 320 in ef8189c
snapshot_data_diff, _ = self._recall_data(index=snapshot_diff)
If every assertion in a ambr file needs an update, this leads to quadratic O(number of assertions in that file^2) behaviour:
- the first
assert ... == snapshot
has an emptyqueue
, and adds an item - the second checks one element of the queue (doesn't match), and adds an item
- the third checks two elements of the queue (don't match) and adds an item
- ...
- the Nth checks N - 1 elements of the queue (all don't match) and adds an item
i.e. N assertions ends up iterating over queue
.
I think this is probably okay:
- this scales per snapshot location, not across the full testing session, since each
queue
is just for one key, which incorporates the snapshot location (i.e. if one has 1000 assertions, spread evenly across 100 test file (i.e. 10 assertions in each file), this is scaling with N = 10, not N = 1000). - how often does one have to completely re-write a whole file?
- the work here is individually cheap (i.e. the constant factor is low)
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.
There appears to be a slowdown: #927 (comment)
I ran inv benchmark
locally as well. Results for this branch:
------------------------------------------------------------------------------ benchmark: 3 tests ------------------------------------------------------------------------------
Name (time in s) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
test_1000x_reads 1.0756 (1.0) 1.2278 (1.0) 1.1648 (1.0) 0.0630 (4.10) 1.1974 (1.0) 0.0948 (3.49) 2;0 0.8585 (1.0) 5 1
test_standard 1.1904 (1.11) 1.3294 (1.08) 1.2822 (1.10) 0.0534 (3.48) 1.2960 (1.08) 0.0439 (1.62) 1;1 0.7799 (0.91) 5 1
test_1000x_writes 1.3974 (1.30) 1.4278 (1.16) 1.4089 (1.21) 0.0153 (1.0) 1.3986 (1.17) 0.0272 (1.0) 1;0 0.7098 (0.83) 5 1
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
and "main":
------------------------------------------------------------------------------ benchmark: 3 tests ------------------------------------------------------------------------------
Name (time in s) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
test_1000x_reads 1.0264 (1.0) 1.1148 (1.0) 1.0855 (1.0) 0.0362 (1.0) 1.1006 (1.0) 0.0474 (1.0) 1;0 0.9212 (1.0) 5 1
test_standard 1.0904 (1.06) 1.1974 (1.07) 1.1461 (1.06) 0.0477 (1.32) 1.1414 (1.04) 0.0876 (1.85) 2;0 0.8725 (0.95) 5 1
test_1000x_writes 1.0937 (1.07) 1.2011 (1.08) 1.1406 (1.05) 0.0405 (1.12) 1.1313 (1.03) 0.0534 (1.13) 2;0 0.8767 (0.95) 5 1
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The 1000x writes test performs 0.8767
operations per second on main but only 0.7098
operations per second with this change. Roughly 20% slowdown.
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.
this scales per snapshot location,
This comes up when users are leveraging test parameterization over data sets. i.e. the benchmark case of parameterizing over 1000 test cases isn't too unrealistic from how I've seen syrupy used in the past.
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.
FYI, the benchmarking is broken in CI for contributors (something to do with permissions in the github workflow I need to fix), however you should be able to run it locally with inv benchmark
.
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.
Updated. See #927 (comment)
Thanks for proving my "I think" wasn't accurate 😄
self.extension.read_snapshot( | ||
test_location=self.test_location, | ||
index=index, | ||
session_id=str(id(self.session)), |
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 note that #606 added this session_id
parameter, which I imagine was meant to help with circumstances like this, but it seems like it ends up ignored/unused in practice?
- Amber doesn't use
cache_key
: https://github.com/tophat/syrupy/blob/ef8189c68460593fead9c484405b755e272c8cca/src/syrupy/extensions/amber/__init__.py#L57-L66 - Single-file doesn't use
session_id
: https://github.com/tophat/syrupy/blob/ef8189c68460593fead9c484405b755e272c8cca/src/syrupy/extensions/single_file.py#L92-L103
And, in practice, I think it may be quite hard to use, since the relevant cached data is held in memory in the session itself (i.e. the extension _read_snapshot_data_from_location
method calls don't really have access to it at all).
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.
The session_id is used as the cache_key argument in Amber's __cacheable_read_snapshot
method. Although it's not used in the function, it's used by the lru_cache decorator (which caches based on the kwargs I believe). So it essentially invalidates the cache
Benchmark
This comment was automatically generated by workflow using github-action-benchmark. |
Thanks for the review. I'll get back to it when I'm back at work after this holiday period. |
----------------------------------------------------------------------------------- benchmark: 3 tests ----------------------------------------------------------------------------------- Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ test_1000x_reads 666.9710 (1.0) 748.6652 (1.0) 705.2418 (1.0) 37.2862 (1.0) 703.0552 (1.0) 70.1912 (1.07) 2;0 1.4180 (1.0) 5 1 test_standard 669.7840 (1.00) 843.3747 (1.13) 733.8905 (1.04) 68.2257 (1.83) 705.8282 (1.00) 85.6269 (1.30) 1;0 1.3626 (0.96) 5 1 test_1000x_writes 793.8229 (1.19) 937.1953 (1.25) 850.9716 (1.21) 54.4067 (1.46) 847.3260 (1.21) 65.9041 (1.0) 2;0 1.1751 (0.83) 5 1 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- test_1000x_reads 625.5781 (1.0) 887.4346 (1.0) 694.6221 (1.0) 109.0048 (1.0) 658.3128 (1.0) 87.7517 (1.0) 1;1 1.4396 (1.0) 5 1 test_1000x_writes 637.3099 (1.02) 1,021.0924 (1.15) 812.9789 (1.17) 150.2342 (1.38) 757.7635 (1.15) 215.9572 (2.46) 2;0 1.2300 (0.85) 5 1 test_standard 694.1814 (1.11) 1,037.9224 (1.17) 845.1463 (1.22) 136.2068 (1.25) 785.6973 (1.19) 194.9636 (2.22) 2;0 1.1832 (0.82) 5 1 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
I've updated this to improve performance and address that concern. This required making I've done this work across many independent commits to hopefully make review easier. Benchmarks on my machine, reporting the mean time ± std-dev:
In summary, I think:
Expand for the full detailsBefore (
Initial version of this PR:
Current version of this PR:
|
"modulename", | ||
".".join([*test_relpath.parent.parts, test_relpath.stem]), | ||
) | ||
object.__setattr__(self, "methodname", None) |
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.
Setting this attribute is newly added: it previously wasn't being set on this codepath, and attempting to hash/compare the location values (to put into the queue dictionary) was blowing up when accessing it.
It seems like it was previously not read at all for doc tests?
🎉 This PR is included in version 4.8.1 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
Thank you! |
Description
This PR fixes
assert ... == snapshot(diff=...)
mode to work better in two scenarios:In particular, it seems like the nice performance optimisation of #606, meant the "recalling" used for diffing wasn't finding newly created or updated data, and instead just reading whatever snapshot is available on disk.
This PR resolves the problem by having the recalling managed via the
SnapshotSession
, which can thus check its write queue before reading disk.Related Issues
Checklist
Additional Comments
No additional comments.