-
Notifications
You must be signed in to change notification settings - Fork 36
Make AnalyzedSeries change points lazy properties #169
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 |
|---|---|---|
|
|
@@ -188,7 +188,8 @@ def test_change_point_detection_performance(): | |
| data={"series": series}, | ||
| attributes={}, | ||
| ) | ||
| test.analyze() | ||
| # access the results so the timing covers detection, not just construction | ||
| test.analyze().change_points_by_time | ||
| end_time = time.process_time() | ||
| assert (end_time - start_time) < 0.5 | ||
|
|
||
|
|
@@ -472,22 +473,6 @@ def test_validate(): | |
| data={"series1": series_1, "series2": series_2}, | ||
| attributes={}, | ||
| ) | ||
| test_fail = Series( | ||
| "test", | ||
| branch=None, | ||
| time=time, | ||
| metrics={"series1": Metric(1, 1.0), "series2": Metric(1, 1.0)}, | ||
| data={"series1": series_1, "series2": series_2}, | ||
| attributes={}, | ||
| ) | ||
|
|
||
| analyzed_series_fail = test_fail.analyze() | ||
| analyzed_series_fail.change_points = None | ||
| err = analyzed_series_fail._validate_append( | ||
| time=[len(time)], new_data={"series1": [0.51]}, attributes={} | ||
| ) | ||
| assert isinstance(err, RuntimeError) | ||
|
|
||
| analyzed_series = test.analyze() | ||
| analyzed_series.append( | ||
| time=[len(time)], new_data={"series1": [0.5], "series2": [1.97]}, attributes={} | ||
|
|
@@ -599,3 +584,188 @@ def test_series_raw_initialization(): | |
|
|
||
| assert len(series.time) == 3 | ||
| assert series.data["throughput"] == [10.0, 12.0, 11.5] | ||
|
|
||
|
|
||
| def test_change_points_computed_lazily_and_cached(monkeypatch): | ||
| from otava import series as series_module | ||
|
|
||
| calls = {"count": 0} | ||
| real_compute = series_module.compute_change_points | ||
|
|
||
| def counting_compute(*args, **kwargs): | ||
| calls["count"] += 1 | ||
| return real_compute(*args, **kwargs) | ||
|
|
||
| monkeypatch.setattr(series_module, "compute_change_points", counting_compute) | ||
|
|
||
| data = [1.0] * 10 + [5.0] * 10 | ||
| test = Series( | ||
| "lazy_test", | ||
| branch=None, | ||
| time=list(range(len(data))), | ||
| metrics={"m1": Metric(1, 1.0), "m2": Metric(1, 1.0)}, | ||
| data={"m1": data, "m2": data.copy()}, | ||
| attributes={}, | ||
| ) | ||
|
|
||
| analyzed = test.analyze() | ||
| assert calls["count"] == 0 | ||
|
Comment on lines
+611
to
+612
|
||
|
|
||
| change_points = analyzed.change_points | ||
| assert calls["count"] == 2 # one computation per metric | ||
| assert [c.index for c in change_points.get_change_points_for_metric("m1")] == [10] | ||
|
|
||
| assert analyzed.change_points is change_points | ||
| assert len(analyzed.change_points_by_time) == 1 | ||
| assert analyzed.weak_change_points is not None | ||
| assert analyzed.change_points_timestamp is not None | ||
| assert calls["count"] == 2 | ||
|
|
||
|
|
||
| def test_append_on_stable_series(monkeypatch): | ||
| from otava import series as series_module | ||
|
|
||
| calls = {"count": 0} | ||
| real_compute = series_module.compute_change_points | ||
|
|
||
| def counting_compute(*args, **kwargs): | ||
| calls["count"] += 1 | ||
| return real_compute(*args, **kwargs) | ||
|
|
||
| monkeypatch.setattr(series_module, "compute_change_points", counting_compute) | ||
|
|
||
| stable = [1.0] * 20 | ||
| test = Series( | ||
| "stable_test", | ||
| branch=None, | ||
| time=list(range(len(stable))), | ||
| metrics={"m1": Metric(1, 1.0)}, | ||
| data={"m1": stable}, | ||
| attributes={}, | ||
| ) | ||
|
|
||
| analyzed = test.analyze() | ||
| assert calls["count"] == 0 | ||
|
|
||
| # a stable series has no change points, which must not be mistaken for "not computed yet" | ||
| assert analyzed.can_append(time=[len(stable)], new_data={"m1": [1.0]}, attributes={}) | ||
| assert calls["count"] == 1 | ||
| assert len(list(analyzed.change_points)) == 0 | ||
|
|
||
| analyzed.append(time=[len(stable)], new_data={"m1": [1.0]}, attributes={}) | ||
| assert len(list(analyzed.change_points)) == 0 | ||
|
|
||
|
|
||
| def test_append_invalidates_by_time_view(monkeypatch): | ||
| data = [1.0] * 10 + [5.0] * 10 | ||
| test = Series( | ||
| "invalidation_test", | ||
| branch=None, | ||
| time=list(range(len(data))), | ||
| metrics={"m1": Metric(1, 1.0)}, | ||
| data={"m1": data}, | ||
| attributes={}, | ||
| ) | ||
|
|
||
| analyzed = test.analyze() | ||
| assert [cpg.time for cpg in analyzed.change_points_by_time] == [10] | ||
|
|
||
| # a second shift, so the by-time view must change after the append | ||
| analyzed.append( | ||
| time=list(range(20, 32)), new_data={"m1": [50.0] * 12}, attributes={} | ||
| ) | ||
|
|
||
| assert [cpg.time for cpg in analyzed.change_points_by_time] == [10, 20] | ||
|
|
||
|
|
||
| def test_append_does_not_rebuild_unread_by_time_view(monkeypatch): | ||
| from otava.change_point_divisive import base as base_module | ||
|
|
||
| data = [1.0] * 10 + [5.0] * 10 | ||
| test = Series( | ||
| "no_rebuild_test", | ||
| branch=None, | ||
| time=list(range(len(data))), | ||
| metrics={"m1": Metric(1, 1.0)}, | ||
| data={"m1": data}, | ||
| attributes={}, | ||
| ) | ||
|
|
||
| analyzed = test.analyze() | ||
| calls = {"count": 0} | ||
| real_by_time = base_module.ChangePointsByMetric.by_time | ||
|
|
||
| def counting_by_time(self, *args, **kwargs): | ||
| calls["count"] += 1 | ||
| return real_by_time(self, *args, **kwargs) | ||
|
|
||
| monkeypatch.setattr(base_module.ChangePointsByMetric, "by_time", counting_by_time) | ||
|
|
||
| analyzed.append(time=[len(data)], new_data={"m1": [5.0]}, attributes={}) | ||
| assert calls["count"] == 0 | ||
|
|
||
| _ = analyzed.change_points_by_time | ||
| assert calls["count"] == 1 | ||
|
|
||
|
|
||
| def test_append_refreshes_change_points_timestamp(): | ||
| data = [1.0] * 10 + [5.0] * 10 | ||
| test = Series( | ||
| "timestamp_test", | ||
| branch=None, | ||
| time=list(range(len(data))), | ||
| metrics={"m1": Metric(1, 1.0)}, | ||
| data={"m1": data}, | ||
| attributes={}, | ||
| ) | ||
|
|
||
| analyzed = test.analyze() | ||
| before = analyzed.change_points_timestamp | ||
|
|
||
| analyzed.append(time=[len(data)], new_data={"m1": [5.0]}, attributes={}) | ||
|
|
||
| assert analyzed.change_points_timestamp > before | ||
|
|
||
|
|
||
| def test_append_computes_change_points_first(): | ||
| data = [1.0] * 10 + [5.0] * 10 | ||
| test = Series( | ||
| "append_lazy_test", | ||
| branch=None, | ||
| time=list(range(len(data))), | ||
| metrics={"m1": Metric(1, 1.0)}, | ||
| data={"m1": data}, | ||
| attributes={}, | ||
| ) | ||
|
|
||
| analyzed = test.analyze() | ||
| analyzed.append(time=[len(data)], new_data={"m1": [5.0]}, attributes={}) | ||
|
Contributor
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. This only covers a series with an existing change point. A stable series computes an empty result, which |
||
|
|
||
| assert [c.index for c in analyzed.change_points.get_change_points_for_metric("m1")] == [10] | ||
| assert len(analyzed.change_points_by_time) == 1 | ||
|
|
||
|
|
||
| def test_from_json_does_not_recompute(monkeypatch): | ||
| from otava import series as series_module | ||
|
|
||
| data = [1.0] * 10 + [5.0] * 10 | ||
| test = Series( | ||
| "roundtrip_lazy_test", | ||
| branch=None, | ||
| time=list(range(len(data))), | ||
| metrics={"m1": Metric(1, 1.0)}, | ||
| data={"m1": data}, | ||
| attributes={}, | ||
| ) | ||
| analyzed = test.analyze() | ||
| payload = analyzed.to_json() | ||
|
|
||
| def fail_compute(*args, **kwargs): | ||
| raise AssertionError("a deserialized series must not recompute change points") | ||
|
|
||
| monkeypatch.setattr(series_module, "compute_change_points", fail_compute) | ||
|
|
||
| restored = AnalyzedSeries.from_json(payload) | ||
| assert restored.change_points_timestamp == analyzed.change_points_timestamp | ||
| assert [c.index for c in restored.change_points.get_change_points_for_metric("m1")] == [10] | ||
| assert len(restored.change_points_by_time) == len(analyzed.change_points_by_time) | ||
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.
logging.info("Computing change points...from line 128 should move here.