diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1f46c326..577a19a0 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -4,4 +4,5 @@ |-----------------|------------------|-------------|------------| | andrewcoughtrie | Andrew Coughtrie | Met Office | 2026-03-05 | | mo-marqh | mark Hedley | Met Office | 2026-03-10 | -| oakleybrunt | Oakley Brunt | Met Office | 2026-03-09 | \ No newline at end of file +| oakleybrunt | Oakley Brunt | Met Office | 2026-03-09 | +| EdHone | Ed Hone | Met Office | 2026-03-20 | \ No newline at end of file diff --git a/post-processing/tests/data/vernier-output-test b/post-processing/tests/data/vernier-output-test index 29bc4c68..de812bf2 100644 --- a/post-processing/tests/data/vernier-output-test +++ b/post-processing/tests/data/vernier-output-test @@ -6,8 +6,8 @@ Profiling on 1 thread(s). (Size; Size/sec; Size/call; MinSize; MaxSize) (self) (sec) (sec) (sec) ms/call ms/call - 1 44.130 2.583 2.583 5.854 1 2583.167 5853.527 __test_app__ - 2 34.563 4.606 2.023 2.077 2 1011.582 1038.697 some_process + 1 44.130 2.583 2.583 5.854 1 2583.167 5853.527 __test_app__@0 + 2 34.563 4.606 2.023 2.077 2 1011.582 1038.697 some_process@0 Task 2 of 2 : MPI rank ID 1 Profiling on 1 thread(s). @@ -16,5 +16,5 @@ Profiling on 1 thread(s). (Size; Size/sec; Size/call; MinSize; MaxSize) (self) (sec) (sec) (sec) ms/call ms/call - 1 43.991 3.009 3.009 3.069 2 1504.362 1534.740 some_process - 2 37.835 5.596 2.588 6.839 1 2587.638 6839.336 __test_app__ + 1 43.991 3.009 3.009 3.069 2 1504.362 1534.740 some_process@0 + 2 37.835 5.596 2.588 6.839 1 2587.638 6839.336 __test_app__@0 diff --git a/post-processing/tests/data/vernier-output-threaded/vernier-output-0 b/post-processing/tests/data/vernier-output-threaded/vernier-output-0 new file mode 100644 index 00000000..8bb75d87 --- /dev/null +++ b/post-processing/tests/data/vernier-output-threaded/vernier-output-0 @@ -0,0 +1,21 @@ + +Task 1 of 2 : MPI rank ID 0 +Profiling on 4 thread(s). + + # % Time Cumul Self Total # of calls Self Total Routine@ + (Size; Size/sec; Size/call; MinSize; MaxSize) + (self) (sec) (sec) (sec) ms/call ms/call + + 1 40.000 2.000 2.000 3.000 2 1000.075 1500.115 MAIN_SUB@0 + 2 40.000 4.000 2.000 5.000 1 2000.140 5000.373 FULL@0 + 3 20.000 5.000 1.000 1.000 1 1000.073 1000.073 MAIN_SUB2@3 + 4 20.000 6.000 1.000 2.000 1 1000.072 2000.152 MAIN_SUB@3 + 5 20.000 7.001 1.000 1.000 1 1000.072 1000.072 MAIN_SUB2@0 + 6 20.000 8.001 1.000 1.000 1 1000.071 1000.071 MAIN_SUB2@2 + 7 20.000 9.001 1.000 1.000 1 1000.069 1000.069 MAIN_SUB2@1 + 8 20.000 10.001 1.000 2.000 1 1000.068 2000.151 MAIN_SUB@2 + 9 20.000 11.001 1.000 2.000 1 1000.067 2000.150 MAIN_SUB@1 + 10 0.000 11.001 0.000 0.000 2 0.008 0.008 __vernier__@2 + 11 0.000 11.001 0.000 0.000 4 0.004 0.004 __vernier__@0 + 12 0.000 11.001 0.000 0.000 2 0.008 0.008 __vernier__@1 + 13 0.000 11.001 0.000 0.000 2 0.007 0.007 __vernier__@3 \ No newline at end of file diff --git a/post-processing/tests/data/vernier-output-threaded/vernier-output-1 b/post-processing/tests/data/vernier-output-threaded/vernier-output-1 new file mode 100644 index 00000000..d6a13a80 --- /dev/null +++ b/post-processing/tests/data/vernier-output-threaded/vernier-output-1 @@ -0,0 +1,21 @@ + +Task 2 of 2 : MPI rank ID 1 +Profiling on 4 thread(s). + + # % Time Cumul Self Total # of calls Self Total Routine@ + (Size; Size/sec; Size/call; MinSize; MaxSize) + (self) (sec) (sec) (sec) ms/call ms/call + + 1 40.031 2.004 2.004 5.006 1 2003.948 5005.940 FULL@0 + 2 39.956 4.004 2.000 3.002 2 1000.084 1500.991 MAIN_SUB@0 + 3 20.056 5.008 1.004 1.004 1 1003.995 1003.995 MAIN_SUB2@1 + 4 20.017 6.010 1.002 1.002 1 1002.034 1002.034 MAIN_SUB2@3 + 5 20.012 7.012 1.002 1.002 1 1001.809 1001.809 MAIN_SUB2@0 + 6 19.979 8.012 1.000 1.000 1 1000.149 1000.149 MAIN_SUB2@2 + 7 19.978 9.012 1.000 2.004 1 1000.063 2004.060 MAIN_SUB@1 + 8 19.978 10.012 1.000 2.002 1 1000.063 2002.101 MAIN_SUB@3 + 9 19.977 11.012 1.000 2.000 1 1000.038 2000.191 MAIN_SUB@2 + 10 0.002 11.012 0.000 0.000 2 0.052 0.052 __vernier__@2 + 11 0.002 11.012 0.000 0.000 2 0.052 0.052 __vernier__@1 + 12 0.002 11.013 0.000 0.000 2 0.052 0.052 __vernier__@3 + 13 0.001 11.013 0.000 0.000 4 0.007 0.007 __vernier__@0 \ No newline at end of file diff --git a/post-processing/tests/data/vernier-output/vernier-output-test-0 b/post-processing/tests/data/vernier-output/vernier-output-test-0 index 8a4d3d78..64d1fc79 100644 --- a/post-processing/tests/data/vernier-output/vernier-output-test-0 +++ b/post-processing/tests/data/vernier-output/vernier-output-test-0 @@ -6,5 +6,5 @@ Profiling on 1 thread(s). (Size; Size/sec; Size/call; MinSize; MaxSize) (self) (sec) (sec) (sec) ms/call ms/call - 1 44.130 2.583 2.583 5.854 1 2583.167 5853.527 __test_app__ - 2 34.563 4.606 2.023 2.077 2 1011.582 1038.697 some_process + 1 44.130 2.583 2.583 5.854 1 2583.167 5853.527 __test_app__@0 + 2 34.563 4.606 2.023 2.077 2 1011.582 1038.697 some_process@0 diff --git a/post-processing/tests/data/vernier-output/vernier-output-test-1 b/post-processing/tests/data/vernier-output/vernier-output-test-1 index 11292d19..d7ffaa91 100644 --- a/post-processing/tests/data/vernier-output/vernier-output-test-1 +++ b/post-processing/tests/data/vernier-output/vernier-output-test-1 @@ -6,5 +6,5 @@ Profiling on 1 thread(s). (Size; Size/sec; Size/call; MinSize; MaxSize) (self) (sec) (sec) (sec) ms/call ms/call - 1 43.991 3.009 3.009 3.069 2 1504.362 1534.740 some_process - 2 37.835 5.596 2.588 6.839 1 2587.638 6839.336 __test_app__ + 1 43.991 3.009 3.009 3.069 2 1504.362 1534.740 some_process@0 + 2 37.835 5.596 2.588 6.839 1 2587.638 6839.336 __test_app__@0 diff --git a/post-processing/tests/test_cli_tools.py b/post-processing/tests/test_cli_tools.py index 4607cd18..66b477d2 100644 --- a/post-processing/tests/test_cli_tools.py +++ b/post-processing/tests/test_cli_tools.py @@ -23,9 +23,9 @@ def setUp(self): self.test_data_dir = Path(__file__).parent / 'data' # pylint: disable=line-too-long self.test_data_kgo = ( - '| Routine | Total time (s) | Self (s) | Cumul time (s) | No. calls | % time | Time per call (s) |\n' + - '| __test_app__ | 6.3465 | 2.5855 | 4.0895 | 1 | 40.9825 | 6.3465 |\n' + - '| some_process | 2.573 | 2.516 | 3.8075 | 2 | 39.277 | 1.2865 |\n' + '| Routine | Total time (s) | Self (s) | Cumul time (s) | Max no. calls | % time | Time per call (s) |\n' + + '| __test_app__ | 6.3465 | 2.5855 | 4.0895 | 1 | 40.9825 | 6.3465 |\n' + + '| some_process | 2.573 | 2.516 | 3.8075 | 2 | 39.277 | 1.2865 |\n' ) return super().setUp() diff --git a/post-processing/tests/test_vernier_data.py b/post-processing/tests/test_vernier_data.py index 30164698..3735152b 100644 --- a/post-processing/tests/test_vernier_data.py +++ b/post-processing/tests/test_vernier_data.py @@ -39,6 +39,89 @@ def test_add_empty_calliper(self): self.assertEqual(self.test_data.data["test_calliper"].self_time, []) self.assertEqual(self.test_data.data["test_calliper"].total_time, []) self.assertEqual(self.test_data.data["test_calliper"].n_calls, []) + self.assertEqual(self.test_data.data["test_calliper"].rank, []) + self.assertEqual(self.test_data.data["test_calliper"].thread, []) + + def test_get(self): + """ + Tests that the get method of VernierData returns the expected data. + """ + self.test_data.add_calliper("test_calliper") + self.test_data.data["test_calliper"].time_percent = [10.0, 20.0] + self.test_data.data["test_calliper"].cumul_time = [30.0, 40.0] + self.test_data.data["test_calliper"].self_time = [5.0, 15.0] + self.test_data.data["test_calliper"].total_time = [25.0, 35.0] + self.test_data.data["test_calliper"].n_calls = [2, 2] + self.test_data.data["test_calliper"].rank = [0, 1] + self.test_data.data["test_calliper"].thread = [0, 0] + calliper_data = self.test_data.get("test_calliper") + self.assertEqual(calliper_data.time_percent, [10.0, 20.0]) + self.assertEqual(calliper_data.cumul_time, [30.0, 40.0]) + self.assertEqual(calliper_data.self_time, [5.0, 15.0]) + self.assertEqual(calliper_data.total_time, [25.0, 35.0]) + self.assertEqual(calliper_data.n_calls, [2, 2]) + self.assertEqual(calliper_data.rank, [0, 1]) + self.assertEqual(calliper_data.thread, [0, 0]) + + def test_get_rank(self): + """ + Tests getter for data from a single rank + """ + self.test_data.add_calliper("test_calliper") + self.test_data.data["test_calliper"].time_percent = [10.0, 20.0] + self.test_data.data["test_calliper"].cumul_time = [30.0, 40.0] + self.test_data.data["test_calliper"].self_time = [5.0, 15.0] + self.test_data.data["test_calliper"].total_time = [25.0, 35.0] + self.test_data.data["test_calliper"].n_calls = [2, 2] + self.test_data.data["test_calliper"].rank = [0, 1] + self.test_data.data["test_calliper"].thread = [0, 0] + calliper_data = self.test_data.get("test_calliper", rank=1) + self.assertEqual(calliper_data.time_percent, [20.0]) + self.assertEqual(calliper_data.cumul_time, [40.0]) + self.assertEqual(calliper_data.self_time, [15.0]) + self.assertEqual(calliper_data.total_time, [35.0]) + self.assertEqual(calliper_data.n_calls, [2]) + self.assertEqual(calliper_data.rank, [1]) + self.assertEqual(calliper_data.thread, [0]) + + def test_get_thread(self): + """ + Tests getter for data from a single thread + """ + self.test_data.add_calliper("test_calliper") + self.test_data.data["test_calliper"].time_percent = [10.0, 20.0] + self.test_data.data["test_calliper"].cumul_time = [30.0, 40.0] + self.test_data.data["test_calliper"].self_time = [5.0, 15.0] + self.test_data.data["test_calliper"].total_time = [25.0, 35.0] + self.test_data.data["test_calliper"].n_calls = [2, 2] + self.test_data.data["test_calliper"].rank = [0, 0] + self.test_data.data["test_calliper"].thread = [0, 1] + calliper_data = self.test_data.get("test_calliper", thread=1) + self.assertEqual(calliper_data.time_percent, [20.0]) + self.assertEqual(calliper_data.cumul_time, [40.0]) + self.assertEqual(calliper_data.self_time, [15.0]) + self.assertEqual(calliper_data.total_time, [35.0]) + self.assertEqual(calliper_data.n_calls, [2]) + self.assertEqual(calliper_data.rank, [0]) + self.assertEqual(calliper_data.thread, [1]) + + def test_get_rank_and_thread(self): + self.test_data.add_calliper("test_calliper") + self.test_data.data["test_calliper"].time_percent = [10.0, 20.0, 10.5, 20.5] + self.test_data.data["test_calliper"].cumul_time = [30.0, 40.0, 30.5, 40.5] + self.test_data.data["test_calliper"].self_time = [5.0, 15.0, 5.5, 15.5] + self.test_data.data["test_calliper"].total_time = [25.0, 35.0, 25.5, 35.5] + self.test_data.data["test_calliper"].n_calls = [2, 2, 2, 2] + self.test_data.data["test_calliper"].rank = [0, 0, 1, 1] + self.test_data.data["test_calliper"].thread = [0, 1, 0, 1] + calliper_data = self.test_data.get("test_calliper", thread=1, rank=1) + self.assertEqual(calliper_data.time_percent, [20.5]) + self.assertEqual(calliper_data.cumul_time, [40.5]) + self.assertEqual(calliper_data.self_time, [15.5]) + self.assertEqual(calliper_data.total_time, [35.5]) + self.assertEqual(calliper_data.n_calls, [2]) + self.assertEqual(calliper_data.rank, [1]) + self.assertEqual(calliper_data.thread, [1]) def test_filter_calliper(self): """ @@ -91,14 +174,16 @@ def test_write_txt_output_file(self): self.test_data.data["test_calliper"].self_time = [5.0, 15.0] self.test_data.data["test_calliper"].total_time = [25.0, 35.0] self.test_data.data["test_calliper"].n_calls = [2] + self.test_data.data["test_calliper"].rank = [0, 1] + self.test_data.data["test_calliper"].thread = [0, 0] # pylint: disable=unspecified-encoding with tempfile.NamedTemporaryFile(delete=False) as tmp_file: self.test_data.write_txt_output(Path(tmp_file.name)) contents = Path(tmp_file.name).read_text().splitlines() # pylint: disable=line-too-long - self.assertEqual("| Routine | Total time (s) | Self (s) | Cumul time (s) | No. calls | % time | Time per call (s) |", contents[0]) - self.assertEqual("| test_calliper | 30.0 | 10.0 | 35.0 | 2 | 15.0 | 15.0 |", contents[1]) + self.assertEqual("| Routine | Total time (s) | Self (s) | Cumul time (s) | Max no. calls | % time | Time per call (s) |", contents[0]) + self.assertEqual("| test_calliper | 30.0 | 10.0 | 35.0 | 2 | 15.0 | 15.0 |", contents[1]) def test_write_txt_output_terminal(self): """ @@ -111,6 +196,8 @@ def test_write_txt_output_terminal(self): self.test_data.data["test_calliper"].self_time = [3.0, 4.0] self.test_data.data["test_calliper"].total_time = [15.0, 55.0] self.test_data.data["test_calliper"].n_calls = [2] + self.test_data.data["test_calliper"].rank = [0, 1] + self.test_data.data["test_calliper"].thread = [0, 0] write_output = StringIO() sys.stdout = write_output @@ -118,8 +205,8 @@ def test_write_txt_output_terminal(self): sys.stdout = sys.__stdout__ # pylint: disable=line-too-long - self.assertEqual("| Routine | Total time (s) | Self (s) | Cumul time (s) | No. calls | % time | Time per call (s) |", write_output.getvalue().splitlines()[0]) - self.assertEqual("| test_calliper | 35.0 | 3.5 | 11.0 | 2 | 45.0 | 17.5 |", write_output.getvalue().splitlines()[1]) + self.assertEqual("| Routine | Total time (s) | Self (s) | Cumul time (s) | Max no. calls | % time | Time per call (s) |", write_output.getvalue().splitlines()[0]) + self.assertEqual("| test_calliper | 35.0 | 3.5 | 11.0 | 2 | 45.0 | 17.5 |", write_output.getvalue().splitlines()[1]) def test_aggregate(self): """ @@ -133,6 +220,8 @@ def test_aggregate(self): data1.data["calliper_a"].self_time = [5.0, 15.0] data1.data["calliper_a"].total_time = [25.0, 35.0] data1.data["calliper_a"].n_calls = [2, 2] + data1.data["calliper_a"].rank = [0, 1] + data1.data["calliper_a"].thread = [0, 0] data2 = VernierData() data2.add_calliper("calliper_a") @@ -141,6 +230,8 @@ def test_aggregate(self): data2.data["calliper_a"].self_time = [6.0, 16.0] data2.data["calliper_a"].total_time = [28.0, 38.0] data2.data["calliper_a"].n_calls = [3, 3] + data2.data["calliper_a"].rank = [0, 1] + data2.data["calliper_a"].thread = [0, 0] aggregated = VernierData() aggregated.aggregate([data1, data2]) @@ -174,6 +265,8 @@ def test_aggregate_inconsistent(self): data1.data["calliper_a"].self_time = [5.0, 15.0] data1.data["calliper_a"].total_time = [25.0, 35.0] data1.data["calliper_a"].n_calls = [2, 2] + data1.data["calliper_a"].rank = [0, 1] + data1.data["calliper_a"].thread = [0, 0] data2 = VernierData() data2.add_calliper("calliper_b") @@ -182,6 +275,8 @@ def test_aggregate_inconsistent(self): data2.data["calliper_b"].self_time = [6.0, 16.0] data2.data["calliper_b"].total_time = [28.0, 38.0] data2.data["calliper_b"].n_calls = [3, 3] + data2.data["calliper_b"].rank = [0, 1] + data2.data["calliper_b"].thread = [0, 0] with self.assertRaises(ValueError): aggregated = VernierData() @@ -203,19 +298,6 @@ def test_aggregate_inconsistent_ok(self): self.assertIn("calliper_a", aggregated.data) self.assertIn("calliper_b", aggregated.data) - def test_get(self): - """ - Test that the get method of the VernierData class works as expected. - """ - data1 = VernierData() - data1.add_calliper("calliper_a") - data1.data["calliper_a"].time_percent = [10.0, 20.0] - data1.data["calliper_a"].cumul_time = [30.0, 40.0] - data1.data["calliper_a"].self_time = [5.0, 15.0] - data1.data["calliper_a"].total_time = [25.0, 35.0] - data1.data["calliper_a"].n_calls = [2, 2] - self.assertEqual(len(data1.get("calliper_a")), 2) - class TestVernierCollation(unittest.TestCase): """ @@ -233,6 +315,8 @@ def _add_data(self): data1.data["calliper_a"].self_time = [5.0, 15.0] data1.data["calliper_a"].total_time = [25.0, 35.0] data1.data["calliper_a"].n_calls = [2, 2] + data1.data["calliper_a"].rank = [0, 1] + data1.data["calliper_a"].thread = [0, 0] data2 = VernierData() data2.add_calliper("calliper_a") @@ -241,6 +325,8 @@ def _add_data(self): data2.data["calliper_a"].self_time = [6.0, 16.0] data2.data["calliper_a"].total_time = [28.0, 38.0] data2.data["calliper_a"].n_calls = [3, 3] + data2.data["calliper_a"].rank = [0, 1] + data2.data["calliper_a"].thread = [0, 0] self.collation.add_data('test1', data1) self.collation.add_data('test2', data2) @@ -262,7 +348,7 @@ def test_remove_data(self): self.collation.remove_data('test1') self.assertEqual(len(self.collation), 1) - def test_get(self): + def test_get__collation(self): """ Test that the get method of VernierCollation returns the expected VernierData instance. @@ -284,6 +370,8 @@ def test_internal_consistency(self): data_inc.data["calliper_a"].self_time = [5.0, 15.0] data_inc.data["calliper_a"].total_time = [25.0, 35.0] data_inc.data["calliper_a"].n_calls = [2, 2] + data_inc.data["calliper_a"].rank = [0, 1] + data_inc.data["calliper_a"].thread = [0, 0] data_inc.add_calliper("calliper_b") data_inc.data["calliper_b"].time_percent = [15.0, 25.0] @@ -291,6 +379,8 @@ def test_internal_consistency(self): data_inc.data["calliper_b"].self_time = [6.0, 16.0] data_inc.data["calliper_b"].total_time = [28.0, 38.0] data_inc.data["calliper_b"].n_calls = [3, 3] + data_inc.data["calliper_b"].rank = [0, 1] + data_inc.data["calliper_b"].thread = [0, 0] with self.assertRaises(ValueError) as test_exception: self.collation.add_data('test3', data_inc) diff --git a/post-processing/tests/test_vernier_reader.py b/post-processing/tests/test_vernier_reader.py index 66c4da57..eb80d849 100644 --- a/post-processing/tests/test_vernier_reader.py +++ b/post-processing/tests/test_vernier_reader.py @@ -34,6 +34,10 @@ def test_load_from_file(self): self.assertIn("some_process", loaded_data.data) self.assertEqual(loaded_data.data["__test_app__"].n_calls, [1, 1]) self.assertEqual(loaded_data.data["some_process"].n_calls, [2, 2]) + self.assertEqual(loaded_data.data["__test_app__"].rank, [0, 1]) + self.assertEqual(loaded_data.data["some_process"].rank, [0, 1]) + self.assertEqual(loaded_data.data["__test_app__"].thread, [0, 0]) + self.assertEqual(loaded_data.data["some_process"].thread, [0, 0]) self.assertEqual(loaded_data.data["__test_app__"].time_percent, [44.130, 37.835]) self.assertEqual(loaded_data.data["some_process"].time_percent, [34.563, 43.991]) self.assertEqual(loaded_data.data["__test_app__"].cumul_time, [2.583, 5.596]) @@ -50,6 +54,10 @@ def test_load_from_directory(self): self.assertIn("some_process", loaded_data.data) self.assertCountEqual(loaded_data.data["__test_app__"].n_calls, [1, 1]) self.assertCountEqual(loaded_data.data["some_process"].n_calls, [2, 2]) + self.assertCountEqual(loaded_data.data["__test_app__"].rank, [0, 1]) + self.assertCountEqual(loaded_data.data["some_process"].rank, [0, 1]) + self.assertCountEqual(loaded_data.data["__test_app__"].thread, [0, 0]) + self.assertCountEqual(loaded_data.data["some_process"].thread, [0, 0]) self.assertCountEqual(loaded_data.data["__test_app__"].time_percent, [44.130, 37.835]) self.assertCountEqual(loaded_data.data["some_process"].time_percent, [34.563, 43.991]) self.assertCountEqual(loaded_data.data["__test_app__"].cumul_time, [2.583, 5.596]) @@ -59,6 +67,34 @@ def test_load_from_directory(self): self.assertCountEqual(loaded_data.data["__test_app__"].total_time, [5.854, 6.839]) self.assertCountEqual(loaded_data.data["some_process"].total_time,[2.077, 3.069]) + def test_load_threaded_data_from_directory(self): + test_reader = VernierReader(self.test_data_dir / "vernier-output-threaded") + loaded_data = test_reader.load() + self.assertIn("FULL", loaded_data.data) + self.assertIn("MAIN_SUB", loaded_data.data) + self.assertIn("MAIN_SUB2", loaded_data.data) + + self.assertCountEqual(loaded_data.data["FULL"].n_calls, [1, 1]) + self.assertCountEqual(loaded_data.data["FULL"].time_percent, [40.0, 40.031]) + self.assertCountEqual(loaded_data.data["FULL"].self_time, [2.0, 2.004]) + self.assertCountEqual(loaded_data.data["FULL"].total_time, [5.0, 5.006]) + self.assertCountEqual(loaded_data.data["FULL"].cumul_time, [4.0, 2.004]) + self.assertCountEqual(loaded_data.data["FULL"].rank, [0, 1]) + self.assertCountEqual(loaded_data.data["FULL"].thread, [0, 0]) + self.assertCountEqual(loaded_data.data["MAIN_SUB"].n_calls, [2, 1, 1, 1, 2, 1, 1, 1]) + self.assertCountEqual(loaded_data.data["MAIN_SUB"].time_percent, [40.0, 20.0, 20.0, 20.0, 39.956, 19.978, 19.978, 19.977]) + self.assertCountEqual(loaded_data.data["MAIN_SUB"].self_time, [2.0, 1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 1.0]) + self.assertCountEqual(loaded_data.data["MAIN_SUB"].total_time, [3.0, 2.0, 2.0, 2.0, 3.002, 2.004, 2.002, 2.0]) + self.assertCountEqual(loaded_data.data["MAIN_SUB"].cumul_time, [2.0, 6.0, 10.001, 11.001, 4.004, 9.012, 10.012, 11.012]) + self.assertCountEqual(loaded_data.data["MAIN_SUB"].rank, [0, 0, 0, 0, 1, 1, 1, 1]) + self.assertCountEqual(loaded_data.data["MAIN_SUB"].thread, [0, 3, 2, 1, 0, 1, 3, 2]) + self.assertCountEqual(loaded_data.data["MAIN_SUB2"].n_calls, [1, 1, 1, 1, 1, 1, 1, 1]) + self.assertCountEqual(loaded_data.data["MAIN_SUB2"].time_percent, [20.0, 20.0, 20.0, 20.0, 20.056, 20.017, 20.012, 19.979]) + self.assertCountEqual(loaded_data.data["MAIN_SUB2"].self_time, [1.0, 1.0, 1.0, 1.0, 1.004, 1.002, 1.002, 1.0]) + self.assertCountEqual(loaded_data.data["MAIN_SUB2"].total_time, [1.0, 1.0, 1.0, 1.0, 1.004, 1.002, 1.002, 1.0]) + self.assertCountEqual(loaded_data.data["MAIN_SUB2"].cumul_time, [5.0, 7.001, 8.001, 9.001, 5.008, 6.01, 7.012, 8.012]) + self.assertCountEqual(loaded_data.data["MAIN_SUB2"].rank, [0, 0, 0, 0, 1, 1, 1, 1]) + self.assertCountEqual(loaded_data.data["MAIN_SUB2"].thread, [3, 0, 2, 1, 1, 3, 0, 2]) if __name__ == '__main__': unittest.main() diff --git a/post-processing/vernier/vernier_data.py b/post-processing/vernier/vernier_data.py index 076ffdc5..0509623b 100644 --- a/post-processing/vernier/vernier_data.py +++ b/post-processing/vernier/vernier_data.py @@ -25,6 +25,8 @@ class VernierCalliper(): self_time: list[float] cumul_time: list[float] n_calls: list[int] + rank: list[int] + thread: list[int] name: str def __init__(self, name: str): @@ -36,6 +38,8 @@ def __init__(self, name: str): """ self.name = name + self.rank = [] + self.thread = [] self.time_percent = [] self.cumul_time = [] self.self_time = [] @@ -55,6 +59,78 @@ def __len__(self): result = len(self.time_percent) return result + def _get_rank_indices(self, rank: int): + """ + Return the indices of the data for a given rank. + + :param int rank: The rank number to extract indices for. + + :returns: A list of indices corresponding to the entries for the + provided rank. + :rtype: list[int] + + """ + rank_indices = [] + start = 0 + # Incrementally iterate though list with List.index() to find all the + # indices matching the chosen rank ID + while True: + try: + # Find next instance of rank ID in rank list + rank_index = self.rank.index(rank, start) + rank_indices.append(rank_index) + start = rank_index + 1 + except ValueError: + return rank_indices + + def _get_thread_indices(self, thread: int): + """ + Return the indices of the data for a given thread. + + :param int thread: The thread number to extract indices for. + + :returns: A list of indices corresponding to the entries for the + provided thread. + :rtype: list[int] + + """ + thread_indices = [] + start = 0 + # Incrementally iterate though list with List.index() to find all the + # indices matching the chosen thread ID + while True: + try: + # Find the next instance of thread ID in the list + thread_index = self.thread.index(thread, start) + thread_indices.append(thread_index) + start = thread_index + 1 + except ValueError: + return thread_indices + + def _filter_by_indices(self, indices: list[int]): + """ + Return a new VernierCalliper containing only the entries corresponding + to the provided indices. + + :param list[int] indices: A list of indices to filter by. + + :returns: A new VernierCalliper instance containing only the entries + corresponding to the provided indices. + :rtype: :py:class:`vernier.VernierCalliper` + + """ + filtered = VernierCalliper(self.name) + for index in indices: + filtered.rank.append(self.rank[index]) + filtered.thread.append(self.thread[index]) + filtered.time_percent.append(self.time_percent[index]) + filtered.cumul_time.append(self.cumul_time[index]) + filtered.self_time.append(self.self_time[index]) + filtered.total_time.append(self.total_time[index]) + filtered.n_calls.append(self.n_calls[index]) + + return filtered + def reduce(self) -> list: """Reduces the data for this calliper to a single row of summary data. @@ -64,19 +140,19 @@ def reduce(self) -> list: """ return [ - self.name.replace('@0', ''), # calliper name + self.name, # calliper name round(np.mean(self.total_time), 5), # mean total time across calls round(np.mean(self.self_time), 5), # mean self time across calls round(np.mean(self.cumul_time), 5), # mean cumulative time across calls self.n_calls[0], # number of calls (should be the same for all entries, so just take the first) round(np.mean(self.time_percent), 5), # mean percentage of time across calls - round(np.mean(self.total_time) / self.n_calls[0], 5) # mean time per call + round(np.mean(np.array(self.total_time) / np.array(self.n_calls)), 5) # mean time per call ] @classmethod def labels(self): return ["Routine", "Total time (s)", "Self (s)", "Cumul time (s)", - "No. calls", "% time", "Time per call (s)"] + "Max no. calls", "% time", "Time per call (s)"] class VernierData(): @@ -153,13 +229,7 @@ def write_txt_output(self, txt_path: Optional[Path] = None): # sort by self time, descending txt_table = sorted(txt_table, key=lambda x: x[2], reverse=True) - txt_table.insert(0, ["Routine", - "Total time (s)", - "Self (s)", - "Cumul time (s)", - "No. calls", - "% time", - "Time per call (s)"]) + txt_table.insert(0, VernierCalliper.labels()) max_calliper_len = max([len(line[0]) for line in txt_table]) @@ -170,18 +240,44 @@ def write_txt_output(self, txt_path: Optional[Path] = None): out = open(txt_path, 'w') for row in txt_table: - out.write('| {:>{}} | {:>14} | {:>12} | {:>14} | {:>9} | {:>8} | {:>17} |\n'.format(row[0], max_calliper_len, *row[1:])) + out.write('| {:>{}} | {:>14} | {:>12} | {:>14} | {:>13} | {:>8} | {:>17} |\n'.format(row[0], max_calliper_len, *row[1:])) if txt_path is not None: out.close() - def get(self, calliper_key): + def get(self, calliper_key: str, rank: Optional[int] = None, thread: Optional[int] = None) -> Optional[VernierCalliper]: """ Return a VernierCalliper of the data for this calliper_key, or None if it does not exist. - """ - return self.data.get(calliper_key, None) + :param str calliper_key: The name of the VernierCalliper to extract + from the VernierData object. + :param int rank: The rank ID to extract data for. + :param int thread: The thread ID to extract data for. + + :returns: A VernierCalliper instance containing the data of all + callipers matching the calliper_key + :rtype: :py:class:`vernier.VernierCalliper` + """ + # First get data for calliper key + return_caliper = self.data[calliper_key] + + # If rank ID given as argument, filter only data indices where rank data + # matches the rank ID given as argument + if rank is not None: + rank_indices = return_caliper._get_rank_indices(rank) + if len(rank_indices) == 0: + return None + return_caliper = return_caliper._filter_by_indices(rank_indices) + + # Same above but for thread ID + if thread is not None: + thread_indices = return_caliper._get_thread_indices(thread) + if len(thread_indices) == 0: + return None + return_caliper = return_caliper._filter_by_indices(thread_indices) + + return return_caliper def aggregate(self, vernier_data_list=None, internal_consistency=True): """ @@ -228,6 +324,8 @@ def aggregate(self, vernier_data_list=None, internal_consistency=True): self.data[calliper].self_time.extend(vernier_data.data[calliper].self_time) self.data[calliper].total_time.extend(vernier_data.data[calliper].total_time) self.data[calliper].n_calls.extend(vernier_data.data[calliper].n_calls) + self.data[calliper].rank.extend(vernier_data.data[calliper].rank) + self.data[calliper].thread.extend(vernier_data.data[calliper].thread) @@ -339,13 +437,15 @@ def calliper_list(self): break return result - def get(self, calliper_key): + def get(self, calliper_key: str, rank: Optional[int] = None, thread: Optional[int] = None) -> Optional[VernierCalliper]: """ Return a VernierCalliper of all the data from all collation members for this calliper_key, or None if it does not exist. :param str calliper_key: The name of the VernierCalliper to extract from the VernierData object. + :param int rank: The rank ID to extract data for. + :param int thread: The thread ID to extract data for. :returns: A VernierCalliper instance containing the data of all callipers matching the calliper_key @@ -357,10 +457,13 @@ def get(self, calliper_key): self.internal_consistency() results = VernierCalliper(calliper_key) for _, vdata in self.vernier_data.items(): - results.total_time += vdata.data[calliper_key].total_time - results.time_percent += vdata.data[calliper_key].time_percent - results.self_time += vdata.data[calliper_key].self_time - results.cumul_time += vdata.data[calliper_key].cumul_time - results.n_calls += vdata.data[calliper_key].n_calls + data_to_add = vdata.get(calliper_key, rank, thread) + if data_to_add is None: + continue + results.total_time += data_to_add.total_time + results.time_percent += data_to_add.time_percent + results.self_time += data_to_add.self_time + results.cumul_time += data_to_add.cumul_time + results.n_calls += data_to_add.n_calls return results diff --git a/post-processing/vernier/vernier_reader.py b/post-processing/vernier/vernier_reader.py index dc27dff6..dd2fd29a 100644 --- a/post-processing/vernier/vernier_reader.py +++ b/post-processing/vernier/vernier_reader.py @@ -38,12 +38,16 @@ def _load_from_file(self) -> VernierData: for line in contents: sline = line.split() if len(sline) > 0: # Line contains data - if sline[0].isdigit(): # Calliper lines start with a digit + if sline[0] == "Task": + rank = int(sline[-1]) # Extract rank number from the data line - calliper = sline[-1] + if sline[0].isdigit(): # Calliper lines start with a digit + calliper, thread = sline[-1].split('@') if not calliper in loaded.data: loaded.add_calliper(calliper) + loaded.data[calliper].rank.append(int(rank)) + loaded.data[calliper].thread.append(int(thread)) loaded.data[calliper].time_percent.append(float(sline[1])) loaded.data[calliper].cumul_time.append(float(sline[2])) loaded.data[calliper].self_time.append(float(sline[3]))