diff --git a/.gitignore b/.gitignore index 0c434ec46cd6ba5a135685a1fb95512a0fa6eaa5..a30bba9d3f57df94b8111121903c1583239515dc 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ coverage.xml *.cover .hypothesis/ /bench/ +/prof/ # Translations *.mo diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 74c0b88a30c60fef2a942cda4f9cbeefec6da5ec..9f81e3673004f7150f09dd64793eea8b09d6ae07 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,6 +40,19 @@ benchmark: paths: - bench +profile: + stage: test + before_script: + - apt-get update -y + - apt-get install -y graphviz + - pip3 install graphviz gprof2dot + script: + - pip3 install --editable .[test] + - ./benchmarks/run_profiling.sh + artifacts: + paths: + - prof + doctest: stage: test script: diff --git a/benchmarks/README.md b/benchmarks/README.md index 93b6c895f3f18a90989010eed2d2af94708b74df..4bcb12d886fafeb1920ff96d0816ef20b7c59066 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -1,10 +1,24 @@ # FINAM benchmarks -Micro-benchmarks for important FINAM functions and functionality. +Micro-benchmarks and profiling for important FINAM runs functions. Note that plot panels have different units! `ms` is milliseconds (1/1,000 second), `us` is microseconds (1/1,000,000 second). +## Full runs + +**Profiling data** for full runs can be found in the latest [job artifacts](https://git.ufz.de/FINAM/finam/-/jobs/artifacts/main/browse/prof?job=profile). + +### Simple link, 365 steps + +Simple run over one year with two coupled components with daily time step. +Left without, right with units conversion. + +The source component assigns time to a data array each step. +Remaining time is data exchange and scheduling (negligible). + + + ## SDK ### Push & pull diff --git a/benchmarks/profiling/simple_run.py b/benchmarks/profiling/simple_run.py new file mode 100644 index 0000000000000000000000000000000000000000..b8ee221d7509674ec9cb0b46ec1044eb34c98ce4 --- /dev/null +++ b/benchmarks/profiling/simple_run.py @@ -0,0 +1,58 @@ +"""Simple coupling setup for profiling. + +Two components, coupled via a single link. + +Simulation runs for 1 year with a daily step in both components. +Components exchange a 128x64 uniform grid. +""" +import datetime as dt + +import finam as fm + + +def run_model(): + start_time = dt.datetime(2000, 1, 1) + end_time = dt.datetime(2000, 12, 31) + + counter = 0 + + size = (128, 64) + + info1 = fm.Info(time=None, grid=fm.UniformGrid(size), units="m") + info2 = fm.Info(time=None, grid=fm.UniformGrid(size), units="m") + data = [ + fm.data.full(0.0, "input", info1, start_time), + fm.data.full(0.0, "input", info1, start_time), + ] + + def gen_data(t): + nonlocal counter + d = data[counter % 2] + counter += 1 + d = fm.data.assign_time(d, t) + return d + + source = fm.modules.CallbackGenerator( + callbacks={"Out": (gen_data, info1.copy())}, + start=start_time, + step=dt.timedelta(days=1), + ) + sink = fm.modules.DebugConsumer( + inputs={ + "In": info2.copy(), + }, + start=start_time, + step=dt.timedelta(days=1), + ) + + composition = fm.Composition([source, sink]) + composition.initialize() + + source["Out"] >> sink["In"] + + composition.run(end_time=end_time) + + +if __name__ == "__main__": + for i in range(10): + run_model() diff --git a/benchmarks/pstats_to_csv.py b/benchmarks/pstats_to_csv.py new file mode 100644 index 0000000000000000000000000000000000000000..d73ac6774d49ea97545836e3f54d70de650f6b8c --- /dev/null +++ b/benchmarks/pstats_to_csv.py @@ -0,0 +1,26 @@ +import io +import os +import pstats + + +def _convert_to_csv(in_path, out_path): + result = io.StringIO() + + pstats.Stats(in_path, stream=result).print_stats() + result = result.getvalue() + result = "ncalls" + result.split("ncalls")[-1] + result = "\n".join( + [",".join(line.rstrip().split(None, 5)) for line in result.split("\n")] + ) + with open(out_path, "w+") as f: + f.write(result) + f.close() + + +if __name__ == "__main__": + path = "prof/" + for file in os.listdir(path): + if file.endswith(".pstats"): + in_file = os.path.join(path, file) + out_file = os.path.join(path, file.replace(".pstats", ".csv")) + _convert_to_csv(in_file, out_file) diff --git a/benchmarks/run/test_run.py b/benchmarks/run/test_run.py new file mode 100644 index 0000000000000000000000000000000000000000..d5391bc4c8388e4a488c6bbc4f0bef78e273eaf3 --- /dev/null +++ b/benchmarks/run/test_run.py @@ -0,0 +1,137 @@ +import datetime as dt +import unittest + +import pytest + +import finam as fm + + +class SimpleRunBase(unittest.TestCase): + def setup(self, benchmark): + self.benchmark = benchmark + self.start_time = dt.datetime(2000, 1, 1) + self.end_time = dt.datetime(2000, 12, 31) + self.counter = 0 + + def gen_data(self, t): + d = self.data[self.counter % 2] + self.counter += 1 + d = fm.data.assign_time(d, t) + return d + + def run_simulation(self): + source = fm.modules.CallbackGenerator( + callbacks={"Out": (self.gen_data, self.info1.copy())}, + start=self.start_time, + step=dt.timedelta(days=1), + ) + sink = fm.modules.DebugConsumer( + inputs={ + "In": self.info2.copy(), + }, + start=self.start_time, + step=dt.timedelta(days=1), + ) + + self.composition = fm.Composition([source, sink]) + self.composition.initialize() + + source["Out"] >> sink["In"] + + self.composition.run(end_time=self.end_time) + + def run_test(self, sx, sy): + self.setup_data(size=(sx, sy)) + self.benchmark(self.run_simulation) + + +class TestSimpleRun(SimpleRunBase): + @pytest.fixture(autouse=True) + def setupBenchmark(self, benchmark): + self.setup(benchmark) + + def setup_data(self, size): + self.info1 = fm.Info(time=None, grid=fm.UniformGrid(size), units="m") + self.info2 = fm.Info(time=None, grid=fm.UniformGrid(size), units="m") + self.data = [ + fm.data.full(0.0, "input", self.info1, self.start_time), + fm.data.full(0.0, "input", self.info1, self.start_time), + ] + + @pytest.mark.benchmark(group="run-sim") + def test_run_simple_01_2x1(self): + self.run_test(2, 1) + + @pytest.mark.benchmark(group="run-sim") + def test_run_simple_02_32x16(self): + self.run_test(32, 16) + + @pytest.mark.benchmark(group="run-sim") + def test_run_simple_03_64x32(self): + self.run_test(64, 32) + + @pytest.mark.benchmark(group="run-sim") + def test_run_simple_04_128x64(self): + self.run_test(128, 64) + + @pytest.mark.benchmark(group="run-sim") + def test_run_simple_05_256x128(self): + self.run_test(256, 128) + + @pytest.mark.benchmark(group="run-sim") + def test_run_simple_06_512x256(self): + self.run_test(512, 256) + + @pytest.mark.benchmark(group="run-sim") + def test_run_simple_07_1024x512(self): + self.run_test(1024, 512) + + @pytest.mark.benchmark(group="run-sim") + def test_run_simple_08_2048x1024(self): + self.run_test(2048, 1024) + + +class TestSimpleRunUnits(SimpleRunBase): + @pytest.fixture(autouse=True) + def setupBenchmark(self, benchmark): + self.setup(benchmark) + + def setup_data(self, size): + self.info1 = fm.Info(time=None, grid=fm.UniformGrid(size), units="m") + self.info2 = fm.Info(time=None, grid=fm.UniformGrid(size), units="km") + self.data = [ + fm.data.full(0.0, "input", self.info1, self.start_time), + fm.data.full(0.0, "input", self.info1, self.start_time), + ] + + @pytest.mark.benchmark(group="run-sim") + def test_run_units_01_2x1(self): + self.run_test(2, 1) + + @pytest.mark.benchmark(group="run-sim") + def test_run_units_02_32x16(self): + self.run_test(32, 16) + + @pytest.mark.benchmark(group="run-sim") + def test_run_units_03_64x32(self): + self.run_test(64, 32) + + @pytest.mark.benchmark(group="run-sim") + def test_run_units_04_128x64(self): + self.run_test(128, 64) + + @pytest.mark.benchmark(group="run-sim") + def test_run_units_05_256x128(self): + self.run_test(256, 128) + + @pytest.mark.benchmark(group="run-sim") + def test_run_units_06_512x256(self): + self.run_test(512, 256) + + @pytest.mark.benchmark(group="run-sim") + def test_run_units_07_1024x512(self): + self.run_test(1024, 512) + + @pytest.mark.benchmark(group="run-sim") + def test_run_units_08_2048x1024(self): + self.run_test(2048, 1024) diff --git a/benchmarks/run_profiling.sh b/benchmarks/run_profiling.sh new file mode 100755 index 0000000000000000000000000000000000000000..3e978005bfd317fcb4fe072247ee28329b23e391 --- /dev/null +++ b/benchmarks/run_profiling.sh @@ -0,0 +1,10 @@ +echo Profiling... + +mkdir -p prof + +python -m cProfile -o prof/simple_run.pstats benchmarks/profiling/simple_run.py +gprof2dot --colour-nodes-by-selftime -f pstats prof/simple_run.pstats > prof/simple_run.dot +dot -Tsvg -o prof/simple_run.svg prof/simple_run.dot +dot -Tpng -o prof/simple_run.png prof/simple_run.dot + +python benchmarks/pstats_to_csv.py