From 4a13993ef94d60b210e768502eff2386a60b10b4 Mon Sep 17 00:00:00 2001 From: mohitt31 Date: Wed, 2 Sep 2026 04:15:08 +0530 Subject: [PATCH] Add file descriptor (nfds) monitoring to countmon Count open file descriptors per process by enumerating /proc/PID/fd entries, aggregated across the monitored process tree. This is useful for detecting file descriptor leaks in long-running HEP workflows. The metric is non-monotonic (fd count can rise and fall), so peak and average values are tracked in the JSON summary. Includes unit tests with precooked test data (3 iterations testing rise and fall of fd count) and an integration test update to test_count.py verifying a minimum fd count per process. --- .../precooked_tests/drop/1/proc/1729/fd/0 | 0 .../precooked_tests/drop/1/proc/1729/fd/1 | 0 .../precooked_tests/drop/1/proc/1729/fd/2 | 0 .../precooked_tests/drop/2/proc/1729/fd/0 | 0 .../precooked_tests/drop/2/proc/1729/fd/1 | 0 .../precooked_tests/drop/2/proc/1729/fd/2 | 0 .../precooked_tests/drop/2/proc/1729/fd/3 | 0 .../precooked_tests/drop/2/proc/1729/fd/4 | 0 .../precooked_tests/drop/3/proc/1729/fd/0 | 0 .../precooked_tests/drop/3/proc/1729/fd/1 | 0 package/src/countmon.cpp | 14 +++++++++ package/src/countmon.h | 8 +++-- package/tests/test_count.py | 11 +++++++ package/tests/test_values.cpp | 31 +++++++++++++++++++ 14 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 package/scripts/precooked_tests/drop/1/proc/1729/fd/0 create mode 100644 package/scripts/precooked_tests/drop/1/proc/1729/fd/1 create mode 100644 package/scripts/precooked_tests/drop/1/proc/1729/fd/2 create mode 100644 package/scripts/precooked_tests/drop/2/proc/1729/fd/0 create mode 100644 package/scripts/precooked_tests/drop/2/proc/1729/fd/1 create mode 100644 package/scripts/precooked_tests/drop/2/proc/1729/fd/2 create mode 100644 package/scripts/precooked_tests/drop/2/proc/1729/fd/3 create mode 100644 package/scripts/precooked_tests/drop/2/proc/1729/fd/4 create mode 100644 package/scripts/precooked_tests/drop/3/proc/1729/fd/0 create mode 100644 package/scripts/precooked_tests/drop/3/proc/1729/fd/1 diff --git a/package/scripts/precooked_tests/drop/1/proc/1729/fd/0 b/package/scripts/precooked_tests/drop/1/proc/1729/fd/0 new file mode 100644 index 00000000..e69de29b diff --git a/package/scripts/precooked_tests/drop/1/proc/1729/fd/1 b/package/scripts/precooked_tests/drop/1/proc/1729/fd/1 new file mode 100644 index 00000000..e69de29b diff --git a/package/scripts/precooked_tests/drop/1/proc/1729/fd/2 b/package/scripts/precooked_tests/drop/1/proc/1729/fd/2 new file mode 100644 index 00000000..e69de29b diff --git a/package/scripts/precooked_tests/drop/2/proc/1729/fd/0 b/package/scripts/precooked_tests/drop/2/proc/1729/fd/0 new file mode 100644 index 00000000..e69de29b diff --git a/package/scripts/precooked_tests/drop/2/proc/1729/fd/1 b/package/scripts/precooked_tests/drop/2/proc/1729/fd/1 new file mode 100644 index 00000000..e69de29b diff --git a/package/scripts/precooked_tests/drop/2/proc/1729/fd/2 b/package/scripts/precooked_tests/drop/2/proc/1729/fd/2 new file mode 100644 index 00000000..e69de29b diff --git a/package/scripts/precooked_tests/drop/2/proc/1729/fd/3 b/package/scripts/precooked_tests/drop/2/proc/1729/fd/3 new file mode 100644 index 00000000..e69de29b diff --git a/package/scripts/precooked_tests/drop/2/proc/1729/fd/4 b/package/scripts/precooked_tests/drop/2/proc/1729/fd/4 new file mode 100644 index 00000000..e69de29b diff --git a/package/scripts/precooked_tests/drop/3/proc/1729/fd/0 b/package/scripts/precooked_tests/drop/3/proc/1729/fd/0 new file mode 100644 index 00000000..e69de29b diff --git a/package/scripts/precooked_tests/drop/3/proc/1729/fd/1 b/package/scripts/precooked_tests/drop/3/proc/1729/fd/1 new file mode 100644 index 00000000..e69de29b diff --git a/package/src/countmon.cpp b/package/src/countmon.cpp index 599144be..edd8c698 100644 --- a/package/src/countmon.cpp +++ b/package/src/countmon.cpp @@ -3,6 +3,7 @@ #include "countmon.h" +#include #include #include @@ -49,6 +50,19 @@ void countmon::update_stats(const std::vector& pids, std::stol(stat_entries[prmon::num_threads]); } stat_entries.clear(); + + std::stringstream fd_dir_name{}; + fd_dir_name << read_path << "/proc/" << pid << "/fd" << std::ends; + DIR* fd_dir = opendir(fd_dir_name.str().c_str()); + if (fd_dir) { + struct dirent* entry; + while ((entry = readdir(fd_dir)) != nullptr) { + if (entry->d_name[0] != '.') { + count_stat_update["nfds"] += 1L; + } + } + closedir(fd_dir); + } } // Update the statistics with the new snapshot values diff --git a/package/src/countmon.h b/package/src/countmon.h index 709b5bfb..bd288a7e 100644 --- a/package/src/countmon.h +++ b/package/src/countmon.h @@ -1,7 +1,7 @@ // Copyright (C) 2018-2025 CERN // License Apache2 - see LICENCE file -// Process and thread number monitoring class +// Process, thread and file descriptor number monitoring class // #ifndef PRMON_COUNTMON_H @@ -21,7 +21,8 @@ class countmon final : public Imonitor, public MessageBase { private: // Setup the parameters to monitor here const prmon::parameter_list params = {{"nprocs", "1", "1"}, - {"nthreads", "1", "1"}}; + {"nthreads", "1", "1"}, + {"nfds", "1", "1"}}; // Dynamic monitoring container for value measurements // This will be filled at initialisation, taking the names @@ -46,6 +47,7 @@ class countmon final : public Imonitor, public MessageBase { void const get_unit_info(nlohmann::json& unit_json); bool const is_valid() { return true; } }; -REGISTER_MONITOR(Imonitor, countmon, "Monitor number of processes and threads") +REGISTER_MONITOR(Imonitor, countmon, + "Monitor number of processes, threads and file descriptors") #endif // PRMON_COUNTMON_H diff --git a/package/tests/test_count.py b/package/tests/test_count.py index 46dcafe0..290985d9 100755 --- a/package/tests/test_count.py +++ b/package/tests/test_count.py @@ -65,6 +65,17 @@ def test_run_test_with_params(self): f"(expected {expect_thread}, got {total_thread}", ) + # File descriptor count sanity check: every process + # has at least stdin, stdout, stderr open + total_nfds = prmon_json["Max"]["nfds"] + min_expected_nfds = 3 * procs + self.assertGreaterEqual( + total_nfds, + min_expected_nfds, + msg="File descriptor count too low " + f"(expected >= {min_expected_nfds}, got {total_nfds})", + ) + return ConfigurableProcessMonitor diff --git a/package/tests/test_values.cpp b/package/tests/test_values.cpp index bb1a151b..c325ac43 100644 --- a/package/tests/test_values.cpp +++ b/package/tests/test_values.cpp @@ -28,6 +28,37 @@ std::shared_ptr f_sink{ bool prmon::sigusr1 = false; +TEST(CountmonTest, CountmonValueTestFixed) { + std::string cur_path = base_path + "drop"; + std::vector fake_pids = mother_pid; + + std::unique_ptr monitor( + registry::Registry::create("countmon")); + + const int iterationCount = 3; + + std::map store_stats; + for (int iteration = 1; iteration <= iterationCount; ++iteration) { + std::stringstream iteration_path{}; + iteration_path << cur_path << "/" << iteration; + monitor->update_stats(fake_pids, iteration_path.str()); + store_stats = monitor->get_text_stats(); + ASSERT_EQ(store_stats["nprocs"], 1); + ASSERT_EQ(store_stats["nthreads"], 1); + if (iteration == 1) { + ASSERT_EQ(store_stats["nfds"], 3); + } else if (iteration == 2) { + ASSERT_EQ(store_stats["nfds"], 5); + } else { + ASSERT_EQ(store_stats["nfds"], 2); + } + } + + // Verify that the peak value reflects the maximum across iterations + auto max_stats = monitor->get_json_total_stats(); + ASSERT_EQ(max_stats["nfds"], 5); +} + TEST(IomonTest, IomonMonitonicityTestFixed) { std::string cur_path = base_path + "drop"; std::vector fake_pids = mother_pid;