k4bench 0.0.13__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. k4bench-0.0.13/.codespellrc +7 -0
  2. k4bench-0.0.13/.devcontainer/devcontainer.json +37 -0
  3. k4bench-0.0.13/.github/actionlint.yaml +3 -0
  4. k4bench-0.0.13/.github/benchmarks/ALLEGRO_o1_v02.yml +48 -0
  5. k4bench-0.0.13/.github/benchmarks/ALLEGRO_o1_v03.yml +48 -0
  6. k4bench-0.0.13/.github/benchmarks/ALLEGRO_o2_v01.yml +48 -0
  7. k4bench-0.0.13/.github/benchmarks/IDEA_o1_v03.yml +50 -0
  8. k4bench-0.0.13/.github/benchmarks/IDEA_o2_v01.yml +50 -0
  9. k4bench-0.0.13/.github/scripts/list_benchmarks.py +120 -0
  10. k4bench-0.0.13/.github/scripts/machine_info.py +202 -0
  11. k4bench-0.0.13/.github/scripts/nightly_benchmark.sh +240 -0
  12. k4bench-0.0.13/.github/workflows/benchmark-detector.yml +112 -0
  13. k4bench-0.0.13/.github/workflows/ci.yml +82 -0
  14. k4bench-0.0.13/.github/workflows/deploy-dashboard.yml +111 -0
  15. k4bench-0.0.13/.github/workflows/docs.yml +52 -0
  16. k4bench-0.0.13/.github/workflows/nightly.yml +43 -0
  17. k4bench-0.0.13/.github/workflows/on-release-main.yml +31 -0
  18. k4bench-0.0.13/.gitignore +28 -0
  19. k4bench-0.0.13/.pre-commit-config.yaml +23 -0
  20. k4bench-0.0.13/CODE_OF_CONDUCT.md +133 -0
  21. k4bench-0.0.13/LICENSE +21 -0
  22. k4bench-0.0.13/PKG-INFO +115 -0
  23. k4bench-0.0.13/README.md +100 -0
  24. k4bench-0.0.13/codecov.yaml +12 -0
  25. k4bench-0.0.13/dashboard/Dockerfile +31 -0
  26. k4bench-0.0.13/dashboard/app.py +405 -0
  27. k4bench-0.0.13/dashboard/config.py +31 -0
  28. k4bench-0.0.13/dashboard/data.py +343 -0
  29. k4bench-0.0.13/dashboard/remote.py +284 -0
  30. k4bench-0.0.13/dashboard/remote_cache.py +71 -0
  31. k4bench-0.0.13/dashboard/requirements.txt +3 -0
  32. k4bench-0.0.13/dashboard/stats.py +102 -0
  33. k4bench-0.0.13/dashboard/tabs/__init__.py +0 -0
  34. k4bench-0.0.13/dashboard/tabs/event_memory.py +163 -0
  35. k4bench-0.0.13/dashboard/tabs/event_timing.py +161 -0
  36. k4bench-0.0.13/dashboard/tabs/impact.py +229 -0
  37. k4bench-0.0.13/dashboard/tabs/machine_info.py +246 -0
  38. k4bench-0.0.13/dashboard/tabs/region_timing/__init__.py +65 -0
  39. k4bench-0.0.13/dashboard/tabs/region_timing/_common.py +34 -0
  40. k4bench-0.0.13/dashboard/tabs/region_timing/attribution.py +273 -0
  41. k4bench-0.0.13/dashboard/tabs/region_timing/current_run.py +51 -0
  42. k4bench-0.0.13/dashboard/tabs/region_timing/historical.py +204 -0
  43. k4bench-0.0.13/dashboard/tabs/region_timing/step_analysis.py +260 -0
  44. k4bench-0.0.13/dashboard/tabs/trends.py +198 -0
  45. k4bench-0.0.13/dashboard/trend_window.py +44 -0
  46. k4bench-0.0.13/dashboard/ui_chrome.py +198 -0
  47. k4bench-0.0.13/dashboard/ui_utils.py +329 -0
  48. k4bench-0.0.13/docs/architecture/component-diagrams.md +79 -0
  49. k4bench-0.0.13/docs/architecture/data-flow.md +84 -0
  50. k4bench-0.0.13/docs/architecture/overview.md +143 -0
  51. k4bench-0.0.13/docs/developer-guide/development-setup.md +109 -0
  52. k4bench-0.0.13/docs/developer-guide/repository-layout.md +104 -0
  53. k4bench-0.0.13/docs/examples/advanced-workflows.md +138 -0
  54. k4bench-0.0.13/docs/examples/common-workflows.md +123 -0
  55. k4bench-0.0.13/docs/faq.md +51 -0
  56. k4bench-0.0.13/docs/gen_ref_pages.py +56 -0
  57. k4bench-0.0.13/docs/getting-started/first-workflow.md +165 -0
  58. k4bench-0.0.13/docs/getting-started/installation.md +126 -0
  59. k4bench-0.0.13/docs/getting-started/quickstart.md +130 -0
  60. k4bench-0.0.13/docs/glossary.md +129 -0
  61. k4bench-0.0.13/docs/index.md +134 -0
  62. k4bench-0.0.13/docs/reference/api-reference.md +62 -0
  63. k4bench-0.0.13/docs/reference/configuration-reference.md +84 -0
  64. k4bench-0.0.13/docs/reference/file-formats.md +117 -0
  65. k4bench-0.0.13/docs/requirements.txt +16 -0
  66. k4bench-0.0.13/docs/user-guide/commands.md +168 -0
  67. k4bench-0.0.13/docs/user-guide/configuration.md +149 -0
  68. k4bench-0.0.13/docs/user-guide/features/analysis.md +148 -0
  69. k4bench-0.0.13/docs/user-guide/features/dashboard.md +120 -0
  70. k4bench-0.0.13/docs/user-guide/features/geometry-patching.md +142 -0
  71. k4bench-0.0.13/docs/user-guide/features/sweep-modes.md +94 -0
  72. k4bench-0.0.13/docs/user-guide/features/timing-plugins.md +158 -0
  73. k4bench-0.0.13/docs/user-guide/overview.md +146 -0
  74. k4bench-0.0.13/k4bench/__init__.py +8 -0
  75. k4bench-0.0.13/k4bench/analysis/__init__.py +19 -0
  76. k4bench-0.0.13/k4bench/analysis/loader.py +258 -0
  77. k4bench-0.0.13/k4bench/analysis/plots/__init__.py +28 -0
  78. k4bench-0.0.13/k4bench/analysis/plots/_theme.py +46 -0
  79. k4bench-0.0.13/k4bench/analysis/plots/_traces.py +90 -0
  80. k4bench-0.0.13/k4bench/analysis/plots/_utils.py +196 -0
  81. k4bench-0.0.13/k4bench/analysis/plots/event.py +512 -0
  82. k4bench-0.0.13/k4bench/analysis/plots/overview.py +199 -0
  83. k4bench-0.0.13/k4bench/analysis/plots/region.py +419 -0
  84. k4bench-0.0.13/k4bench/benchmark/__init__.py +0 -0
  85. k4bench-0.0.13/k4bench/benchmark/ddsim.py +337 -0
  86. k4bench-0.0.13/k4bench/cli.py +237 -0
  87. k4bench-0.0.13/k4bench/geometry/__init__.py +0 -0
  88. k4bench-0.0.13/k4bench/geometry/patcher.py +406 -0
  89. k4bench-0.0.13/k4bench/geometry/scanner.py +135 -0
  90. k4bench-0.0.13/k4bench/plugin/__init__.py +0 -0
  91. k4bench-0.0.13/k4bench/plugin/runtime.py +144 -0
  92. k4bench-0.0.13/k4bench/results/__init__.py +0 -0
  93. k4bench-0.0.13/k4bench/results/model.py +103 -0
  94. k4bench-0.0.13/k4bench/results/reporter.py +73 -0
  95. k4bench-0.0.13/k4bench/runner/__init__.py +0 -0
  96. k4bench-0.0.13/k4bench/runner/executor.py +282 -0
  97. k4bench-0.0.13/k4bench/runner/parser.py +119 -0
  98. k4bench-0.0.13/k4bench.egg-info/PKG-INFO +115 -0
  99. k4bench-0.0.13/k4bench.egg-info/SOURCES.txt +135 -0
  100. k4bench-0.0.13/k4bench.egg-info/dependency_links.txt +1 -0
  101. k4bench-0.0.13/k4bench.egg-info/entry_points.txt +2 -0
  102. k4bench-0.0.13/k4bench.egg-info/requires.txt +5 -0
  103. k4bench-0.0.13/k4bench.egg-info/top_level.txt +1 -0
  104. k4bench-0.0.13/mkdocs.yml +156 -0
  105. k4bench-0.0.13/openshift/deployment.yaml +49 -0
  106. k4bench-0.0.13/openshift/route.yaml +17 -0
  107. k4bench-0.0.13/openshift/service.yaml +15 -0
  108. k4bench-0.0.13/openshift/userprovideddirectory.yaml +21 -0
  109. k4bench-0.0.13/plugin/CMakeLists.txt +68 -0
  110. k4bench-0.0.13/plugin/build.sh +58 -0
  111. k4bench-0.0.13/plugin/k4BenchRegionTimingAction.cpp +1280 -0
  112. k4bench-0.0.13/plugin/k4BenchTimingAction.cpp +210 -0
  113. k4bench-0.0.13/pyproject.toml +48 -0
  114. k4bench-0.0.13/requirements.txt +2 -0
  115. k4bench-0.0.13/setup.cfg +4 -0
  116. k4bench-0.0.13/setup.sh +81 -0
  117. k4bench-0.0.13/tests/conftest.py +2 -0
  118. k4bench-0.0.13/tests/fixtures/minimal_geometry/materials.xml +11 -0
  119. k4bench-0.0.13/tests/fixtures/minimal_geometry/minimal.xml +9 -0
  120. k4bench-0.0.13/tests/fixtures/minimal_geometry/subdetectors/calorimeter.xml +11 -0
  121. k4bench-0.0.13/tests/fixtures/minimal_geometry/subdetectors/tracker.xml +11 -0
  122. k4bench-0.0.13/tests/fixtures/time_output.txt +23 -0
  123. k4bench-0.0.13/tests/integration/test_plugin_build.py +55 -0
  124. k4bench-0.0.13/tests/integration/test_simulation.py +151 -0
  125. k4bench-0.0.13/tests/unit/test_analysis_loader.py +331 -0
  126. k4bench-0.0.13/tests/unit/test_analysis_plots.py +452 -0
  127. k4bench-0.0.13/tests/unit/test_benchmark.py +246 -0
  128. k4bench-0.0.13/tests/unit/test_cli.py +169 -0
  129. k4bench-0.0.13/tests/unit/test_dashboard_trends.py +69 -0
  130. k4bench-0.0.13/tests/unit/test_executor.py +220 -0
  131. k4bench-0.0.13/tests/unit/test_parser.py +156 -0
  132. k4bench-0.0.13/tests/unit/test_patcher.py +212 -0
  133. k4bench-0.0.13/tests/unit/test_plugin_runtime.py +398 -0
  134. k4bench-0.0.13/tests/unit/test_remote.py +223 -0
  135. k4bench-0.0.13/tests/unit/test_reporter.py +76 -0
  136. k4bench-0.0.13/tests/unit/test_scanner.py +177 -0
  137. k4bench-0.0.13/tests/unit/test_trend_window.py +65 -0
@@ -0,0 +1,7 @@
1
+ [codespell]
2
+ builtin = clear,rare,en-GB_to_en-US,names,informal,code
3
+ check-filenames =
4
+ check-hidden =
5
+ skip = */.git,*/build,*/prefix,*tests/data,*/.vscode,*/playground,*/py-venv,CODE_OF_CONDUCT.md
6
+ quiet-level = 2
7
+ ignore-words-list = jupyter
@@ -0,0 +1,37 @@
1
+ // Devcontainer for local development
2
+ // See https://github.com/key4hep/key4hep-images/ for available images
3
+ // Requires the host have
4
+ // a) CernVM-FS installed (see https://cvmfs.readthedocs.io/en/stable/cpt-quickstart.html for instructions)
5
+ // b) The necessary repositories mounted on the host machine; sudo mount -t cvmfs sw.hsf.org /cvmfs/sw.hsf.org
6
+ {
7
+ "name": "Key4HEP Dev",
8
+ "image": "ghcr.io/key4hep/key4hep-images/alma9-cvmfs:latest",
9
+
10
+ // Make sure CVMFS is mounted on the host (and bind-mounted):
11
+ "mounts": [
12
+ "source=/cvmfs,target=/cvmfs,type=bind"
13
+ ],
14
+
15
+ // "postCreateCommand" runs once after the container is created.
16
+ // Here, we append the source line to ~/.bashrc so new terminals pick it up.
17
+ "postCreateCommand": [
18
+ "bash",
19
+ "-c",
20
+ "echo 'source setup.sh' >> ~/.bashrc"
21
+ ],
22
+ // Install and activate vs code extensions for environment
23
+ "customizations": {
24
+ "vscode": {
25
+ "extensions": [
26
+ "ms-python.python",
27
+ "ms-toolsai.jupyter",
28
+ "ms-vscode.cpptools",
29
+ "llvm-vs-code-extensions.vscode-clangd",
30
+ "ms-vscode.cmake-tools",
31
+ "ibm.output-colorizer",
32
+ "charliermarsh.ruff",
33
+ "eamodio.gitlens"
34
+ ]
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,3 @@
1
+ self-hosted-runner:
2
+ labels:
3
+ - fcc-ironic
@@ -0,0 +1,48 @@
1
+ # k4Bench nightly benchmark config — ALLEGRO o1 v02
2
+ #
3
+ # The nightly workflow expands every file in .github/benchmarks/ into a matrix
4
+ # of (detector × sample) jobs and uploads each result to EOS under:
5
+ # {detector}/{platform}/key4hep-{release}/{sample}/{date}/
6
+ # To add a new detector, drop a new <detector>.yml file in this directory.
7
+ # To add a new sample, append an entry to samples: below.
8
+ #
9
+ # ── Top-level keys (apply to every sample in this file) ────────────────────
10
+ # xml required detector geometry, $K4GEO-relative or absolute path
11
+ # verbose optional stream ddsim output to stdout (default: false)
12
+ # sweep optional baseline + one run per subdetector dropped
13
+ # include_only optional keep only these subdetectors (list, single run)
14
+ # exclude_only optional drop these subdetectors (list, single run)
15
+ # ddsim_args optional ddsim flags applied to every sample
16
+ # steering_file optional ddsim --steeringFile path; $VAR refs (e.g. $FCCCONFIG)
17
+ # are expanded inside the runner. Steering files for FCC
18
+ # detectors live in https://github.com/HEP-FCC/FCC-config
19
+ # and are installed under $FCCCONFIG by Key4hep, e.g.
20
+ # $FCCCONFIG/FullSim/IDEA/IDEA_o1_v03/SteeringFile_IDEA_o1_v03.py
21
+ # samples required list of sample entries — see below
22
+ #
23
+ # ── Per-sample keys (under samples:) ───────────────────────────────────────
24
+ # name required slug — becomes the EOS sample directory + job label
25
+ # n_events required positive integer
26
+ # ddsim_args optional appended to top-level ddsim_args (not replaced)
27
+ # input_files optional HepMC path(s); mutually exclusive with --enableGun
28
+ # steering_file optional overrides the top-level steering_file for this sample
29
+ #
30
+ # Sample-level keys override top-level, except ddsim_args (which concatenates).
31
+ # sweep / include_only / exclude_only are mutually exclusive.
32
+
33
+ xml: FCCee/ALLEGRO/compact/ALLEGRO_o1_v02/ALLEGRO_o1_v02.xml
34
+ verbose: false
35
+ sweep: true
36
+
37
+ samples:
38
+ - name: single_e-_10GeV
39
+ n_events: 500
40
+ ddsim_args: >-
41
+ --enableGun
42
+ --gun.particle e-
43
+ --gun.distribution uniform
44
+ --gun.energy 10*GeV
45
+
46
+ - name: p8_ee_Zbb_ecm91
47
+ n_events: 100
48
+ input_files: root://eospublic.cern.ch//eos/experiment/fcc/ee/generation/hepmc/p8_ee_Zbb_ecm91/events_noVtxSmear.hepmc
@@ -0,0 +1,48 @@
1
+ # k4Bench nightly benchmark config — ALLEGRO o1 v03
2
+ #
3
+ # The nightly workflow expands every file in .github/benchmarks/ into a matrix
4
+ # of (detector × sample) jobs and uploads each result to EOS under:
5
+ # {detector}/{platform}/key4hep-{release}/{sample}/{date}/
6
+ # To add a new detector, drop a new <detector>.yml file in this directory.
7
+ # To add a new sample, append an entry to samples: below.
8
+ #
9
+ # ── Top-level keys (apply to every sample in this file) ────────────────────
10
+ # xml required detector geometry, $K4GEO-relative or absolute path
11
+ # verbose optional stream ddsim output to stdout (default: false)
12
+ # sweep optional baseline + one run per subdetector dropped
13
+ # include_only optional keep only these subdetectors (list, single run)
14
+ # exclude_only optional drop these subdetectors (list, single run)
15
+ # ddsim_args optional ddsim flags applied to every sample
16
+ # steering_file optional ddsim --steeringFile path; $VAR refs (e.g. $FCCCONFIG)
17
+ # are expanded inside the runner. Steering files for FCC
18
+ # detectors live in https://github.com/HEP-FCC/FCC-config
19
+ # and are installed under $FCCCONFIG by Key4hep, e.g.
20
+ # $FCCCONFIG/FullSim/IDEA/IDEA_o1_v03/SteeringFile_IDEA_o1_v03.py
21
+ # samples required list of sample entries — see below
22
+ #
23
+ # ── Per-sample keys (under samples:) ───────────────────────────────────────
24
+ # name required slug — becomes the EOS sample directory + job label
25
+ # n_events required positive integer
26
+ # ddsim_args optional appended to top-level ddsim_args (not replaced)
27
+ # input_files optional HepMC path(s); mutually exclusive with --enableGun
28
+ # steering_file optional overrides the top-level steering_file for this sample
29
+ #
30
+ # Sample-level keys override top-level, except ddsim_args (which concatenates).
31
+ # sweep / include_only / exclude_only are mutually exclusive.
32
+
33
+ xml: FCCee/ALLEGRO/compact/ALLEGRO_o1_v03/ALLEGRO_o1_v03.xml
34
+ verbose: false
35
+ sweep: true
36
+
37
+ samples:
38
+ - name: single_e-_10GeV
39
+ n_events: 500
40
+ ddsim_args: >-
41
+ --enableGun
42
+ --gun.particle e-
43
+ --gun.distribution uniform
44
+ --gun.energy 10*GeV
45
+
46
+ - name: p8_ee_Zbb_ecm91
47
+ n_events: 100
48
+ input_files: root://eospublic.cern.ch//eos/experiment/fcc/ee/generation/hepmc/p8_ee_Zbb_ecm91/events_noVtxSmear.hepmc
@@ -0,0 +1,48 @@
1
+ # k4Bench nightly benchmark config — ALLEGRO o2 v01
2
+ #
3
+ # The nightly workflow expands every file in .github/benchmarks/ into a matrix
4
+ # of (detector × sample) jobs and uploads each result to EOS under:
5
+ # {detector}/{platform}/key4hep-{release}/{sample}/{date}/
6
+ # To add a new detector, drop a new <detector>.yml file in this directory.
7
+ # To add a new sample, append an entry to samples: below.
8
+ #
9
+ # ── Top-level keys (apply to every sample in this file) ────────────────────
10
+ # xml required detector geometry, $K4GEO-relative or absolute path
11
+ # verbose optional stream ddsim output to stdout (default: false)
12
+ # sweep optional baseline + one run per subdetector dropped
13
+ # include_only optional keep only these subdetectors (list, single run)
14
+ # exclude_only optional drop these subdetectors (list, single run)
15
+ # ddsim_args optional ddsim flags applied to every sample
16
+ # steering_file optional ddsim --steeringFile path; $VAR refs (e.g. $FCCCONFIG)
17
+ # are expanded inside the runner. Steering files for FCC
18
+ # detectors live in https://github.com/HEP-FCC/FCC-config
19
+ # and are installed under $FCCCONFIG by Key4hep, e.g.
20
+ # $FCCCONFIG/FullSim/IDEA/IDEA_o1_v03/SteeringFile_IDEA_o1_v03.py
21
+ # samples required list of sample entries — see below
22
+ #
23
+ # ── Per-sample keys (under samples:) ───────────────────────────────────────
24
+ # name required slug — becomes the EOS sample directory + job label
25
+ # n_events required positive integer
26
+ # ddsim_args optional appended to top-level ddsim_args (not replaced)
27
+ # input_files optional HepMC path(s); mutually exclusive with --enableGun
28
+ # steering_file optional overrides the top-level steering_file for this sample
29
+ #
30
+ # Sample-level keys override top-level, except ddsim_args (which concatenates).
31
+ # sweep / include_only / exclude_only are mutually exclusive.
32
+
33
+ xml: FCCee/ALLEGRO/compact/ALLEGRO_o2_v01/ALLEGRO_o2_v01.xml
34
+ verbose: false
35
+ sweep: true
36
+
37
+ samples:
38
+ - name: single_e-_10GeV
39
+ n_events: 500
40
+ ddsim_args: >-
41
+ --enableGun
42
+ --gun.particle e-
43
+ --gun.distribution uniform
44
+ --gun.energy 10*GeV
45
+
46
+ - name: p8_ee_Zbb_ecm91
47
+ n_events: 100
48
+ input_files: root://eospublic.cern.ch//eos/experiment/fcc/ee/generation/hepmc/p8_ee_Zbb_ecm91/events_noVtxSmear.hepmc
@@ -0,0 +1,50 @@
1
+ # k4Bench nightly benchmark config — IDEA o1 v03
2
+ #
3
+ # The nightly workflow expands every file in .github/benchmarks/ into a matrix
4
+ # of (detector × sample) jobs and uploads each result to EOS under:
5
+ # {detector}/{platform}/key4hep-{release}/{sample}/{date}/
6
+ # To add a new detector, drop a new <detector>.yml file in this directory.
7
+ # To add a new sample, append an entry to samples: below.
8
+ #
9
+ # ── Top-level keys (apply to every sample in this file) ────────────────────
10
+ # xml required detector geometry, $K4GEO-relative or absolute path
11
+ # verbose optional stream ddsim output to stdout (default: false)
12
+ # sweep optional baseline + one run per subdetector dropped
13
+ # include_only optional keep only these subdetectors (list, single run)
14
+ # exclude_only optional drop these subdetectors (list, single run)
15
+ # ddsim_args optional ddsim flags applied to every sample
16
+ # steering_file optional ddsim --steeringFile path; $VAR refs (e.g. $FCCCONFIG)
17
+ # are expanded inside the runner. Steering files for FCC
18
+ # detectors live in https://github.com/HEP-FCC/FCC-config
19
+ # and are installed under $FCCCONFIG by Key4hep, e.g.
20
+ # $FCCCONFIG/FullSim/IDEA/IDEA_o1_v03/SteeringFile_IDEA_o1_v03.py
21
+ # samples required list of sample entries — see below
22
+ #
23
+ # ── Per-sample keys (under samples:) ───────────────────────────────────────
24
+ # name required slug — becomes the EOS sample directory + job label
25
+ # n_events required positive integer
26
+ # ddsim_args optional appended to top-level ddsim_args (not replaced)
27
+ # input_files optional HepMC path(s); mutually exclusive with --enableGun
28
+ # steering_file optional overrides the top-level steering_file for this sample
29
+ #
30
+ # Sample-level keys override top-level, except ddsim_args (which concatenates).
31
+ # sweep / include_only / exclude_only are mutually exclusive.
32
+
33
+ xml: FCCee/IDEA/compact/IDEA_o1_v03/IDEA_o1_v03.xml
34
+ steering_file: $FCCCONFIG/FullSim/IDEA/IDEA_o1_v03/SteeringFile_IDEA_o1_v03.py
35
+
36
+ verbose: false
37
+ sweep: false
38
+
39
+ samples:
40
+ - name: single_e-_10GeV
41
+ n_events: 100
42
+ ddsim_args: >-
43
+ --enableGun
44
+ --gun.particle e-
45
+ --gun.distribution uniform
46
+ --gun.energy 10*GeV
47
+
48
+ - name: p8_ee_Zbb_ecm91
49
+ n_events: 100
50
+ input_files: root://eospublic.cern.ch//eos/experiment/fcc/ee/generation/hepmc/p8_ee_Zbb_ecm91/events_noVtxSmear.hepmc
@@ -0,0 +1,50 @@
1
+ # k4Bench nightly benchmark config — IDEA o2 v01
2
+ #
3
+ # The nightly workflow expands every file in .github/benchmarks/ into a matrix
4
+ # of (detector × sample) jobs and uploads each result to EOS under:
5
+ # {detector}/{platform}/key4hep-{release}/{sample}/{date}/
6
+ # To add a new detector, drop a new <detector>.yml file in this directory.
7
+ # To add a new sample, append an entry to samples: below.
8
+ #
9
+ # ── Top-level keys (apply to every sample in this file) ────────────────────
10
+ # xml required detector geometry, $K4GEO-relative or absolute path
11
+ # verbose optional stream ddsim output to stdout (default: false)
12
+ # sweep optional baseline + one run per subdetector dropped
13
+ # include_only optional keep only these subdetectors (list, single run)
14
+ # exclude_only optional drop these subdetectors (list, single run)
15
+ # ddsim_args optional ddsim flags applied to every sample
16
+ # steering_file optional ddsim --steeringFile path; $VAR refs (e.g. $FCCCONFIG)
17
+ # are expanded inside the runner. Steering files for FCC
18
+ # detectors live in https://github.com/HEP-FCC/FCC-config
19
+ # and are installed under $FCCCONFIG by Key4hep, e.g.
20
+ # $FCCCONFIG/FullSim/IDEA/IDEA_o2_v01/SteeringFile_IDEA_o2_v01.py
21
+ # samples required list of sample entries — see below
22
+ #
23
+ # ── Per-sample keys (under samples:) ───────────────────────────────────────
24
+ # name required slug — becomes the EOS sample directory + job label
25
+ # n_events required positive integer
26
+ # ddsim_args optional appended to top-level ddsim_args (not replaced)
27
+ # input_files optional HepMC path(s); mutually exclusive with --enableGun
28
+ # steering_file optional overrides the top-level steering_file for this sample
29
+ #
30
+ # Sample-level keys override top-level, except ddsim_args (which concatenates).
31
+ # sweep / include_only / exclude_only are mutually exclusive.
32
+
33
+ xml: FCCee/IDEA/compact/IDEA_o2_v01/IDEA_o2_v01.xml
34
+ steering_file: $FCCCONFIG/FullSim/IDEA/IDEA_o2_v01/SteeringFile_IDEA_o2_v01.py
35
+
36
+ verbose: false
37
+ sweep: false
38
+
39
+ samples:
40
+ - name: single_e-_10GeV
41
+ n_events: 100
42
+ ddsim_args: >-
43
+ --enableGun
44
+ --gun.particle e-
45
+ --gun.distribution uniform
46
+ --gun.energy 10*GeV
47
+
48
+ - name: p8_ee_Zbb_ecm91
49
+ n_events: 100
50
+ input_files: root://eospublic.cern.ch//eos/experiment/fcc/ee/generation/hepmc/p8_ee_Zbb_ecm91/events_noVtxSmear.hepmc
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Expand .github/benchmarks/*.yml into benchmark jobs and print them as JSON
4
+ on stdout. Two output shapes:
5
+
6
+ default — flat list, one record per (detector, sample)
7
+ --grouped — list of {detector, samples: [...]} so the nightly workflow can
8
+ fan out one reusable-workflow call per detector and nest the
9
+ sample matrix beneath it in the Actions UI
10
+
11
+ Each sample record is a fully-merged config consumed downstream as plain env
12
+ vars — no YAML parsing happens after this script runs.
13
+
14
+ Top-level keys in a benchmark file are detector-wide defaults; keys inside a
15
+ samples[] entry override them for that sample only. The single exception is
16
+ ddsim_args: top-level and sample-level strings are concatenated (top first),
17
+ which lets shared ddsim flags live at the detector level. Lists are joined
18
+ to space-separated strings so they round-trip through env vars unchanged.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import re
24
+ import shlex
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ import yaml
29
+
30
+ BENCH_DIR = Path(".github/benchmarks")
31
+ CONFIG_RE = re.compile(r"^[A-Za-z0-9_-]+$")
32
+ SAMPLE_RE = re.compile(r"^[A-Za-z0-9_.+-]+$")
33
+
34
+ SCALAR_KEYS = ("xml", "n_events", "ddsim_args", "verbose", "sweep", "steering_file")
35
+ LIST_KEYS = ("input_files", "include_only", "exclude_only")
36
+
37
+
38
+ def _scalar(v) -> str:
39
+ if v is None: return ""
40
+ if isinstance(v, bool): return str(v).lower()
41
+ return str(v).strip()
42
+
43
+
44
+ def _list(v) -> str:
45
+ if v is None: return ""
46
+ if isinstance(v, list): return " ".join(str(x) for x in v)
47
+ return str(v).strip()
48
+
49
+
50
+ def _die(msg: str) -> None:
51
+ print(f"ERROR: {msg}", file=sys.stderr)
52
+ sys.exit(1)
53
+
54
+
55
+ def expand(path: Path) -> list[dict]:
56
+ config = path.stem
57
+ if not CONFIG_RE.match(config):
58
+ _die(f"invalid config name {config!r} (from {path})")
59
+
60
+ cfg = yaml.safe_load(path.read_text()) or {}
61
+ if not isinstance(cfg, dict):
62
+ _die(f"top-level YAML must be a mapping in {path}")
63
+ samples = cfg.get("samples") or []
64
+ if not isinstance(samples, list):
65
+ _die(f"'samples' must be a list in {path}")
66
+ if not samples:
67
+ _die(f"no samples defined in {path}")
68
+
69
+ records = []
70
+ for s in samples:
71
+ if not isinstance(s, dict):
72
+ _die(f"sample entries in {path} must be mappings, got {type(s).__name__}")
73
+ name = s.get("name")
74
+ if not name:
75
+ _die(f"sample entry in {path} is missing 'name'")
76
+ if not SAMPLE_RE.match(name):
77
+ _die(f"invalid sample name {name!r} in {path}")
78
+
79
+ def merge(k):
80
+ # ddsim_args concatenates (top + sample); everything else overrides.
81
+ if k == "ddsim_args":
82
+ parts = [v for v in (cfg.get(k), s.get(k)) if v]
83
+ return " ".join(str(p).strip() for p in parts) if parts else None
84
+ return s[k] if k in s else cfg.get(k)
85
+
86
+ rec = {"config": config, "sample": name}
87
+ for k in SCALAR_KEYS: rec[k] = _scalar(merge(k))
88
+ for k in LIST_KEYS: rec[k] = _list(merge(k))
89
+
90
+ loc = f"{path}::{name}"
91
+ if not rec["n_events"].isdigit() or int(rec["n_events"]) <= 0:
92
+ _die(f"n_events must be a positive integer ({loc})")
93
+ if rec["input_files"] and "--enableGun" in shlex.split(rec["ddsim_args"]):
94
+ _die(f"input_files and '--enableGun' in ddsim_args are mutually exclusive ({loc})")
95
+ modes = (rec["sweep"] == "true") + bool(rec["include_only"]) + bool(rec["exclude_only"])
96
+ if modes > 1:
97
+ _die(f"sweep / include_only / exclude_only are mutually exclusive ({loc})")
98
+
99
+ records.append(rec)
100
+ return records
101
+
102
+
103
+ def _group_by_detector(items: list[dict]) -> list[dict]:
104
+ grouped: dict[str, list[dict]] = {}
105
+ for item in items:
106
+ grouped.setdefault(item["config"], []).append(item)
107
+ return [{"detector": d, "samples": s} for d, s in grouped.items()]
108
+
109
+
110
+ def main() -> None:
111
+ grouped = "--grouped" in sys.argv[1:]
112
+ paths = sorted(BENCH_DIR.glob("*.yml"))
113
+ if not paths:
114
+ _die(f"no benchmark configs found in {BENCH_DIR}/")
115
+ items = [r for p in paths for r in expand(p)]
116
+ print(json.dumps(_group_by_detector(items) if grouped else items))
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Collect machine info around a benchmark run.
4
+
5
+ machine_info.py start <output_dir> # snapshot before the benchmark
6
+ machine_info.py finalize <output_dir> # snapshot after, merged with start
7
+
8
+ The start step writes <output_dir>/_machine_info_start.json with static
9
+ fields (CPU model, RAM total, OS, kernel) and "_start" baselines for the
10
+ dynamic fields (load, free memory, swap, CPU frequency, thermal throttle
11
+ counter). The finalize step reads that file, adds matching "_end" fields,
12
+ computes thermal_throttle_events as the delta over the run, writes
13
+ <output_dir>/machine_info.json, and removes the intermediate start file.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import glob
18
+ import json
19
+ import os
20
+ import platform
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ START_FILE = "_machine_info_start.json"
25
+ FINAL_FILE = "machine_info.json"
26
+
27
+
28
+ def _read(path: str, default: str = "") -> str:
29
+ try:
30
+ with open(path) as f:
31
+ return f.read()
32
+ except OSError:
33
+ return default
34
+
35
+
36
+ def _parse_meminfo() -> dict[str, int]:
37
+ out: dict[str, int] = {}
38
+ for line in _read("/proc/meminfo").splitlines():
39
+ p = line.split()
40
+ if len(p) >= 2:
41
+ try:
42
+ out[p[0].rstrip(":")] = int(p[1])
43
+ except ValueError:
44
+ pass
45
+ return out
46
+
47
+
48
+ def _loadavg() -> list[str]:
49
+ return _read("/proc/loadavg").split()
50
+
51
+
52
+ def _cpu_freq_mhz() -> float | None:
53
+ raw = _read("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq").strip()
54
+ if not raw:
55
+ return None
56
+ try:
57
+ return round(int(raw) / 1000, 1)
58
+ except ValueError:
59
+ return None
60
+
61
+
62
+ def _throttle_paths() -> list[str]:
63
+ return glob.glob("/sys/devices/system/cpu/cpu*/thermal_throttle/core_throttle_count")
64
+
65
+
66
+ def _sum_throttle(paths: list[str]) -> int | None:
67
+ if not paths:
68
+ return None
69
+ total = 0
70
+ for p in paths:
71
+ raw = _read(p).strip()
72
+ try:
73
+ total += int(raw)
74
+ except ValueError:
75
+ pass
76
+ return total
77
+
78
+
79
+ def _kib_to_gib(kib: int) -> float:
80
+ return round(kib / 1024**2, 2)
81
+
82
+
83
+ def collect_start() -> dict:
84
+ cpuinfo = _read("/proc/cpuinfo")
85
+ cpu_model = next(
86
+ (line.split(":", 1)[1].strip() for line in cpuinfo.splitlines() if "model name" in line),
87
+ "unknown",
88
+ )
89
+ # Tolerate any whitespace between the field name and the colon.
90
+ cpu_logical = sum(1 for line in cpuinfo.splitlines() if line.startswith("processor"))
91
+
92
+ # Count unique (physical_id, core_id) pairs — more accurate than socket count
93
+ # alone. Falls back to logical count if these fields are absent (VMs, containers).
94
+ pairs: set = set()
95
+ current: dict = {}
96
+
97
+ def _flush(block: dict) -> None:
98
+ if "processor" in block:
99
+ pairs.add((
100
+ block.get("physical id", "0"),
101
+ block.get("core id", block.get("processor", "0")),
102
+ ))
103
+
104
+ for line in cpuinfo.splitlines():
105
+ if not line.strip():
106
+ _flush(current)
107
+ current = {}
108
+ elif ":" in line:
109
+ k, _, v = line.partition(":")
110
+ current[k.strip()] = v.strip()
111
+ _flush(current) # capture the final block when no trailing blank line
112
+ cpu_physical = len(pairs) if pairs else cpu_logical
113
+ cpu_flags = next(
114
+ (line.split(":", 1)[1].strip().split() for line in cpuinfo.splitlines() if line.startswith("flags")),
115
+ [],
116
+ )
117
+
118
+ mem = _parse_meminfo()
119
+ loadavg = _loadavg()
120
+ swap_used_kib = mem.get("SwapTotal", 0) - mem.get("SwapFree", mem.get("SwapTotal", 0))
121
+
122
+ os_name = next(
123
+ (line.split("=", 1)[1].strip('"') for line in _read("/etc/os-release").splitlines()
124
+ if line.startswith("PRETTY_NAME=")),
125
+ "unknown",
126
+ )
127
+
128
+ return {
129
+ "cpu_model": cpu_model,
130
+ "cpu_physical_cores": cpu_physical,
131
+ "cpu_logical_cores": cpu_logical,
132
+ "cpu_flags": cpu_flags,
133
+ "ram_total_gb": _kib_to_gib(mem.get("MemTotal", 0)),
134
+ "ram_available_gb_start": _kib_to_gib(mem.get("MemAvailable", 0)),
135
+ "swap_total_gb": _kib_to_gib(mem.get("SwapTotal", 0)),
136
+ "swap_used_gb_start": _kib_to_gib(swap_used_kib),
137
+ "load_avg_1m_start": float(loadavg[0]) if len(loadavg) > 0 else None,
138
+ "load_avg_5m_start": float(loadavg[1]) if len(loadavg) > 1 else None,
139
+ "cpu_governor": _read("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor").strip() or None,
140
+ "cpu_freq_mhz_start": _cpu_freq_mhz(),
141
+ "thermal_throttle_count_start": _sum_throttle(_throttle_paths()),
142
+ "kernel": platform.release(),
143
+ "os": os_name,
144
+ "hostname": os.uname().nodename,
145
+ "in_container": os.path.exists("/.dockerenv"),
146
+ }
147
+
148
+
149
+ def finalize(machine_info: dict) -> dict:
150
+ mem = _parse_meminfo()
151
+ loadavg = _loadavg()
152
+ swap_used_kib = mem.get("SwapTotal", 0) - mem.get("SwapFree", mem.get("SwapTotal", 0))
153
+
154
+ machine_info["ram_available_gb_end"] = _kib_to_gib(mem.get("MemAvailable", 0))
155
+ machine_info["swap_used_gb_end"] = _kib_to_gib(swap_used_kib)
156
+ machine_info["load_avg_1m_end"] = float(loadavg[0]) if len(loadavg) > 0 else None
157
+ machine_info["load_avg_5m_end"] = float(loadavg[1]) if len(loadavg) > 1 else None
158
+ machine_info["cpu_freq_mhz_end"] = _cpu_freq_mhz()
159
+
160
+ paths = _throttle_paths()
161
+ end = _sum_throttle(paths)
162
+ start = machine_info.get("thermal_throttle_count_start")
163
+ machine_info["thermal_throttle_events"] = (
164
+ max(0, end - start) if (paths and start is not None and end is not None) else None
165
+ )
166
+ return machine_info
167
+
168
+
169
+ def cmd_start(out_dir: Path) -> None:
170
+ out_dir.mkdir(parents=True, exist_ok=True)
171
+ info = collect_start()
172
+ (out_dir / START_FILE).write_text(json.dumps(info, indent=2))
173
+ print(f"cpu_model : {info['cpu_model']}")
174
+ print(f"cpu_logical_cores: {info['cpu_logical_cores']}")
175
+ print(f"ram_total_gb : {info['ram_total_gb']:.2f} GB")
176
+ print(f"ram_available : {info['ram_available_gb_start']:.2f} GB")
177
+ print(f"load_avg_1m : {info['load_avg_1m_start']}")
178
+
179
+
180
+ def cmd_finalize(out_dir: Path) -> None:
181
+ start_path = out_dir / START_FILE
182
+ if start_path.exists():
183
+ info = json.loads(start_path.read_text())
184
+ start_path.unlink()
185
+ else:
186
+ print(f"WARNING: {start_path} not found — machine info start snapshot missing", flush=True)
187
+ info = {}
188
+ info = finalize(info)
189
+ (out_dir / FINAL_FILE).write_text(json.dumps(info, indent=2))
190
+ print(f"Written: {out_dir / FINAL_FILE}")
191
+
192
+
193
+ def main() -> None:
194
+ if len(sys.argv) != 3 or sys.argv[1] not in ("start", "finalize"):
195
+ sys.exit(f"usage: {sys.argv[0]} (start|finalize) <output_dir>")
196
+ cmd = sys.argv[1]
197
+ out_dir = Path(sys.argv[2])
198
+ (cmd_start if cmd == "start" else cmd_finalize)(out_dir)
199
+
200
+
201
+ if __name__ == "__main__":
202
+ main()