-
Notifications
You must be signed in to change notification settings - Fork 41
Measure what the lineage ids cost, and fix the tool that measures #959
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,9 @@ | |
| import pytest | ||
|
|
||
| import dascore as dc | ||
| from dascore.config import config_context | ||
| from dascore.utils.patch import get_start_stop_step | ||
| from dascore.workflow.processor import _FINGERPRINTS | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
|
|
@@ -428,3 +430,99 @@ def test_align_1d_shift_valid(self, patch_2d_with_1d_shift): | |
| """Benchmark 2D patch with 1D shift coordinate (300 shifts), valid mode.""" | ||
| patch = patch_2d_with_1d_shift | ||
| patch.align_to_coord(time="shift_time", mode="valid") | ||
|
|
||
|
|
||
| class TestIdentityOverhead: | ||
| """ | ||
| What maintaining the lineage ids costs. | ||
|
|
||
| The charge is per operation -- canonicalizing the call and digesting | ||
| it -- so it is invisible next to real signal processing and plain | ||
| next to an operation which barely touches the data. Both ends are | ||
| timed, because it is the cheap end which decides whether the | ||
| `patch_provenance` knob is worth keeping. | ||
|
|
||
| Repeating one call is the cheap case: `fingerprint_call` memoizes, so | ||
| the second identical call pays the lookup and not the digest. Real | ||
| loops vary their arguments, so the uncached case is timed too. | ||
| """ | ||
|
|
||
| @pytest.fixture(scope="class") | ||
| def tiny_patch(self): | ||
| """The smallest patch worth having: all overhead, no work.""" | ||
| return dc.Patch( | ||
| data=np.ones((2, 2)), | ||
| coords={"distance": np.arange(2), "time": np.arange(2)}, | ||
| dims=("distance", "time"), | ||
| ) | ||
|
|
||
| @pytest.fixture(scope="class") | ||
| def big_mask(self, example_patch): | ||
| """A mask the fingerprint has to hash, being an array parameter.""" | ||
| return np.asarray(example_patch.data) > 0.5 | ||
|
|
||
| @pytest.fixture() | ||
| def ids_disabled(self): | ||
| """ | ||
| Turn the ids off around a benchmark, not inside it. | ||
|
|
||
| Entering the context builds and validates a whole config, which | ||
| is not what the control is supposed to be measuring. | ||
| """ | ||
| with config_context(patch_provenance="disabled"): | ||
| yield | ||
|
|
||
| @pytest.mark.benchmark | ||
| def test_identity_overhead_tiny_patch(self, tiny_patch): | ||
| """The charge on a call which does nothing else, memoized.""" | ||
| tiny_patch.transpose() | ||
|
|
||
| @pytest.mark.benchmark | ||
| def test_identity_overhead_tiny_patch_disabled(self, tiny_patch, ids_disabled): | ||
| """The same call with the ids off, as the control.""" | ||
| tiny_patch.transpose() | ||
|
|
||
| @pytest.mark.benchmark | ||
| def test_identity_overhead_uncached(self, tiny_patch): | ||
| """ | ||
| The charge with the memo missed, which is what a real loop pays. | ||
|
|
||
| The cache is cleared rather than the arguments varied, so this | ||
| times the same call as the memoized benchmark above and the two | ||
| differ by the digest alone. | ||
| """ | ||
| _FINGERPRINTS.clear() | ||
| tiny_patch.transpose() | ||
|
|
||
| @pytest.mark.benchmark | ||
| def test_identity_overhead_uncached_disabled(self, tiny_patch, ids_disabled): | ||
| """The control for the uncached charge, clearing included.""" | ||
| _FINGERPRINTS.clear() | ||
| tiny_patch.transpose() | ||
|
|
||
| @pytest.mark.benchmark | ||
| def test_identity_overhead_array_argument(self, example_patch, big_mask): | ||
| """An array parameter is hashed, which is the one unflat cost.""" | ||
| example_patch.where(big_mask) | ||
|
|
||
| @pytest.mark.benchmark | ||
| def test_identity_overhead_array_argument_disabled( | ||
| self, example_patch, big_mask, ids_disabled | ||
| ): | ||
| """The control: the same call without hashing the mask.""" | ||
| example_patch.where(big_mask) | ||
|
Comment on lines
+503
to
+513
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For the large-mask scenario, this benchmark records only the total cost of Useful? React with 👍 / 👎. |
||
|
|
||
| @pytest.mark.benchmark | ||
| def test_identity_overhead_real_work(self, example_patch): | ||
| """Next to actual filtering the charge should not be findable.""" | ||
| example_patch.pass_filter(time=(10, 100)) | ||
|
|
||
| @pytest.mark.benchmark | ||
| def test_identity_overhead_real_work_disabled(self, example_patch, ids_disabled): | ||
| """The control for real work.""" | ||
| example_patch.pass_filter(time=(10, 100)) | ||
|
|
||
| @pytest.mark.benchmark | ||
| def test_processor_fingerprint(self, example_patch): | ||
| """Building an operation and asking it what it is.""" | ||
| dc.proc.normalize.op(dim="time").fingerprint | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -319,6 +319,29 @@ The path is part of the id, so a derived id is not stable across machines. A sto | |
|
|
||
| An operation which hands the patch straight back did nothing, and records nothing. Neither id is part of `Patch.equals`: two patches holding the same data are equal however they were made. | ||
|
|
||
| ### What builds a patch, and what operates on one | ||
|
|
||
| `new`, `update` and `update_attrs` carry both ids through, unless you name one and set it yourself. They are not operations — they are how a patch function assembles its own result — so stamping there would count every operation twice. | ||
|
|
||
| That has a consequence worth knowing: changing data through `new` yourself leaves the ids saying the data is unchanged and the route is the same one. | ||
|
|
||
| ```{python} | ||
| doubled = patch.new(data=patch.data * 2) | ||
|
|
||
| # It says it is the same data by the same route, because nothing told it otherwise. | ||
| assert doubled.attrs.patch_id == patch.attrs.patch_id | ||
| assert doubled.attrs.processing_id == patch.attrs.processing_id | ||
| ``` | ||
|
|
||
| The ids describe what DASCore was asked to do. Work done through [patch functions](processing.qmd) is described; work done by reaching past them is not. Building a patch from arrays rather than from another patch is the honest case, and mints a new `patch_id`: | ||
|
|
||
| ```{python} | ||
| import numpy as np | ||
|
|
||
| built = dc.Patch(data=np.asarray(patch.data), coords=patch.coords, dims=patch.dims) | ||
| assert built.attrs.patch_id != patch.attrs.patch_id | ||
|
Comment on lines
+341
to
+342
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a user follows this example as the suggested alternative to Useful? React with 👍 / 👎. |
||
| ``` | ||
|
|
||
| Set `patch_provenance="disabled"` to stop maintaining them: | ||
|
|
||
| ```{python} | ||
|
|
||
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.
When this benchmark runs after any code that has already made the same argument-free
transpose()call,fingerprint_calluses the process-wide_FINGERPRINTScache, whereas a standalone or differently ordered run may pay the initial binding and digest cost. Because that state change is comparable to the small overhead being measured, the recorded result can alternate between cold- and warm-cache behavior based on test order; warm the exact call or reset the cache in fixture setup so the intended state is explicit and outside the timed body.Useful? React with 👍 / 👎.