Skip to content

Commit c5e0353

Browse files
Remove unit benchmarks (#5949)
* Create unit_style directory. * Simple lift-and-shift fully unit-style modules. * Consistent import naming. * Restored experimental/ugrid/__init__.py * Remove ARTIFICIAL_DIM_SIZE. * Restore iterate benchmarks to first class. * Lift-and-shift unit-style cube benchmarks. * Add stock.realistic_4d_w_everything(). * Restore lost coverage. * Favour integration-style benchmarks. * What's New entries. * Clearer README phrasing. --------- Co-authored-by: Elias <110238618+ESadek-MO@users.noreply.github.com>
1 parent a56f939 commit c5e0353

21 files changed

Lines changed: 958 additions & 493 deletions

File tree

benchmarks/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ interest. Is set during the benchmark runner `cperf` and `sperf` sub-commands.
6262

6363
[See the ASV docs](https://asv.readthedocs.io/) for full detail.
6464

65+
### What benchmarks to write
66+
67+
It is not possible to maintain a full suite of 'unit style' benchmarks:
68+
69+
* Benchmarks take longer to run than tests.
70+
* Small benchmarks are more vulnerable to noise - they report a lot of false
71+
positive regressions.
72+
73+
We therefore recommend writing benchmarks representing scripts or single
74+
operations that are likely to be run at the user level.
75+
76+
The drawback of this approach: a reported regression is less likely to reveal
77+
the root cause (e.g. if a commit caused a regression in coordinate-creation
78+
time, but the only benchmark covering this was for file-loading). Be prepared
79+
for manual investigations; and consider committing any useful benchmarks as
80+
[on-demand benchmarks](#on-demand-benchmarks) for future developers to use.
81+
6582
### Data generation
6683
**Important:** be sure not to use the benchmarking environment to generate any
6784
test objects/files, as this environment changes with each commit being
@@ -86,6 +103,10 @@ estimate run-time, and these will still be subject to the original problem.
86103

87104
### Scaling / non-Scaling Performance Differences
88105

106+
**(We no longer advocate the below for benchmarks run during CI, given the
107+
limited available runtime and risk of false-positives. It remains useful for
108+
manual investigations).**
109+
89110
When comparing performance between commits/file-type/whatever it can be helpful
90111
to know if the differences exist in scaling or non-scaling parts of the Iris
91112
functionality in question. This can be done using a size parameter, setting

benchmarks/benchmarks/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
from os import environ
88
import resource
99

10-
ARTIFICIAL_DIM_SIZE = int(10e3) # For all artificial cubes, coords etc.
11-
1210

1311
def disable_repeat_between_setup(benchmark_object):
1412
"""Benchmark where object persistence would be inappropriate (decorator).

benchmarks/benchmarks/cube.py

Lines changed: 104 additions & 242 deletions
Original file line numberDiff line numberDiff line change
@@ -4,249 +4,111 @@
44
# See LICENSE in the root of the repository for full licensing details.
55
"""Cube benchmark tests."""
66

7-
import numpy as np
8-
9-
from iris import analysis, aux_factory, coords, cube
10-
11-
from . import ARTIFICIAL_DIM_SIZE, disable_repeat_between_setup
12-
from .generate_data.stock import sample_meshcoord
13-
14-
15-
def setup(*params):
16-
"""General variables needed by multiple benchmark classes."""
17-
global data_1d
18-
global data_2d
19-
global general_cube
20-
21-
data_2d = np.zeros((ARTIFICIAL_DIM_SIZE,) * 2)
22-
data_1d = data_2d[0]
23-
general_cube = cube.Cube(data_2d)
24-
25-
26-
class ComponentCommon:
27-
# TODO: once https://github.com/airspeed-velocity/asv/pull/828 is released:
28-
# * make class an ABC
29-
# * remove NotImplementedError
30-
# * combine setup_common into setup
31-
"""Run a generalised suite of benchmarks for cubes.
32-
33-
A base class running a generalised suite of benchmarks for cubes that
34-
include a specified component (e.g. Coord, CellMeasure etc.). Component to
35-
be specified in a subclass.
36-
37-
ASV will run the benchmarks within this class for any subclasses.
38-
39-
Should only be instantiated within subclasses, but cannot enforce this
40-
since ASV cannot handle classes that include abstract methods.
41-
"""
42-
43-
def setup(self):
44-
"""Prevent ASV instantiating (must therefore override setup() in any subclasses.)."""
45-
raise NotImplementedError
46-
47-
def create(self):
48-
"""Create a cube (generic).
49-
50-
cube_kwargs allow dynamic inclusion of different components;
51-
specified in subclasses.
52-
"""
53-
return cube.Cube(data=data_2d, **self.cube_kwargs)
54-
55-
def setup_common(self):
56-
"""Shared setup code that can be called by subclasses."""
57-
self.cube = self.create()
58-
59-
def time_create(self):
60-
"""Create a cube that includes an instance of the benchmarked component."""
61-
self.create()
62-
63-
def time_add(self):
64-
"""Add an instance of the benchmarked component to an existing cube."""
65-
# Unable to create the copy during setup since this needs to be re-done
66-
# for every repeat of the test (some components disallow duplicates).
67-
general_cube_copy = general_cube.copy(data=data_2d)
68-
self.add_method(general_cube_copy, *self.add_args)
69-
70-
71-
class Cube:
72-
def time_basic(self):
73-
cube.Cube(data_2d)
74-
75-
def time_rename(self):
76-
general_cube.name = "air_temperature"
77-
78-
79-
class AuxCoord(ComponentCommon):
80-
def setup(self):
81-
self.coord_name = "test"
82-
coord_bounds = np.array([data_1d - 1, data_1d + 1]).transpose()
83-
aux_coord = coords.AuxCoord(
84-
long_name=self.coord_name,
85-
points=data_1d,
86-
bounds=coord_bounds,
87-
units="days since 1970-01-01",
88-
climatological=True,
7+
from iris import coords
8+
from iris.cube import Cube
9+
10+
from .generate_data.stock import realistic_4d_w_everything
11+
12+
13+
class CubeCreation:
14+
params = [[False, True], ["instantiate", "construct"]]
15+
param_names = ["Cube has mesh", "Cube creation strategy"]
16+
17+
cube_kwargs: dict
18+
19+
def setup(self, w_mesh: bool, _) -> None:
20+
# Loaded as two cubes due to the hybrid height.
21+
source_cube = realistic_4d_w_everything(w_mesh=w_mesh)
22+
23+
def get_coords_and_dims(
24+
coords_tuple: tuple[coords._DimensionalMetadata, ...],
25+
) -> list[tuple[coords._DimensionalMetadata, tuple[int, ...]]]:
26+
return [(c, c.cube_dims(source_cube)) for c in coords_tuple]
27+
28+
self.cube_kwargs = dict(
29+
data=source_cube.data,
30+
standard_name=source_cube.standard_name,
31+
long_name=source_cube.long_name,
32+
var_name=source_cube.var_name,
33+
units=source_cube.units,
34+
attributes=source_cube.attributes,
35+
cell_methods=source_cube.cell_methods,
36+
dim_coords_and_dims=get_coords_and_dims(source_cube.dim_coords),
37+
aux_coords_and_dims=get_coords_and_dims(source_cube.aux_coords),
38+
aux_factories=source_cube.aux_factories,
39+
cell_measures_and_dims=get_coords_and_dims(source_cube.cell_measures()),
40+
ancillary_variables_and_dims=get_coords_and_dims(
41+
source_cube.ancillary_variables()
42+
),
8943
)
9044

91-
# Variables needed by the ComponentCommon base class.
92-
self.cube_kwargs = {"aux_coords_and_dims": [(aux_coord, 0)]}
93-
self.add_method = cube.Cube.add_aux_coord
94-
self.add_args = (aux_coord, (0))
95-
96-
self.setup_common()
97-
98-
def time_return_coords(self):
99-
self.cube.coords()
100-
101-
def time_return_coord_dims(self):
102-
self.cube.coord_dims(self.coord_name)
103-
104-
105-
class AuxFactory(ComponentCommon):
106-
def setup(self):
107-
coord = coords.AuxCoord(points=data_1d, units="m")
108-
self.hybrid_factory = aux_factory.HybridHeightFactory(delta=coord)
109-
110-
# Variables needed by the ComponentCommon base class.
111-
self.cube_kwargs = {
112-
"aux_coords_and_dims": [(coord, 0)],
113-
"aux_factories": [self.hybrid_factory],
114-
}
115-
116-
self.setup_common()
117-
118-
# Variables needed by the overridden time_add benchmark in this subclass.
119-
cube_w_coord = self.cube.copy()
120-
[cube_w_coord.remove_aux_factory(i) for i in cube_w_coord.aux_factories]
121-
self.cube_w_coord = cube_w_coord
122-
123-
def time_add(self):
124-
# Requires override from super().time_add because the cube needs an
125-
# additional coord.
126-
self.cube_w_coord.add_aux_factory(self.hybrid_factory)
127-
128-
129-
class CellMeasure(ComponentCommon):
130-
def setup(self):
131-
cell_measure = coords.CellMeasure(data_1d)
132-
133-
# Variables needed by the ComponentCommon base class.
134-
self.cube_kwargs = {"cell_measures_and_dims": [(cell_measure, 0)]}
135-
self.add_method = cube.Cube.add_cell_measure
136-
self.add_args = (cell_measure, 0)
137-
138-
self.setup_common()
139-
140-
141-
class CellMethod(ComponentCommon):
142-
def setup(self):
143-
cell_method = coords.CellMethod("test")
144-
145-
# Variables needed by the ComponentCommon base class.
146-
self.cube_kwargs = {"cell_methods": [cell_method]}
147-
self.add_method = cube.Cube.add_cell_method
148-
self.add_args = [cell_method]
149-
150-
self.setup_common()
151-
152-
153-
class AncillaryVariable(ComponentCommon):
154-
def setup(self):
155-
ancillary_variable = coords.AncillaryVariable(data_1d)
156-
157-
# Variables needed by the ComponentCommon base class.
158-
self.cube_kwargs = {"ancillary_variables_and_dims": [(ancillary_variable, 0)]}
159-
self.add_method = cube.Cube.add_ancillary_variable
160-
self.add_args = (ancillary_variable, 0)
161-
162-
self.setup_common()
163-
164-
165-
class MeshCoord:
45+
def time_create(self, _, cube_creation_strategy: str) -> None:
46+
if cube_creation_strategy == "instantiate":
47+
_ = Cube(**self.cube_kwargs)
48+
49+
elif cube_creation_strategy == "construct":
50+
new_cube = Cube(data=self.cube_kwargs["data"])
51+
new_cube.standard_name = self.cube_kwargs["standard_name"]
52+
new_cube.long_name = self.cube_kwargs["long_name"]
53+
new_cube.var_name = self.cube_kwargs["var_name"]
54+
new_cube.units = self.cube_kwargs["units"]
55+
new_cube.attributes = self.cube_kwargs["attributes"]
56+
new_cube.cell_methods = self.cube_kwargs["cell_methods"]
57+
for coord, dims in self.cube_kwargs["dim_coords_and_dims"]:
58+
coord: coords.DimCoord # Type hint to help linters.
59+
new_cube.add_dim_coord(coord, dims)
60+
for coord, dims in self.cube_kwargs["aux_coords_and_dims"]:
61+
new_cube.add_aux_coord(coord, dims)
62+
for aux_factory in self.cube_kwargs["aux_factories"]:
63+
new_cube.add_aux_factory(aux_factory)
64+
for cell_measure, dims in self.cube_kwargs["cell_measures_and_dims"]:
65+
new_cube.add_cell_measure(cell_measure, dims)
66+
for ancillary_variable, dims in self.cube_kwargs[
67+
"ancillary_variables_and_dims"
68+
]:
69+
new_cube.add_ancillary_variable(ancillary_variable, dims)
70+
71+
else:
72+
message = f"Unknown cube creation strategy: {cube_creation_strategy}"
73+
raise NotImplementedError(message)
74+
75+
76+
class CubeEquality:
16677
params = [
167-
6, # minimal cube-sphere
168-
int(1e6), # realistic cube-sphere size
169-
ARTIFICIAL_DIM_SIZE, # To match size in :class:`AuxCoord`
78+
[False, True],
79+
[False, True],
80+
["metadata_inequality", "coord_inequality", "data_inequality", "all_equal"],
17081
]
171-
param_names = ["number of faces"]
172-
173-
def setup(self, n_faces):
174-
mesh_kwargs = dict(n_nodes=n_faces + 2, n_edges=n_faces * 2, n_faces=n_faces)
175-
176-
self.mesh_coord = sample_meshcoord(sample_mesh_kwargs=mesh_kwargs)
177-
self.data = np.zeros(n_faces)
178-
self.cube_blank = cube.Cube(data=self.data)
179-
self.cube = self.create()
180-
181-
def create(self):
182-
return cube.Cube(data=self.data, aux_coords_and_dims=[(self.mesh_coord, 0)])
183-
184-
def time_create(self, n_faces):
185-
_ = self.create()
186-
187-
@disable_repeat_between_setup
188-
def time_add(self, n_faces):
189-
self.cube_blank.add_aux_coord(self.mesh_coord, 0)
190-
191-
@disable_repeat_between_setup
192-
def time_remove(self, n_faces):
193-
self.cube.remove_coord(self.mesh_coord)
194-
195-
196-
class Merge:
197-
def setup(self):
198-
self.cube_list = cube.CubeList()
199-
for i in np.arange(2):
200-
i_cube = general_cube.copy()
201-
i_coord = coords.AuxCoord([i])
202-
i_cube.add_aux_coord(i_coord)
203-
self.cube_list.append(i_cube)
204-
205-
def time_merge(self):
206-
self.cube_list.merge()
207-
208-
209-
class Concatenate:
210-
def setup(self):
211-
dim_size = ARTIFICIAL_DIM_SIZE
212-
self.cube_list = cube.CubeList()
213-
for i in np.arange(dim_size * 2, step=dim_size):
214-
i_cube = general_cube.copy()
215-
i_coord = coords.DimCoord(np.arange(dim_size) + (i * dim_size))
216-
i_cube.add_dim_coord(i_coord, 0)
217-
self.cube_list.append(i_cube)
218-
219-
def time_concatenate(self):
220-
self.cube_list.concatenate()
221-
222-
223-
class Equality:
224-
def setup(self):
225-
self.cube_a = general_cube.copy()
226-
self.cube_b = general_cube.copy()
227-
228-
aux_coord = coords.AuxCoord(data_1d)
229-
self.cube_a.add_aux_coord(aux_coord, 0)
230-
self.cube_b.add_aux_coord(aux_coord, 1)
231-
232-
def time_equality(self):
233-
self.cube_a == self.cube_b
234-
235-
236-
class Aggregation:
237-
def setup(self):
238-
repeat_number = 10
239-
repeat_range = range(int(ARTIFICIAL_DIM_SIZE / repeat_number))
240-
array_repeat = np.repeat(repeat_range, repeat_number)
241-
array_unique = np.arange(len(array_repeat))
242-
243-
coord_repeat = coords.AuxCoord(points=array_repeat, long_name="repeat")
244-
coord_unique = coords.DimCoord(points=array_unique, long_name="unique")
245-
246-
local_cube = general_cube.copy()
247-
local_cube.add_aux_coord(coord_repeat, 0)
248-
local_cube.add_dim_coord(coord_unique, 0)
249-
self.cube = local_cube
250-
251-
def time_aggregated_by(self):
252-
self.cube.aggregated_by("repeat", analysis.MEAN)
82+
param_names = ["Cubes are lazy", "Cubes have meshes", "Scenario"]
83+
84+
cube_1: Cube
85+
cube_2: Cube
86+
coord_name = "surface_altitude"
87+
88+
def setup(self, lazy: bool, w_mesh: bool, scenario: str) -> None:
89+
self.cube_1 = realistic_4d_w_everything(w_mesh=w_mesh, lazy=lazy)
90+
# Using Cube.copy() produces different results due to sharing of the
91+
# Mesh instance.
92+
self.cube_2 = realistic_4d_w_everything(w_mesh=w_mesh, lazy=lazy)
93+
94+
match scenario:
95+
case "metadata_inequality":
96+
self.cube_2.long_name = "different"
97+
case "coord_inequality":
98+
coord = self.cube_2.coord(self.coord_name)
99+
coord.points = coord.core_points() * 2
100+
case "data_inequality":
101+
self.cube_2.data = self.cube_2.core_data() * 2
102+
case "all_equal":
103+
pass
104+
case _:
105+
message = f"Unknown scenario: {scenario}"
106+
raise NotImplementedError(message)
107+
108+
def time_equality(self, lazy: bool, __, ___) -> None:
109+
_ = self.cube_1 == self.cube_2
110+
if lazy:
111+
for cube in (self.cube_1, self.cube_2):
112+
# Confirm that this benchmark is safe for repetition.
113+
assert cube.coord(self.coord_name).has_lazy_points()
114+
assert cube.has_lazy_data()

0 commit comments

Comments
 (0)