From a8c8191346b58c6f56d73c7ed20c9b678e338c84 Mon Sep 17 00:00:00 2001 From: dawnli139 Date: Tue, 11 Aug 2026 00:05:54 +0800 Subject: [PATCH 1/3] feat(icts): add deterministic destruction lifecycle --- src/interface/python/py_icts/py_icts.cpp | 5 + src/interface/python/py_icts/py_icts.h | 1 + .../python/py_icts/py_register_icts.h | 1 + src/interface/tcl/tcl_icts/tcl_cts.cpp | 14 + src/interface/tcl/tcl_icts/tcl_cts.h | 16 +- src/interface/tcl/tcl_icts/tcl_register_cts.h | 4 +- src/operation/iCTS/interface/CTSAPI.cc | 28 +- src/operation/iCTS/interface/CTSAPI.hh | 2 +- .../data_manager/adapter/fast_sta/FastSTA.cc | 18 +- .../iCTS/source/toolkit/utility/Utility.cc | 47 +++ .../iCTS/source/toolkit/utility/Utility.hh | 10 + .../iCTS/test/data_manager/CMakeLists.txt | 1 + .../adapter/fast_sta/FastSTATest.cc | 11 + .../data_manager/lifecycle/CMakeLists.txt | 21 + .../lifecycle/CTSAPILifecycleTest.cc | 366 ++++++++++++++++++ .../lifecycle/CTSProcessExitTest.cc | 32 ++ src/operation/iCTS/test/main.cc | 37 +- .../iCTS/test/toolkit/CMakeLists.txt | 1 + .../iCTS/test/toolkit/utility/CMakeLists.txt | 5 + .../iCTS/test/toolkit/utility/UtilityTest.cc | 57 +++ 20 files changed, 642 insertions(+), 35 deletions(-) create mode 100644 src/operation/iCTS/test/data_manager/lifecycle/CMakeLists.txt create mode 100644 src/operation/iCTS/test/data_manager/lifecycle/CTSAPILifecycleTest.cc create mode 100644 src/operation/iCTS/test/data_manager/lifecycle/CTSProcessExitTest.cc create mode 100644 src/operation/iCTS/test/toolkit/utility/CMakeLists.txt create mode 100644 src/operation/iCTS/test/toolkit/utility/UtilityTest.cc diff --git a/src/interface/python/py_icts/py_icts.cpp b/src/interface/python/py_icts/py_icts.cpp index 989aa597d..6290f2127 100644 --- a/src/interface/python/py_icts/py_icts.cpp +++ b/src/interface/python/py_icts/py_icts.cpp @@ -31,6 +31,11 @@ bool CtsReport(const std::string& path) return CTS_API_INST.report(path).ok(); } +bool CtsDestroy() +{ + return CTS_API_INST.destroyCTS().ok(); +} + pybind11::dict CtsTimingFeature() { namespace py = pybind11; diff --git a/src/interface/python/py_icts/py_icts.h b/src/interface/python/py_icts/py_icts.h index f857d7522..3bd2fd069 100644 --- a/src/interface/python/py_icts/py_icts.h +++ b/src/interface/python/py_icts/py_icts.h @@ -22,5 +22,6 @@ namespace python_interface { bool CtsAutoRun(const std::string& cts_config, const std::string& cts_work_dir); bool CtsReport(const std::string& path); +bool CtsDestroy(); pybind11::dict CtsTimingFeature(); } // namespace python_interface diff --git a/src/interface/python/py_icts/py_register_icts.h b/src/interface/python/py_icts/py_register_icts.h index 73261d119..7e7732518 100644 --- a/src/interface/python/py_icts/py_register_icts.h +++ b/src/interface/python/py_icts/py_register_icts.h @@ -26,6 +26,7 @@ void register_icts(pybind11::module& m) { m.def("run_cts", CtsAutoRun, py::arg("cts_config"), py::arg("cts_work_dir")); m.def("cts_report", CtsReport, py::arg("path")); + m.def("destroy_cts", CtsDestroy); m.def("cts_timing_feature", CtsTimingFeature); } diff --git a/src/interface/tcl/tcl_icts/tcl_cts.cpp b/src/interface/tcl/tcl_icts/tcl_cts.cpp index 6e6c459c8..c3483edd1 100644 --- a/src/interface/tcl/tcl_icts/tcl_cts.cpp +++ b/src/interface/tcl/tcl_icts/tcl_cts.cpp @@ -98,4 +98,18 @@ unsigned CmdCTSReport::exec() return 0; } + +CmdCTSDestroy::CmdCTSDestroy(const char* cmd_name) : TclCmd(cmd_name) +{ +} + +unsigned CmdCTSDestroy::check() +{ + return 1U; +} + +unsigned CmdCTSDestroy::exec() +{ + return CTS_API_INST.destroyCTS().ok() ? 1U : 0U; +} } // namespace tcl diff --git a/src/interface/tcl/tcl_icts/tcl_cts.h b/src/interface/tcl/tcl_icts/tcl_cts.h index 2d6be5aef..6e53ae225 100644 --- a/src/interface/tcl/tcl_icts/tcl_cts.h +++ b/src/interface/tcl/tcl_icts/tcl_cts.h @@ -35,7 +35,8 @@ using ecc::TclStringOption; namespace tcl { -class CmdCTSAutoRun : public TclCmd { +class CmdCTSAutoRun : public TclCmd +{ public: explicit CmdCTSAutoRun(const char* cmd_name); ~CmdCTSAutoRun() override = default; @@ -48,7 +49,8 @@ class CmdCTSAutoRun : public TclCmd { // private data }; -class CmdCTSReport : public TclCmd { +class CmdCTSReport : public TclCmd +{ public: explicit CmdCTSReport(const char* cmd_name); ~CmdCTSReport() override = default; @@ -60,4 +62,14 @@ class CmdCTSReport : public TclCmd { // private function // private data }; + +class CmdCTSDestroy : public TclCmd +{ + public: + explicit CmdCTSDestroy(const char* cmd_name); + ~CmdCTSDestroy() override = default; + + unsigned check() override; + unsigned exec() override; +}; } // namespace tcl diff --git a/src/interface/tcl/tcl_icts/tcl_register_cts.h b/src/interface/tcl/tcl_icts/tcl_register_cts.h index 1986db58d..f06d812f4 100644 --- a/src/interface/tcl/tcl_icts/tcl_register_cts.h +++ b/src/interface/tcl/tcl_icts/tcl_register_cts.h @@ -31,10 +31,12 @@ using namespace ecc; namespace tcl { -int registerCmdCTS() { +int registerCmdCTS() +{ registerTclCmd(CmdCTSAutoRun, "run_cts"); registerTclCmd(CmdCTSReport, "cts_report"); registerTclCmd(CmdCTSConfig, "cts_config"); + registerTclCmd(CmdCTSDestroy, "destroy_cts"); return EXIT_SUCCESS; } diff --git a/src/operation/iCTS/interface/CTSAPI.cc b/src/operation/iCTS/interface/CTSAPI.cc index ff655b3a6..9b8c8eb8b 100644 --- a/src/operation/iCTS/interface/CTSAPI.cc +++ b/src/operation/iCTS/interface/CTSAPI.cc @@ -29,6 +29,7 @@ #include "LogTable.hh" #include "Logger.hh" #include "Monitor.hh" +#include "Utility.hh" #include "data_manager/DataManager.hh" #include "evaluation/Evaluation.hh" #include "evaluation/qor/QOREvaluation.hh" @@ -71,11 +72,7 @@ auto buildInputStatus(const DataManagerStatus& input_status) -> CTSStatus CTSAPI::CTSAPI() = default; -CTSAPI::~CTSAPI() -{ - DataManager::destroyInst(); - Logger::destroyInst(); -} +CTSAPI::~CTSAPI() = default; auto CTSAPI::setLastStatus(CTSStatus status) -> CTSStatus { @@ -152,18 +149,28 @@ auto CTSAPI::report(const std::string& save_dir) -> CTSStatus : CTSStatus{.code = CTSStatusCode::kReportError, .message = "CTS report generation failed.", .diagnostics = {}}); } -auto CTSAPI::resetAPI() -> void +auto CTSAPI::destroyCTS() -> CTSStatus { auto& api = getInst(); + Logger::initInst(); + CTSLOG.info(Loc::current(), "Starting CTS destruction..."); DataManager::destroyInst(); - Logger::destroyInst(); api._initialized = false; - api.setLastStatus(buildOkStatus("CTS API reset.")); + const auto memory_stats = Utility::releaseMemory(); + const auto status = api.setLastStatus(buildOkStatus("CTS destruction completed.")); + if (memory_stats.supported) { + CTSLOG.info(Loc::current(), "Completed CTS destruction; allocator release supported, RSS before=", Utility::formatFixed(memory_stats.rss_before_mb, 2), + " MiB, RSS after=", Utility::formatFixed(memory_stats.rss_after_mb, 2), " MiB."); + } else { + CTSLOG.info(Loc::current(), "Completed CTS destruction; allocator release unsupported."); + } + Logger::destroyInst(); + return status; } auto CTSAPI::init(const std::string& config_file, const std::string& work_dir) -> CTSStatus { - resetAPI(); + (void) destroyCTS(); auto& api = getInst(); Logger::initInst(); Monitor monitor; @@ -176,8 +183,7 @@ auto CTSAPI::init(const std::string& config_file, const std::string& work_dir) - auto status = buildInputStatus(input_status); if (!input_status.ok()) { CTSLOG.warn(Loc::current(), "CTS initialization failed: ", input_status.message, monitor.getStatsInfo()); - DataManager::destroyInst(); - Logger::destroyInst(); + (void) destroyCTS(); return api.setLastStatus(std::move(status)); } api._initialized = true; diff --git a/src/operation/iCTS/interface/CTSAPI.hh b/src/operation/iCTS/interface/CTSAPI.hh index db8a19663..19cc37948 100644 --- a/src/operation/iCTS/interface/CTSAPI.hh +++ b/src/operation/iCTS/interface/CTSAPI.hh @@ -63,7 +63,7 @@ class CTSAPI static auto report(const std::string& save_dir) -> CTSStatus; // Lifecycle API - static auto resetAPI() -> void; + static auto destroyCTS() -> CTSStatus; static auto init(const std::string& config_file, const std::string& work_dir = "") -> CTSStatus; static auto lastStatus() -> CTSStatus; diff --git a/src/operation/iCTS/source/data_manager/adapter/fast_sta/FastSTA.cc b/src/operation/iCTS/source/data_manager/adapter/fast_sta/FastSTA.cc index a767b8d8b..1834b770e 100644 --- a/src/operation/iCTS/source/data_manager/adapter/fast_sta/FastSTA.cc +++ b/src/operation/iCTS/source/data_manager/adapter/fast_sta/FastSTA.cc @@ -193,10 +193,12 @@ auto FastSTA::buildClockContext(const FastStaClockBuildInput& input) -> FastStaC auto FastSTA::eraseClockContext(FastStaClockId clock_id) -> bool { - if (clock_id >= _contexts->clock_context_valid.size() || !_contexts->clock_context_valid.at(clock_id)) { + if (clock_id >= _contexts->clock_contexts.size() || clock_id >= _contexts->clock_context_valid.size() || !_contexts->clock_context_valid.at(clock_id) + || _contexts->clock_contexts.at(clock_id) == nullptr) { return false; } _contexts->clock_context_valid.at(clock_id) = false; + _contexts->clock_contexts.at(clock_id).reset(); return true; } @@ -223,17 +225,19 @@ auto FastSTA::buildCharContext(const FastStaCharTopologySpec& spec) -> FastStaCh auto FastSTA::eraseCharContext(FastStaCharContextId char_context_id) -> bool { - if (char_context_id >= _contexts->char_context_valid.size() || !_contexts->char_context_valid.at(char_context_id)) { + if (char_context_id >= _contexts->char_contexts.size() || char_context_id >= _contexts->char_context_valid.size() + || !_contexts->char_context_valid.at(char_context_id) || _contexts->char_contexts.at(char_context_id) == nullptr) { return false; } _contexts->char_context_valid.at(char_context_id) = false; + _contexts->char_contexts.at(char_context_id).reset(); return true; } auto FastSTA::setCharLoad(FastStaCharContextId char_context_id, double effective_load_pf) -> bool { if (char_context_id >= _contexts->char_contexts.size() || char_context_id >= _contexts->char_context_valid.size() - || !_contexts->char_context_valid.at(char_context_id)) { + || !_contexts->char_context_valid.at(char_context_id) || _contexts->char_contexts.at(char_context_id) == nullptr) { CTSLOG.warn(Loc::current(), "FastSTA: characterization load update skipped because char context id is invalid."); return false; } @@ -243,7 +247,7 @@ auto FastSTA::setCharLoad(FastStaCharContextId char_context_id, double effective auto FastSTA::runCharSample(FastStaCharContextId char_context_id, double input_slew_ns) -> FastStaCharSampleResult { if (char_context_id >= _contexts->char_contexts.size() || char_context_id >= _contexts->char_context_valid.size() - || !_contexts->char_context_valid.at(char_context_id)) { + || !_contexts->char_context_valid.at(char_context_id) || _contexts->char_contexts.at(char_context_id) == nullptr) { CTSLOG.warn(Loc::current(), "FastSTA: characterization sample skipped because char context id is invalid."); return {}; } @@ -444,7 +448,8 @@ auto FastSTA::queryPower(FastStaClockId clock_id) const -> std::optional const FastStaClockContext* { - if (clock_id >= _contexts->clock_contexts.size() || clock_id >= _contexts->clock_context_valid.size() || !_contexts->clock_context_valid.at(clock_id)) { + if (clock_id >= _contexts->clock_contexts.size() || clock_id >= _contexts->clock_context_valid.size() || !_contexts->clock_context_valid.at(clock_id) + || _contexts->clock_contexts.at(clock_id) == nullptr) { return nullptr; } return _contexts->clock_contexts.at(clock_id).get(); @@ -452,7 +457,8 @@ auto FastSTA::queryClockContext(FastStaClockId clock_id) const -> const FastStaC auto FastSTA::mutableClockContext(FastStaClockId clock_id) -> FastStaClockContext* { - if (clock_id >= _contexts->clock_contexts.size() || clock_id >= _contexts->clock_context_valid.size() || !_contexts->clock_context_valid.at(clock_id)) { + if (clock_id >= _contexts->clock_contexts.size() || clock_id >= _contexts->clock_context_valid.size() || !_contexts->clock_context_valid.at(clock_id) + || _contexts->clock_contexts.at(clock_id) == nullptr) { return nullptr; } return _contexts->clock_contexts.at(clock_id).get(); diff --git a/src/operation/iCTS/source/toolkit/utility/Utility.cc b/src/operation/iCTS/source/toolkit/utility/Utility.cc index 05b7d210b..48a10682e 100644 --- a/src/operation/iCTS/source/toolkit/utility/Utility.cc +++ b/src/operation/iCTS/source/toolkit/utility/Utility.cc @@ -23,6 +23,17 @@ #include "Utility.hh" +#include +#include + +#ifdef __GLIBC__ +#include +#endif + +#ifdef __linux__ +#include +#endif + namespace icts { auto Utility::getElapsedSeconds(std::chrono::steady_clock::time_point start_time) -> double @@ -30,4 +41,40 @@ auto Utility::getElapsedSeconds(std::chrono::steady_clock::time_point start_time return std::chrono::duration(std::chrono::steady_clock::now() - start_time).count(); } +auto Utility::currentRssMb() -> std::optional +{ +#ifdef __linux__ + std::ifstream statm("/proc/self/statm"); + std::size_t total_pages = 0U; + std::size_t resident_pages = 0U; + if (!(statm >> total_pages >> resident_pages)) { + return std::nullopt; + } + const auto page_size_bytes = ::sysconf(_SC_PAGESIZE); + if (page_size_bytes <= 0) { + return std::nullopt; + } + constexpr double bytes_per_mebibyte = 1024.0 * 1024.0; + return static_cast(resident_pages) * static_cast(page_size_bytes) / bytes_per_mebibyte; +#else + return std::nullopt; +#endif +} + +auto Utility::releaseMemory() -> MemoryReleaseStats +{ + MemoryReleaseStats stats; +#if defined(__linux__) && defined(__GLIBC__) + const auto rss_before_mb = currentRssMb(); + (void) ::malloc_trim(0); + const auto rss_after_mb = currentRssMb(); + if (rss_before_mb.has_value() && rss_after_mb.has_value()) { + stats.supported = true; + stats.rss_before_mb = *rss_before_mb; + stats.rss_after_mb = *rss_after_mb; + } +#endif + return stats; +} + } // namespace icts diff --git a/src/operation/iCTS/source/toolkit/utility/Utility.hh b/src/operation/iCTS/source/toolkit/utility/Utility.hh index 0912fdd13..7d0b7a325 100644 --- a/src/operation/iCTS/source/toolkit/utility/Utility.hh +++ b/src/operation/iCTS/source/toolkit/utility/Utility.hh @@ -25,18 +25,28 @@ #include #include +#include #include #include #include namespace icts { +struct MemoryReleaseStats +{ + bool supported = false; + double rss_before_mb = 0.0; + double rss_after_mb = 0.0; +}; + class Utility final { public: Utility() = delete; static auto getElapsedSeconds(std::chrono::steady_clock::time_point start_time) -> double; + static auto currentRssMb() -> std::optional; + static auto releaseMemory() -> MemoryReleaseStats; template static auto getString(Args&&... args) -> std::string diff --git a/src/operation/iCTS/test/data_manager/CMakeLists.txt b/src/operation/iCTS/test/data_manager/CMakeLists.txt index b54bd11af..0c67499e5 100644 --- a/src/operation/iCTS/test/data_manager/CMakeLists.txt +++ b/src/operation/iCTS/test/data_manager/CMakeLists.txt @@ -4,6 +4,7 @@ icts_add_test_executable( ${ICTS_TEST}/data_manager/DataManagerTest.cc ) +add_subdirectory(lifecycle) add_subdirectory(adapter) add_subdirectory(config) add_subdirectory(design) diff --git a/src/operation/iCTS/test/data_manager/adapter/fast_sta/FastSTATest.cc b/src/operation/iCTS/test/data_manager/adapter/fast_sta/FastSTATest.cc index 1d666c424..c916775a6 100644 --- a/src/operation/iCTS/test/data_manager/adapter/fast_sta/FastSTATest.cc +++ b/src/operation/iCTS/test/data_manager/adapter/fast_sta/FastSTATest.cc @@ -570,6 +570,17 @@ TEST(FastSTATest, CharacterizationSampleRejectsMissingSourceBoundaryNet) EXPECT_FALSE(sample.valid); } +TEST(FastSTATest, InvalidCharacterizationContextAccessIsSafe) +{ + icts::FastSTA fast_sta; + + EXPECT_FALSE(fast_sta.eraseCharContext(0U)); + EXPECT_FALSE(fast_sta.setCharLoad(0U, 0.25)); + EXPECT_FALSE(fast_sta.runCharSample(0U, 0.10).valid); + fast_sta.reset(); + EXPECT_FALSE(fast_sta.eraseCharContext(0U)); +} + TEST(FastSTATest, IncrementalMasterChangeMatchesFullRecompute) { auto incremental_context = MakeTwoLevelContext(); diff --git a/src/operation/iCTS/test/data_manager/lifecycle/CMakeLists.txt b/src/operation/iCTS/test/data_manager/lifecycle/CMakeLists.txt new file mode 100644 index 000000000..ace9ee2c7 --- /dev/null +++ b/src/operation/iCTS/test/data_manager/lifecycle/CMakeLists.txt @@ -0,0 +1,21 @@ +icts_add_test_executable( + icts_test_data_manager_lifecycle + SOURCES + ${ICTS_TEST}/data_manager/lifecycle/CTSAPILifecycleTest.cc +) + +add_executable( + icts_test_data_manager_lifecycle_exit + ${ICTS_TEST}/data_manager/lifecycle/CTSProcessExitTest.cc +) + +target_link_libraries( + icts_test_data_manager_lifecycle_exit + PRIVATE + icts_test_base +) + +add_test( + NAME icts_test_data_manager_lifecycle_exit + COMMAND icts_test_data_manager_lifecycle_exit +) diff --git a/src/operation/iCTS/test/data_manager/lifecycle/CTSAPILifecycleTest.cc b/src/operation/iCTS/test/data_manager/lifecycle/CTSAPILifecycleTest.cc new file mode 100644 index 000000000..33d999366 --- /dev/null +++ b/src/operation/iCTS/test/data_manager/lifecycle/CTSAPILifecycleTest.cc @@ -0,0 +1,366 @@ +// *************************************************************************************** +// Copyright (c) 2023-2025 Peng Cheng Laboratory +// Copyright (c) 2023-2025 Institute of Computing Technology, Chinese Academy of Sciences +// Copyright (c) 2023-2025 Beijing Institute of Open Source Chip +// +// iEDA is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// +// See the Mulan PSL v2 for more details. +// *************************************************************************************** +/** + * @file CTSAPILifecycleTest.cc + * @author Dawn Li (dawnli619215645@gmail.com) + * @date 2026-08-10 + * @brief Public CTS destruction lifecycle and session-ownership tests. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CTSAPI.hh" +#include "IdbDesign.h" +#include "Logger.hh" +#include "Utility.hh" +#include "data_manager/DataManager.hh" +#include "data_manager/io/Wrapper.hh" +#include "idm.h" + +namespace icts_test { +namespace { + +auto MakeUniqueOutputDir(const std::string& label) -> std::filesystem::path +{ + const auto suffix = std::chrono::steady_clock::now().time_since_epoch().count(); + return std::filesystem::temp_directory_path() / ("icts_lifecycle_" + label + "_" + std::to_string(suffix)); +} + +void RemoveOutputDir(const std::filesystem::path& output_dir) +{ + std::error_code error_code; + std::filesystem::remove_all(output_dir, error_code); +} + +auto WriteTextFile(const std::filesystem::path& path, const std::string& content) -> bool +{ + std::ofstream stream(path); + stream << content; + return stream.good(); +} + +class ScopedMinimalExternalIdb +{ + public: + explicit ScopedMinimalExternalIdb(const std::string& label) : _output_dir(MakeUniqueOutputDir(label)) + { + std::filesystem::create_directories(_output_dir); + _config_path = _output_dir / "cts_config.json"; + const auto tech_lef_path = _output_dir / "minimal.lef"; + const auto def_path = _output_dir / "minimal.def"; + _work_dir = _output_dir / "cts_work"; + + constexpr auto tech_lef = R"lef(VERSION 5.8 ; +BUSBITCHARS "[]" ; +DIVIDERCHAR "/" ; +UNITS + DATABASE MICRONS 1000 ; +END UNITS +MANUFACTURINGGRID 0.001 ; +LAYER M1 + TYPE ROUTING ; + DIRECTION HORIZONTAL ; + PITCH 0.10 ; + WIDTH 0.05 ; + RESISTANCE RPERSQ 0.10 ; + CAPACITANCE CPERSQDIST 0.001 ; +END M1 +END LIBRARY +)lef"; + constexpr auto design_def = R"def(VERSION 5.8 ; +DIVIDERCHAR "/" ; +BUSBITCHARS "[]" ; +DESIGN icts_lifecycle ; +UNITS DISTANCE MICRONS 1000 ; +DIEAREA ( 0 0 ) ( 1000 1000 ) ; +COMPONENTS 0 ; +END COMPONENTS +PINS 0 ; +END PINS +NETS 0 ; +END NETS +END DESIGN +)def"; + + dmInst->reset(); + _ready = WriteTextFile(_config_path, "{}") && WriteTextFile(tech_lef_path, tech_lef) && WriteTextFile(def_path, design_def) + && dmInst->readLef(std::vector{tech_lef_path.string()}, true) && dmInst->readDef(def_path.string()); + if (_ready) { + dmInst->get_config().set_sdc_path(""); + } + } + + ~ScopedMinimalExternalIdb() + { + (void) icts::CTSAPI::destroyCTS(); + dmInst->reset(); + RemoveOutputDir(_output_dir); + } + + ScopedMinimalExternalIdb(const ScopedMinimalExternalIdb& rhs) = delete; + ScopedMinimalExternalIdb(ScopedMinimalExternalIdb&& rhs) = delete; + auto operator=(const ScopedMinimalExternalIdb& rhs) -> ScopedMinimalExternalIdb& = delete; + auto operator=(ScopedMinimalExternalIdb&& rhs) -> ScopedMinimalExternalIdb& = delete; + + auto ready() const -> bool { return _ready; } + auto configPath() const -> const std::filesystem::path& { return _config_path; } + auto workDir() const -> const std::filesystem::path& { return _work_dir; } + + private: + std::filesystem::path _output_dir; + std::filesystem::path _config_path; + std::filesystem::path _work_dir; + bool _ready = false; +}; + +auto Median(std::array samples) -> double +{ + std::ranges::sort(samples); + return (samples.at(1U) + samples.at(2U)) / 2.0; +} + +struct ExternalDesignFingerprint +{ + std::vector insts; + std::vector nets; + std::vector pins; + + auto operator==(const ExternalDesignFingerprint& rhs) const -> bool = default; +}; + +auto Fingerprint(idb::IdbDesign& design) -> ExternalDesignFingerprint +{ + ExternalDesignFingerprint fingerprint; + for (auto* inst : design.get_instance_list()->get_instance_list()) { + if (inst == nullptr) { + continue; + } + auto* coordinate = inst->get_coordinate(); + fingerprint.insts.push_back(inst->get_name() + "@" + std::to_string(coordinate == nullptr ? 0 : coordinate->get_x()) + "," + + std::to_string(coordinate == nullptr ? 0 : coordinate->get_y())); + for (auto* pin : inst->get_pin_list()->get_pin_list()) { + if (pin == nullptr) { + continue; + } + auto* location = pin->get_location(); + fingerprint.pins.push_back(inst->get_name() + "/" + pin->get_pin_name() + "->" + pin->get_net_name() + "@" + + std::to_string(location == nullptr ? 0 : location->get_x()) + "," + + std::to_string(location == nullptr ? 0 : location->get_y())); + } + } + for (auto* net : design.get_net_list()->get_net_list()) { + if (net == nullptr) { + continue; + } + std::vector connections; + for (auto* pin : net->get_instance_pin_list()->get_pin_list()) { + if (pin != nullptr && pin->get_instance() != nullptr) { + connections.push_back(pin->get_instance()->get_name() + "/" + pin->get_pin_name()); + } + } + std::ranges::sort(connections); + std::string record = net->get_net_name(); + for (const auto& connection : connections) { + record.append("|").append(connection); + } + fingerprint.nets.push_back(std::move(record)); + } + std::ranges::sort(fingerprint.insts); + std::ranges::sort(fingerprint.nets); + std::ranges::sort(fingerprint.pins); + return fingerprint; +} + +void PopulateRepresentativeSession(std::size_t inst_count) +{ + for (std::size_t index = 0U; index < inst_count; ++index) { + ASSERT_NE(CTSDM.getDesign().makeInst("session_inst_" + std::to_string(index)), nullptr); + } +} + +TEST(CTSAPILifecycleTest, DestroyIsSuccessfulBeforeInitializationAndWhenRepeated) +{ + const auto first = icts::CTSAPI::destroyCTS(); + const auto second = icts::CTSAPI::destroyCTS(); + + EXPECT_EQ(first.code, icts::CTSStatusCode::kOk); + EXPECT_EQ(second.code, icts::CTSStatusCode::kOk); + EXPECT_TRUE(icts::CTSAPI::lastStatus().ok()); + EXPECT_TRUE(icts::CTSAPI::outputClockTiming().empty()); +} + +TEST(CTSAPILifecycleTest, FailedInitializationUsesCanonicalCleanupAndLeavesAPIUninitialized) +{ + const auto output_dir = MakeUniqueOutputDir("failed_init"); + const auto status = icts::CTSAPI::init((output_dir / "missing_config.json").string(), output_dir.string()); + + EXPECT_EQ(status.code, icts::CTSStatusCode::kConfigError); + EXPECT_EQ(icts::CTSAPI::runCTS().code, icts::CTSStatusCode::kNotInitialized); + EXPECT_EQ(icts::CTSAPI::report(output_dir.string()).code, icts::CTSStatusCode::kNotInitialized); + EXPECT_TRUE(icts::CTSAPI::outputClockTiming().empty()); + EXPECT_TRUE(icts::CTSAPI::destroyCTS().ok()); + EXPECT_TRUE(icts::CTSAPI::destroyCTS().ok()); + + RemoveOutputDir(output_dir); +} + +TEST(CTSAPILifecycleTest, DestroyReleasesRepresentativeSessionAndFreshOwnerStartsEmpty) +{ + icts::DataManager::initInst(); + PopulateRepresentativeSession(256U); + ASSERT_NE(CTSDM.getDesign().findInst("session_inst_255"), nullptr); + + EXPECT_TRUE(icts::CTSAPI::destroyCTS().ok()); + EXPECT_TRUE(icts::CTSAPI::outputClockTiming().empty()); + + icts::DataManager::initInst(); + EXPECT_EQ(CTSDM.getState(), icts::CTSRunState::kEmpty); + EXPECT_TRUE(CTSDM.getDesign().get_insts().empty()); + EXPECT_EQ(CTSDM.getDesign().findInst("session_inst_255"), nullptr); +} + +TEST(CTSAPILifecycleTest, PublicInitializedSessionCanDestroyAndReinitialize) +{ + const ScopedMinimalExternalIdb external_idb("public_reinit"); + ASSERT_TRUE(external_idb.ready()); + + const auto first_init = icts::CTSAPI::init(external_idb.configPath().string(), external_idb.workDir().string()); + ASSERT_EQ(first_init.code, icts::CTSStatusCode::kOk) << first_init.message; + EXPECT_TRUE(icts::CTSAPI::outputClockTiming().empty()); + EXPECT_EQ(icts::CTSAPI::runCTS().code, icts::CTSStatusCode::kNoOp); + + EXPECT_TRUE(icts::CTSAPI::destroyCTS().ok()); + EXPECT_EQ(icts::CTSAPI::runCTS().code, icts::CTSStatusCode::kNotInitialized); + EXPECT_EQ(icts::CTSAPI::report(external_idb.workDir().string()).code, icts::CTSStatusCode::kNotInitialized); + + const auto second_init = icts::CTSAPI::init(external_idb.configPath().string(), external_idb.workDir().string()); + ASSERT_EQ(second_init.code, icts::CTSStatusCode::kOk) << second_init.message; + EXPECT_EQ(icts::CTSAPI::runCTS().code, icts::CTSStatusCode::kNoOp); + EXPECT_TRUE(icts::CTSAPI::destroyCTS().ok()); + EXPECT_TRUE(icts::CTSAPI::destroyCTS().ok()); +} + +TEST(CTSAPILifecycleTest, DestroyPreservesBorrowedExternalIdbObjectsAndConnectivity) +{ + icts::DataManager::initInst(); + idb::IdbDesign external_design; + auto* driver_inst = external_design.get_instance_list()->add_instance("cts_driver"); + auto* load_inst = external_design.get_instance_list()->add_instance("cts_load"); + ASSERT_NE(driver_inst, nullptr); + ASSERT_NE(load_inst, nullptr); + driver_inst->set_coodinate(100, 200, false); + load_inst->set_coodinate(300, 400, false); + auto* driver_pin = driver_inst->addPin("Y"); + auto* load_pin = load_inst->addPin("A"); + ASSERT_NE(driver_pin, nullptr); + ASSERT_NE(load_pin, nullptr); + driver_pin->set_location(110, 210); + load_pin->set_location(310, 410); + + auto* clock_net = external_design.get_net_list()->add_net("cts_clock_net", idb::IdbConnectType::kClock); + ASSERT_NE(clock_net, nullptr); + clock_net->add_instance_pin(driver_pin); + clock_net->add_instance_pin(load_pin); + driver_pin->set_net(clock_net); + driver_pin->set_net_name(clock_net->get_net_name()); + load_pin->set_net(clock_net); + load_pin->set_net_name(clock_net->get_net_name()); + + CTSDM.getWrapper().set_idb_design(&external_design); + const auto before = Fingerprint(external_design); + + EXPECT_TRUE(icts::CTSAPI::destroyCTS().ok()); + + const auto after = Fingerprint(external_design); + EXPECT_EQ(after, before); + ASSERT_EQ(after.insts.size(), 2U); + ASSERT_EQ(after.nets.size(), 1U); + ASSERT_EQ(after.pins.size(), 2U); +} + +TEST(CTSAPILifecycleTest, CompletionIsLoggedBeforeLoggerDestruction) +{ + icts::Logger::initInst(); + icts::DataManager::initInst(); + const auto output_dir = MakeUniqueOutputDir("log_order"); + std::filesystem::create_directories(output_dir); + const auto log_path = output_dir / "cts.log"; + CTSLOG.openLogFileStream(log_path.string()); + PopulateRepresentativeSession(32U); + + ASSERT_TRUE(icts::CTSAPI::destroyCTS().ok()); + + std::ifstream log_file(log_path); + const std::string log_text((std::istreambuf_iterator(log_file)), std::istreambuf_iterator()); + const auto start_pos = log_text.find("Starting CTS destruction"); + const auto completion_pos = log_text.find("Completed CTS destruction"); + EXPECT_NE(start_pos, std::string::npos); + EXPECT_NE(completion_pos, std::string::npos); + EXPECT_LT(start_pos, completion_pos); + + RemoveOutputDir(output_dir); +} + +TEST(CTSAPILifecycleTest, TenCyclesRemainWithinApprovedPostDestroyRssThreshold) +{ +#ifdef __linux__ + const ScopedMinimalExternalIdb external_idb("ten_cycles"); + ASSERT_TRUE(external_idb.ready()); + std::array post_destroy_rss_mb{}; + for (std::size_t cycle = 0U; cycle < post_destroy_rss_mb.size(); ++cycle) { + const auto init_status = icts::CTSAPI::init(external_idb.configPath().string(), external_idb.workDir().string()); + ASSERT_EQ(init_status.code, icts::CTSStatusCode::kOk) << "cycle " << cycle << ": " << init_status.message; + EXPECT_TRUE(icts::CTSAPI::outputClockTiming().empty()); + EXPECT_EQ(icts::CTSAPI::runCTS().code, icts::CTSStatusCode::kNoOp); + PopulateRepresentativeSession(4096U); + ASSERT_TRUE(icts::CTSAPI::destroyCTS().ok()); + ASSERT_TRUE(icts::CTSAPI::destroyCTS().ok()); + const auto rss_mb = icts::Utility::currentRssMb(); + ASSERT_TRUE(rss_mb.has_value()); + post_destroy_rss_mb.at(cycle) = rss_mb.value_or(0.0); + } + + const std::array early_samples = {post_destroy_rss_mb.at(1U), post_destroy_rss_mb.at(2U), post_destroy_rss_mb.at(3U), post_destroy_rss_mb.at(4U)}; + const std::array late_samples = {post_destroy_rss_mb.at(6U), post_destroy_rss_mb.at(7U), post_destroy_rss_mb.at(8U), post_destroy_rss_mb.at(9U)}; + const double early_median_mb = Median(early_samples); + const double late_median_mb = Median(late_samples); + constexpr double absolute_tolerance_mb = 16.0; + const double allowed_growth_mb = std::max(absolute_tolerance_mb, early_median_mb * 0.05); + EXPECT_LE(late_median_mb, early_median_mb + allowed_growth_mb); + + bool sustained_growth = true; + for (std::size_t index = 2U; index < post_destroy_rss_mb.size(); ++index) { + sustained_growth = sustained_growth && post_destroy_rss_mb.at(index) > post_destroy_rss_mb.at(index - 1U); + } + EXPECT_FALSE(sustained_growth); +#else + GTEST_SKIP() << "Linux /proc RSS sampling is unavailable."; +#endif +} + +} // namespace +} // namespace icts_test diff --git a/src/operation/iCTS/test/data_manager/lifecycle/CTSProcessExitTest.cc b/src/operation/iCTS/test/data_manager/lifecycle/CTSProcessExitTest.cc new file mode 100644 index 000000000..0cad84c07 --- /dev/null +++ b/src/operation/iCTS/test/data_manager/lifecycle/CTSProcessExitTest.cc @@ -0,0 +1,32 @@ +// *************************************************************************************** +// Copyright (c) 2023-2025 Peng Cheng Laboratory +// Copyright (c) 2023-2025 Institute of Computing Technology, Chinese Academy of Sciences +// Copyright (c) 2023-2025 Beijing Institute of Open Source Chip +// +// iEDA is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// +// See the Mulan PSL v2 for more details. +// *************************************************************************************** +/** + * @file CTSProcessExitTest.cc + * @author Dawn Li (dawnli619215645@gmail.com) + * @date 2026-08-10 + * @brief Process-exit fallback test for CTS static RAII owners. + */ + +#include "Logger.hh" +#include "data_manager/DataManager.hh" + +auto main() -> int +{ + icts::Logger::initInst(); + icts::DataManager::initInst(); + return CTSDM.getDesign().makeInst("process_exit_owned_inst") == nullptr ? 1 : 0; +} diff --git a/src/operation/iCTS/test/main.cc b/src/operation/iCTS/test/main.cc index 88eb46e16..1bf1c2ee7 100644 --- a/src/operation/iCTS/test/main.cc +++ b/src/operation/iCTS/test/main.cc @@ -24,6 +24,7 @@ #include #include +#include #include "Logger.hh" #include "data_manager/DataManager.hh" @@ -31,19 +32,30 @@ namespace icts_test { namespace { +void ResetTestState() +{ + icts::Logger::initInst(); + icts::DataManager::initInst(); + CTSLOG.closeLogFileStream(); + CTSDM.reset(); +} + +void DestroyTestState() +{ + icts::DataManager::destroyInst(); + icts::Logger::destroyInst(); +} + +auto IsLifecycleTest(const ::testing::TestInfo& test_info) -> bool +{ + return std::string_view(test_info.test_suite_name()) == "CTSAPILifecycleTest"; +} + class DataManagerTestListener final : public ::testing::EmptyTestEventListener { public: - void OnTestStart([[maybe_unused]] const ::testing::TestInfo& test_info) override - { - CTSLOG.closeLogFileStream(); - CTSDM.reset(); - } - void OnTestEnd([[maybe_unused]] const ::testing::TestInfo& test_info) override - { - CTSLOG.closeLogFileStream(); - CTSDM.reset(); - } + void OnTestStart(const ::testing::TestInfo& test_info) override { IsLifecycleTest(test_info) ? DestroyTestState() : ResetTestState(); } + void OnTestEnd(const ::testing::TestInfo& test_info) override { IsLifecycleTest(test_info) ? DestroyTestState() : ResetTestState(); } }; } // namespace @@ -52,8 +64,6 @@ class DataManagerTestListener final : public ::testing::EmptyTestEventListener auto main(int argc, char** argv) -> int { ::testing::InitGoogleTest(&argc, argv); - icts::Logger::initInst(); - icts::DataManager::initInst(); auto& listeners = ::testing::UnitTest::GetInstance()->listeners(); auto data_manager_listener = std::make_unique(); @@ -61,7 +71,6 @@ auto main(int argc, char** argv) -> int const int result = RUN_ALL_TESTS(); (void) listeners.Release(data_manager_listener.get()); - icts::DataManager::destroyInst(); - icts::Logger::destroyInst(); + icts_test::DestroyTestState(); return result; } diff --git a/src/operation/iCTS/test/toolkit/CMakeLists.txt b/src/operation/iCTS/test/toolkit/CMakeLists.txt index 01e5e9a11..3108410ec 100644 --- a/src/operation/iCTS/test/toolkit/CMakeLists.txt +++ b/src/operation/iCTS/test/toolkit/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(logger) add_subdirectory(monitor) +add_subdirectory(utility) add_subdirectory(io) add_subdirectory(visualization) diff --git a/src/operation/iCTS/test/toolkit/utility/CMakeLists.txt b/src/operation/iCTS/test/toolkit/utility/CMakeLists.txt new file mode 100644 index 000000000..e3346e382 --- /dev/null +++ b/src/operation/iCTS/test/toolkit/utility/CMakeLists.txt @@ -0,0 +1,5 @@ +icts_add_test_executable( + icts_test_toolkit_utility + SOURCES + ${ICTS_TEST}/toolkit/utility/UtilityTest.cc +) diff --git a/src/operation/iCTS/test/toolkit/utility/UtilityTest.cc b/src/operation/iCTS/test/toolkit/utility/UtilityTest.cc new file mode 100644 index 000000000..8faadc659 --- /dev/null +++ b/src/operation/iCTS/test/toolkit/utility/UtilityTest.cc @@ -0,0 +1,57 @@ +// *************************************************************************************** +// Copyright (c) 2023-2025 Peng Cheng Laboratory +// Copyright (c) 2023-2025 Institute of Computing Technology, Chinese Academy of Sciences +// Copyright (c) 2023-2025 Beijing Institute of Open Source Chip +// +// iEDA is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// +// See the Mulan PSL v2 for more details. +// *************************************************************************************** +/** + * @file UtilityTest.cc + * @author Dawn Li (dawnli619215645@gmail.com) + * @date 2026-08-10 + * @brief Tests for CTS process-memory sampling and allocator release evidence. + */ + +#include + +#include "Utility.hh" + +namespace icts_test { +namespace { + +TEST(UtilityTest, CurrentRssReportsPlatformAvailability) +{ + const auto rss_mb = icts::Utility::currentRssMb(); +#ifdef __linux__ + ASSERT_TRUE(rss_mb.has_value()); + EXPECT_GT(rss_mb.value_or(0.0), 0.0); +#else + EXPECT_FALSE(rss_mb.has_value()); +#endif +} + +TEST(UtilityTest, ReleaseMemoryReportsEvidenceWithoutRequiringRssReduction) +{ + const auto stats = icts::Utility::releaseMemory(); +#if defined(__linux__) && defined(__GLIBC__) + EXPECT_TRUE(stats.supported); + EXPECT_GT(stats.rss_before_mb, 0.0); + EXPECT_GT(stats.rss_after_mb, 0.0); +#else + EXPECT_FALSE(stats.supported); + EXPECT_DOUBLE_EQ(stats.rss_before_mb, 0.0); + EXPECT_DOUBLE_EQ(stats.rss_after_mb, 0.0); +#endif +} + +} // namespace +} // namespace icts_test From a99ad21ad97ae40a9474581cff578dd8424f1e87 Mon Sep 17 00:00:00 2001 From: dawnli139 Date: Tue, 11 Aug 2026 10:56:22 +0800 Subject: [PATCH 2/3] perf(icts): optimize FastSTA batch timing updates --- .../data_manager/adapter/fast_sta/FastSTA.cc | 38 ++- .../clock_sizing/FastSTAIncremental.cc | 236 ++++++++++++++-- .../clock_sizing/FastSTAIncremental.hh | 3 + .../fast_sta/clock_state/FastSTAClockState.hh | 2 + .../fast_sta/clock_tree/FastSTAClockTree.cc | 13 +- .../adapter/fast_sta/power/FastSTAPower.cc | 9 + .../adapter/fast_sta/timing/FastSTATiming.cc | 37 ++- .../adapter/fast_sta/FastSTATest.cc | 256 ++++++++++++++++++ 8 files changed, 544 insertions(+), 50 deletions(-) diff --git a/src/operation/iCTS/source/data_manager/adapter/fast_sta/FastSTA.cc b/src/operation/iCTS/source/data_manager/adapter/fast_sta/FastSTA.cc index 1834b770e..45a8d6b40 100644 --- a/src/operation/iCTS/source/data_manager/adapter/fast_sta/FastSTA.cc +++ b/src/operation/iCTS/source/data_manager/adapter/fast_sta/FastSTA.cc @@ -104,21 +104,31 @@ auto makeClockTreeTopology(const FastStaClockContext& context) -> FastStaClockTr topology.source_node_id = context.source_node_id; topology.parent_by_node.assign(context.nodes.size(), kInvalidFastStaNodeId); - std::unordered_map input_by_inst; - input_by_inst.reserve(context.nodes.size()); - for (FastStaNodeId node_id = 0U; node_id < context.nodes.size(); ++node_id) { - const auto& node = context.nodes.at(node_id); - if (node.kind == FastStaNodeKind::kBufferInput && !node.inst_name.empty()) { - input_by_inst[node.inst_name] = node_id; + const auto find_buffer_input = [&](const std::string& inst_name) -> FastStaNodeId { + if (const auto indexed = context.buffer_input_node_id_by_inst.find(inst_name); indexed != context.buffer_input_node_id_by_inst.end()) { + if (indexed->second < context.nodes.size()) { + const auto& node = context.nodes.at(indexed->second); + if (node.kind == FastStaNodeKind::kBufferInput && node.inst_name == inst_name) { + return indexed->second; + } + } + return kInvalidFastStaNodeId; } - } + for (FastStaNodeId node_id = 0U; node_id < context.nodes.size(); ++node_id) { + const auto& node = context.nodes.at(node_id); + if (node.kind == FastStaNodeKind::kBufferInput && node.inst_name == inst_name) { + return node_id; + } + } + return kInvalidFastStaNodeId; + }; for (FastStaNodeId node_id = 0U; node_id < context.nodes.size(); ++node_id) { const auto& node = context.nodes.at(node_id); if (node.kind == FastStaNodeKind::kBufferOutput) { - const auto input_iter = input_by_inst.find(node.inst_name); - if (input_iter != input_by_inst.end()) { - topology.parent_by_node.at(node_id) = input_iter->second; + const auto input_node_id = find_buffer_input(node.inst_name); + if (input_node_id != kInvalidFastStaNodeId) { + topology.parent_by_node.at(node_id) = input_node_id; } continue; } @@ -280,12 +290,14 @@ auto FastSTA::changeBufferMastersTimingOnly(FastStaClockId clock_id, const std:: if (changes.empty()) { return context->timing_valid; } - if (!FastStaIncremental::changeBufferMasters(*context, changes)) { + const auto dirty_region = FastStaIncremental::changeBufferMastersIncremental(*context, changes); + if (!dirty_region.has_value() || !FastStaTiming::updateRegion(*context, *dirty_region)) { + context->timing_valid = false; + context->power_valid = false; return false; } - const bool timing_updated = FastStaTiming::update(*context); context->power_valid = false; - return timing_updated; + return context->timing_valid; } auto FastSTA::updateTiming(FastStaClockId clock_id) -> bool diff --git a/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_sizing/FastSTAIncremental.cc b/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_sizing/FastSTAIncremental.cc index d7ca9b4ea..c34005fda 100644 --- a/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_sizing/FastSTAIncremental.cc +++ b/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_sizing/FastSTAIncremental.cc @@ -23,10 +23,11 @@ #include "FastSTAIncremental.hh" +#include +#include #include #include -#include -#include +#include #include #include "FastSTAClockState.hh" @@ -50,6 +51,15 @@ auto normalizeBufferInputNodeId(const FastStaClockContext& context, FastStaNodeI if (node.kind != FastStaNodeKind::kBufferOutput || node.inst_name.empty()) { return kInvalidFastStaNodeId; } + if (const auto indexed = context.buffer_input_node_id_by_inst.find(node.inst_name); indexed != context.buffer_input_node_id_by_inst.end()) { + if (indexed->second < context.nodes.size()) { + const auto& input_node = context.nodes.at(indexed->second); + if (input_node.kind == FastStaNodeKind::kBufferInput && input_node.inst_name == node.inst_name) { + return indexed->second; + } + } + return kInvalidFastStaNodeId; + } for (FastStaNodeId candidate_id = 0U; candidate_id < context.nodes.size(); ++candidate_id) { const auto& candidate = context.nodes.at(candidate_id); if (candidate.kind == FastStaNodeKind::kBufferInput && candidate.inst_name == node.inst_name) { @@ -59,27 +69,54 @@ auto normalizeBufferInputNodeId(const FastStaClockContext& context, FastStaNodeI return kInvalidFastStaNodeId; } -auto markReachableFromNode(const FastStaClockContext& context, FastStaNodeId node_id, FastStaDirtyRegion& dirty_region, - std::unordered_set& node_seen, std::unordered_set& net_seen) -> void +auto normalizeBufferOutputNodeId(const FastStaClockContext& context, FastStaNodeId node_id) -> FastStaNodeId +{ + if (node_id >= context.nodes.size()) { + return kInvalidFastStaNodeId; + } + const auto& node = context.nodes.at(node_id); + if (node.kind == FastStaNodeKind::kBufferOutput) { + return node_id; + } + if (node.kind != FastStaNodeKind::kBufferInput || node.inst_name.empty()) { + return kInvalidFastStaNodeId; + } + if (const auto indexed = context.buffer_output_node_id_by_inst.find(node.inst_name); indexed != context.buffer_output_node_id_by_inst.end()) { + if (indexed->second < context.nodes.size()) { + const auto& output_node = context.nodes.at(indexed->second); + if (output_node.kind == FastStaNodeKind::kBufferOutput && output_node.inst_name == node.inst_name) { + return indexed->second; + } + } + return kInvalidFastStaNodeId; + } + for (FastStaNodeId candidate_id = 0U; candidate_id < context.nodes.size(); ++candidate_id) { + const auto& candidate = context.nodes.at(candidate_id); + if (candidate.kind == FastStaNodeKind::kBufferOutput && candidate.inst_name == node.inst_name) { + return candidate_id; + } + } + return kInvalidFastStaNodeId; +} + +auto markReachableFromNode(const FastStaClockContext& context, FastStaNodeId node_id, FastStaDirtyRegion& dirty_region, std::vector& node_seen, + std::vector& net_seen) -> void { std::vector pending_nodes{node_id}; while (!pending_nodes.empty()) { const auto current_node_id = pending_nodes.back(); pending_nodes.pop_back(); - if (current_node_id >= context.nodes.size() || node_seen.contains(current_node_id)) { + if (current_node_id >= context.nodes.size() || node_seen.at(current_node_id) != 0U) { continue; } - node_seen.insert(current_node_id); + node_seen.at(current_node_id) = 1U; dirty_region.node_ids.push_back(current_node_id); const auto& node = context.nodes.at(current_node_id); if (node.kind == FastStaNodeKind::kBufferInput) { - for (FastStaNodeId output_id = 0U; output_id < context.nodes.size(); ++output_id) { - const auto& output_node = context.nodes.at(output_id); - if (output_node.kind == FastStaNodeKind::kBufferOutput && output_node.inst_name == node.inst_name) { - pending_nodes.push_back(output_id); - break; - } + const auto output_id = normalizeBufferOutputNodeId(context, current_node_id); + if (output_id != kInvalidFastStaNodeId) { + pending_nodes.push_back(output_id); } continue; } @@ -88,8 +125,8 @@ auto markReachableFromNode(const FastStaClockContext& context, FastStaNodeId nod if (net_id >= context.nets.size()) { continue; } - if (!net_seen.contains(net_id)) { - net_seen.insert(net_id); + if (net_seen.at(net_id) == 0U) { + net_seen.at(net_id) = 1U; dirty_region.net_ids.push_back(net_id); } for (const auto load_node_id : context.nets.at(net_id).load_node_ids) { @@ -99,11 +136,10 @@ auto markReachableFromNode(const FastStaClockContext& context, FastStaNodeId nod } } -auto collectDirtyRegion(const FastStaClockContext& context, FastStaNodeId changed_input_node_id) -> FastStaDirtyRegion +auto dirtyRegionStartNode(const FastStaClockContext& context, FastStaNodeId changed_input_node_id) -> FastStaNodeId { - FastStaDirtyRegion dirty_region; if (changed_input_node_id >= context.nodes.size()) { - return dirty_region; + return kInvalidFastStaNodeId; } auto start_node_id = changed_input_node_id; @@ -119,15 +155,83 @@ auto collectDirtyRegion(const FastStaClockContext& context, FastStaNodeId change start_node_id = incoming_driver_id; } } + return start_node_id; +} + +auto parentNodeId(const FastStaClockContext& context, FastStaNodeId node_id) -> FastStaNodeId +{ + if (node_id >= context.nodes.size()) { + return kInvalidFastStaNodeId; + } + const auto& node = context.nodes.at(node_id); + if (node.kind == FastStaNodeKind::kBufferOutput) { + return normalizeBufferInputNodeId(context, node_id); + } + if (node.incoming_net_id < context.nets.size()) { + const auto parent_id = context.nets.at(node.incoming_net_id).driver_node_id; + return parent_id < context.nodes.size() ? parent_id : kInvalidFastStaNodeId; + } + return kInvalidFastStaNodeId; +} + +auto lowestCommonAncestor(const FastStaClockContext& context, FastStaNodeId lhs, FastStaNodeId rhs) -> FastStaNodeId +{ + if (lhs >= context.nodes.size() || rhs >= context.nodes.size()) { + return kInvalidFastStaNodeId; + } + std::vector lhs_ancestors(context.nodes.size(), 0U); + auto current = lhs; + for (std::size_t step = 0U; current < context.nodes.size() && step <= context.nodes.size(); ++step) { + if (lhs_ancestors.at(current) != 0U) { + return kInvalidFastStaNodeId; + } + lhs_ancestors.at(current) = 1U; + current = parentNodeId(context, current); + } + + std::vector rhs_seen(context.nodes.size(), 0U); + current = rhs; + for (std::size_t step = 0U; current < context.nodes.size() && step <= context.nodes.size(); ++step) { + if (lhs_ancestors.at(current) != 0U) { + return current; + } + if (rhs_seen.at(current) != 0U) { + return kInvalidFastStaNodeId; + } + rhs_seen.at(current) = 1U; + current = parentNodeId(context, current); + } + return kInvalidFastStaNodeId; +} + +auto collectDirtyRegionFromStart(const FastStaClockContext& context, FastStaNodeId start_node_id) -> FastStaDirtyRegion +{ + FastStaDirtyRegion dirty_region; + if (start_node_id >= context.nodes.size()) { + return dirty_region; + } + if (context.nodes.at(start_node_id).kind == FastStaNodeKind::kBufferOutput) { + start_node_id = normalizeBufferInputNodeId(context, start_node_id); + if (start_node_id == kInvalidFastStaNodeId) { + return dirty_region; + } + } dirty_region.valid = true; dirty_region.start_node_id = start_node_id; - std::unordered_set node_seen; - std::unordered_set net_seen; + std::vector node_seen(context.nodes.size(), 0U); + std::vector net_seen(context.nets.size(), 0U); markReachableFromNode(context, start_node_id, dirty_region, node_seen, net_seen); return dirty_region; } +auto collectDirtyRegion(const FastStaClockContext& context, FastStaNodeId changed_input_node_id) -> FastStaDirtyRegion +{ + return collectDirtyRegionFromStart(context, dirtyRegionStartNode(context, changed_input_node_id)); +} + +auto prepareBufferMasterChanges(FastStaClockContext& context, const std::vector& changes) -> bool; + auto applyBufferMasterChange(FastStaClockContext& context, FastStaNodeId node_id, std::string_view cell_master, bool invalidate_context) -> FastStaNodeId { if (node_id >= context.nodes.size()) { @@ -156,16 +260,17 @@ auto applyBufferMasterChange(FastStaClockContext& context, FastStaNodeId node_id } context.liberty_cell_by_master.emplace(target_master, *liberty_cell); } - const auto inst_name = context.nodes.at(input_node_id).inst_name; - for (auto& candidate : context.nodes) { - if (candidate.inst_name == inst_name && (candidate.kind == FastStaNodeKind::kBufferInput || candidate.kind == FastStaNodeKind::kBufferOutput)) { - candidate.cell_master = target_master; - if (candidate.kind == FastStaNodeKind::kBufferInput) { - candidate.input_cap_pf = context.liberty_cell_by_master.at(target_master).input_cap_pf; - candidate.max_slew_ns = context.liberty_cell_by_master.at(target_master).input_slew_limit_ns; - } - } + const auto output_node_id = normalizeBufferOutputNodeId(context, input_node_id); + if (output_node_id == kInvalidFastStaNodeId) { + CTSLOG.warn(Loc::current(), "FastStaIncremental: buffer master change skipped because buffer output node is unavailable for \"", node.name, "\"."); + return kInvalidFastStaNodeId; } + auto& input_node = context.nodes.at(input_node_id); + auto& output_node = context.nodes.at(output_node_id); + input_node.cell_master = target_master; + input_node.input_cap_pf = context.liberty_cell_by_master.at(target_master).input_cap_pf; + input_node.max_slew_ns = context.liberty_cell_by_master.at(target_master).input_slew_limit_ns; + output_node.cell_master = target_master; if (invalidate_context) { context.timing_valid = false; context.power_valid = false; @@ -188,6 +293,10 @@ auto validateBufferMasterChange(const FastStaClockContext& context, const FastSt CTSLOG.warn(Loc::current(), "FastStaIncremental: buffer master change skipped because buffer input node is unavailable for \"", node.name, "\"."); return false; } + if (normalizeBufferOutputNodeId(context, change.node_id) == kInvalidFastStaNodeId) { + CTSLOG.warn(Loc::current(), "FastStaIncremental: buffer master change skipped because buffer output node is unavailable for \"", node.name, "\"."); + return false; + } if (change.cell_master.empty()) { CTSLOG.warn(Loc::current(), "FastStaIncremental: buffer master change skipped because target master is empty for \"", node.name, "\"."); return false; @@ -195,6 +304,35 @@ auto validateBufferMasterChange(const FastStaClockContext& context, const FastSt return true; } +auto prepareBufferMasterChanges(FastStaClockContext& context, const std::vector& changes) -> bool +{ + if (!FastStaIncremental::validateBufferMasterChanges(context, changes)) { + return false; + } + std::vector> missing_cells; + for (const auto& change : changes) { + if (context.liberty_cell_by_master.contains(change.cell_master)) { + continue; + } + if (context.wrapper == nullptr) { + CTSLOG.error(Loc::current(), "FastStaIncremental: Wrapper is unavailable."); + } + if (std::ranges::any_of(missing_cells, [&](const auto& cell) -> bool { return cell.first == change.cell_master; })) { + continue; + } + const auto liberty_cell = FastStaLiberty::extractBufferCell(*context.wrapper, change.cell_master); + if (!liberty_cell.has_value()) { + CTSLOG.warn(Loc::current(), "FastStaIncremental: required Liberty data is unavailable for target master \"", change.cell_master, "\"."); + return false; + } + missing_cells.emplace_back(change.cell_master, *liberty_cell); + } + for (auto& [cell_master, liberty_cell] : missing_cells) { + context.liberty_cell_by_master.emplace(std::move(cell_master), std::move(liberty_cell)); + } + return true; +} + } // namespace auto FastStaIncremental::changeBufferMaster(FastStaClockContext& context, FastStaNodeId node_id, std::string_view cell_master) -> bool @@ -202,13 +340,21 @@ auto FastStaIncremental::changeBufferMaster(FastStaClockContext& context, FastSt return applyBufferMasterChange(context, node_id, cell_master, true) != kInvalidFastStaNodeId; } -auto FastStaIncremental::changeBufferMasters(FastStaClockContext& context, const std::vector& changes) -> bool +auto FastStaIncremental::validateBufferMasterChanges(const FastStaClockContext& context, const std::vector& changes) -> bool { for (const auto& change : changes) { if (!validateBufferMasterChange(context, change)) { return false; } } + return true; +} + +auto FastStaIncremental::changeBufferMasters(FastStaClockContext& context, const std::vector& changes) -> bool +{ + if (!prepareBufferMasterChanges(context, changes)) { + return false; + } for (const auto& change : changes) { if (applyBufferMasterChange(context, change.node_id, change.cell_master, false) == kInvalidFastStaNodeId) { context.timing_valid = false; @@ -221,6 +367,38 @@ auto FastStaIncremental::changeBufferMasters(FastStaClockContext& context, const return true; } +auto FastStaIncremental::changeBufferMastersIncremental(FastStaClockContext& context, const std::vector& changes) + -> std::optional +{ + if (changes.empty() || !prepareBufferMasterChanges(context, changes)) { + return std::nullopt; + } + auto common_start_node_id = kInvalidFastStaNodeId; + for (const auto& change : changes) { + const auto input_node_id = normalizeBufferInputNodeId(context, change.node_id); + const auto start_node_id = dirtyRegionStartNode(context, input_node_id); + if (start_node_id == kInvalidFastStaNodeId) { + return std::nullopt; + } + common_start_node_id = common_start_node_id == kInvalidFastStaNodeId ? start_node_id : lowestCommonAncestor(context, common_start_node_id, start_node_id); + if (common_start_node_id == kInvalidFastStaNodeId) { + return std::nullopt; + } + } + auto dirty_region = collectDirtyRegionFromStart(context, common_start_node_id); + if (!dirty_region.valid) { + return std::nullopt; + } + for (const auto& change : changes) { + if (applyBufferMasterChange(context, change.node_id, change.cell_master, false) == kInvalidFastStaNodeId) { + context.timing_valid = false; + context.power_valid = false; + return std::nullopt; + } + } + return dirty_region; +} + auto FastStaIncremental::changeBufferMasterIncremental(FastStaClockContext& context, FastStaNodeId node_id, std::string_view cell_master) -> std::optional { diff --git a/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_sizing/FastSTAIncremental.hh b/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_sizing/FastSTAIncremental.hh index 00afa64ca..e787ad488 100644 --- a/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_sizing/FastSTAIncremental.hh +++ b/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_sizing/FastSTAIncremental.hh @@ -40,7 +40,10 @@ class FastStaIncremental FastStaIncremental() = delete; static auto changeBufferMaster(FastStaClockContext& context, FastStaNodeId node_id, std::string_view cell_master) -> bool; + static auto validateBufferMasterChanges(const FastStaClockContext& context, const std::vector& changes) -> bool; static auto changeBufferMasters(FastStaClockContext& context, const std::vector& changes) -> bool; + static auto changeBufferMastersIncremental(FastStaClockContext& context, const std::vector& changes) + -> std::optional; static auto changeBufferMasterIncremental(FastStaClockContext& context, FastStaNodeId node_id, std::string_view cell_master) -> std::optional; }; diff --git a/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_state/FastSTAClockState.hh b/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_state/FastSTAClockState.hh index a86f85140..634856c0e 100644 --- a/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_state/FastSTAClockState.hh +++ b/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_state/FastSTAClockState.hh @@ -91,6 +91,8 @@ struct FastStaClockContext std::vector nodes; std::vector nets; std::unordered_map node_id_by_name; + std::unordered_map buffer_input_node_id_by_inst; + std::unordered_map buffer_output_node_id_by_inst; std::unordered_map, FastStaNodeId, FastStaPointKeyHash> node_id_by_location; std::unordered_map net_id_by_name; std::unordered_map liberty_cell_by_master; diff --git a/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_tree/FastSTAClockTree.cc b/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_tree/FastSTAClockTree.cc index ca64c7c6c..33facd8ff 100644 --- a/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_tree/FastSTAClockTree.cc +++ b/src/operation/iCTS/source/data_manager/adapter/fast_sta/clock_tree/FastSTAClockTree.cc @@ -81,16 +81,25 @@ auto appendPinNode(const Clock& clock, Pin* pin, FastStaClockContext& context) - const auto node_id = context.nodes.size(); context.node_id_by_name[node_name] = node_id; context.node_id_by_location.emplace(makeLocationKey(location), node_id); + const auto node_kind = makeNodeKind(clock, pin); + const auto inst_name = inst != nullptr ? inst->get_name() : std::string{}; context.nodes.push_back(FastStaNode{ - .kind = makeNodeKind(clock, pin), + .kind = node_kind, .name = node_name, - .inst_name = inst != nullptr ? inst->get_name() : std::string{}, + .inst_name = inst_name, .pin_name = pin->get_name(), .cell_master = inst != nullptr ? inst->get_cell_master() : std::string{}, .location = location, .output_net_ids = {}, .timing = {}, }); + if (!inst_name.empty()) { + if (node_kind == FastStaNodeKind::kBufferInput) { + context.buffer_input_node_id_by_inst[inst_name] = node_id; + } else if (node_kind == FastStaNodeKind::kBufferOutput) { + context.buffer_output_node_id_by_inst[inst_name] = node_id; + } + } return node_id; } diff --git a/src/operation/iCTS/source/data_manager/adapter/fast_sta/power/FastSTAPower.cc b/src/operation/iCTS/source/data_manager/adapter/fast_sta/power/FastSTAPower.cc index 8869bd7f1..a9a08d94f 100644 --- a/src/operation/iCTS/source/data_manager/adapter/fast_sta/power/FastSTAPower.cc +++ b/src/operation/iCTS/source/data_manager/adapter/fast_sta/power/FastSTAPower.cc @@ -73,6 +73,15 @@ auto resolveVoltage(const FastStaClockContext& context) -> std::optional auto findBufferInputNode(const FastStaClockContext& context, const FastStaNode& output_node) -> FastStaNodeId { + if (const auto indexed = context.buffer_input_node_id_by_inst.find(output_node.inst_name); indexed != context.buffer_input_node_id_by_inst.end()) { + if (indexed->second < context.nodes.size()) { + const auto& input_node = context.nodes.at(indexed->second); + if (input_node.kind == FastStaNodeKind::kBufferInput && input_node.inst_name == output_node.inst_name) { + return indexed->second; + } + } + return kInvalidFastStaNodeId; + } for (FastStaNodeId node_id = 0U; node_id < context.nodes.size(); ++node_id) { const auto& node = context.nodes.at(node_id); if (node.kind == FastStaNodeKind::kBufferInput && node.inst_name == output_node.inst_name) { diff --git a/src/operation/iCTS/source/data_manager/adapter/fast_sta/timing/FastSTATiming.cc b/src/operation/iCTS/source/data_manager/adapter/fast_sta/timing/FastSTATiming.cc index f4a47dd69..b3bc7768f 100644 --- a/src/operation/iCTS/source/data_manager/adapter/fast_sta/timing/FastSTATiming.cc +++ b/src/operation/iCTS/source/data_manager/adapter/fast_sta/timing/FastSTATiming.cc @@ -49,6 +49,15 @@ auto findBufferInputNode(const FastStaClockContext& context, const FastStaNode& if (output_node.inst_name.empty()) { return kInvalidFastStaNodeId; } + if (const auto indexed = context.buffer_input_node_id_by_inst.find(output_node.inst_name); indexed != context.buffer_input_node_id_by_inst.end()) { + if (indexed->second < context.nodes.size()) { + const auto& input_node = context.nodes.at(indexed->second); + if (input_node.kind == FastStaNodeKind::kBufferInput && input_node.inst_name == output_node.inst_name) { + return indexed->second; + } + } + return kInvalidFastStaNodeId; + } for (FastStaNodeId node_id = 0U; node_id < context.nodes.size(); ++node_id) { const auto& node = context.nodes.at(node_id); if (node.kind == FastStaNodeKind::kBufferInput && node.inst_name == output_node.inst_name) { @@ -207,7 +216,23 @@ auto hasCompleteSinkTiming(const FastStaClockContext& context) -> bool auto propagateReadyQueue(FastStaClockContext& context, std::queue& ready_nodes) -> void { - const auto output_by_inst = mapBufferOutputByInst(context); + std::unordered_map fallback_output_by_inst; + const auto find_buffer_output = [&](const FastStaNode& input_node) -> FastStaNodeId { + if (const auto indexed = context.buffer_output_node_id_by_inst.find(input_node.inst_name); indexed != context.buffer_output_node_id_by_inst.end()) { + if (indexed->second < context.nodes.size()) { + const auto& output_node = context.nodes.at(indexed->second); + if (output_node.kind == FastStaNodeKind::kBufferOutput && output_node.inst_name == input_node.inst_name) { + return indexed->second; + } + } + return kInvalidFastStaNodeId; + } + if (fallback_output_by_inst.empty()) { + fallback_output_by_inst = mapBufferOutputByInst(context); + } + const auto fallback = fallback_output_by_inst.find(input_node.inst_name); + return fallback == fallback_output_by_inst.end() ? kInvalidFastStaNodeId : fallback->second; + }; std::size_t visited_steps = 0U; const auto max_steps = std::max(1U, context.nodes.size() + context.nets.size() + 1U) * 4U; while (!ready_nodes.empty() && visited_steps < max_steps) { @@ -219,16 +244,16 @@ auto propagateReadyQueue(FastStaClockContext& context, std::queue } auto& node = context.nodes.at(node_id); if (node.kind == FastStaNodeKind::kBufferInput) { - const auto output_iter = output_by_inst.find(node.inst_name); - if (output_iter != output_by_inst.end() && output_iter->second < context.nodes.size()) { - auto& output_node = context.nodes.at(output_iter->second); + const auto output_node_id = find_buffer_output(node); + if (output_node_id < context.nodes.size()) { + auto& output_node = context.nodes.at(output_node_id); if (!output_node.output_net_ids.empty() && output_node.output_net_ids.front() < context.nets.size()) { - propagateBufferOutput(context, output_iter->second, context.nets.at(output_node.output_net_ids.front())); + propagateBufferOutput(context, output_node_id, context.nets.at(output_node.output_net_ids.front())); } else { output_node.timing = node.timing; } if (output_node.timing.valid) { - ready_nodes.push(output_iter->second); + ready_nodes.push(output_node_id); } } continue; diff --git a/src/operation/iCTS/test/data_manager/adapter/fast_sta/FastSTATest.cc b/src/operation/iCTS/test/data_manager/adapter/fast_sta/FastSTATest.cc index c916775a6..5802b7e19 100644 --- a/src/operation/iCTS/test/data_manager/adapter/fast_sta/FastSTATest.cc +++ b/src/operation/iCTS/test/data_manager/adapter/fast_sta/FastSTATest.cc @@ -24,6 +24,9 @@ #include #include +#include +#include +#include #include #include #include @@ -225,6 +228,8 @@ auto MakeTinyContext() -> icts::FastStaClockContext MakeNode(icts::FastStaNodeKind::kSink, "sink/CLK", "sink", "CLK", "", icts::FastStaPoint{.x_dbu = 2000, .y_dbu = 0}, 0.10, 1U, {}), }; context.node_id_by_name = {{"clk_src", 0U}, {"buf/A", 1U}, {"buf/Y", 2U}, {"sink/CLK", 3U}}; + context.buffer_input_node_id_by_inst = {{"buf", 1U}}; + context.buffer_output_node_id_by_inst = {{"buf", 2U}}; context.node_id_by_location = {{{0, 0}, 0U}, {{1000, 0}, 1U}, {{2000, 0}, 3U}}; context.nets = { MakeNet("clk_net", 0U, {1U}, 3.0, @@ -257,6 +262,8 @@ auto MakeTwoLevelContext() -> icts::FastStaClockContext MakeNode(icts::FastStaNodeKind::kSink, "sink/CLK", "sink", "CLK", "", {}, 0.10, 2U, {}), }; context.node_id_by_name = {{"clk_src", 0U}, {"buf1/A", 1U}, {"buf1/Y", 2U}, {"buf2/A", 3U}, {"buf2/Y", 4U}, {"sink/CLK", 5U}}; + context.buffer_input_node_id_by_inst = {{"buf1", 1U}, {"buf2", 3U}}; + context.buffer_output_node_id_by_inst = {{"buf1", 2U}, {"buf2", 4U}}; context.nets = { MakeNet("clk_net", 0U, {1U}, 3.0, MakeParasitic({MakeRcNode("clk_net@0", 0.0, 0.0, 0.0, 0.0, 0U), MakeRcNode("clk_net@1", 0.0, 0.20, 0.20, 0.0, 1U)}, @@ -272,6 +279,127 @@ auto MakeTwoLevelContext() -> icts::FastStaClockContext return context; } +auto MakeScaleContext(std::size_t node_count) -> icts::FastStaClockContext +{ + icts::FastStaClockContext context; + context.clock_name = "scale_clk"; + context.clock_net_name = "scale_source_net"; + context.clock_period_ns = 10.0; + context.root_input_slew_ns = 0.1; + context.liberty_cell_by_master["BUF_X1"] = MakeCell("BUF_X1", 0.20, 1.5, 0.01); + context.liberty_cell_by_master["BUF_X2"] = MakeCell("BUF_X2", 0.40, 2.5, 0.02); + + const auto buffer_count = (node_count - 2U) / 2U; + const auto buffer_input_id = [](std::size_t buffer_id) -> icts::FastStaNodeId { return 1U + 2U * buffer_id; }; + const auto buffer_output_id = [](std::size_t buffer_id) -> icts::FastStaNodeId { return 2U + 2U * buffer_id; }; + context.nodes.reserve(node_count); + context.nets.reserve(buffer_count + 1U); + context.node_id_by_name.reserve(node_count); + context.buffer_input_node_id_by_inst.reserve(buffer_count); + context.buffer_output_node_id_by_inst.reserve(buffer_count); + context.net_id_by_name.reserve(buffer_count + 1U); + + context.source_node_id = 0U; + context.nodes.push_back(MakeNode(icts::FastStaNodeKind::kSource, "scale_source", "", "scale_source", "", {}, 0.0, icts::kInvalidFastStaNetId, {0U})); + context.node_id_by_name.emplace("scale_source", 0U); + for (std::size_t buffer_id = 0U; buffer_id < buffer_count; ++buffer_id) { + const auto inst_name = "scale_buf_" + std::to_string(buffer_id); + const auto input_name = inst_name + "/A"; + const auto output_name = inst_name + "/Y"; + const auto input_id = buffer_input_id(buffer_id); + const auto output_id = buffer_output_id(buffer_id); + const auto incoming_net_id = buffer_id == 0U ? 0U : (buffer_id - 1U) / 2U + 1U; + context.nodes.push_back(MakeNode(icts::FastStaNodeKind::kBufferInput, input_name, inst_name, "A", "BUF_X1", {}, 0.20, incoming_net_id, {})); + context.nodes.push_back( + MakeNode(icts::FastStaNodeKind::kBufferOutput, output_name, inst_name, "Y", "BUF_X1", {}, 0.0, icts::kInvalidFastStaNetId, {buffer_id + 1U})); + context.node_id_by_name.emplace(input_name, input_id); + context.node_id_by_name.emplace(output_name, output_id); + context.buffer_input_node_id_by_inst.emplace(inst_name, input_id); + context.buffer_output_node_id_by_inst.emplace(inst_name, output_id); + } + + const auto sink_node_id = context.nodes.size(); + context.nodes.push_back(MakeNode(icts::FastStaNodeKind::kSink, "scale_sink/CLK", "scale_sink", "CLK", "", {}, 0.10, buffer_count, {})); + context.node_id_by_name.emplace("scale_sink/CLK", sink_node_id); + + context.nets.push_back(MakeNet("scale_source_net", 0U, {buffer_input_id(0U)}, 3.0)); + context.net_id_by_name.emplace("scale_source_net", 0U); + for (std::size_t buffer_id = 0U; buffer_id < buffer_count; ++buffer_id) { + std::vector load_node_ids; + const auto left_child = 2U * buffer_id + 1U; + const auto right_child = left_child + 1U; + if (left_child < buffer_count) { + load_node_ids.push_back(buffer_input_id(left_child)); + } + if (right_child < buffer_count) { + load_node_ids.push_back(buffer_input_id(right_child)); + } + if (buffer_id + 1U == buffer_count) { + load_node_ids.push_back(sink_node_id); + } + const auto net_name = "scale_net_" + std::to_string(buffer_id); + context.nets.push_back(MakeNet(net_name, buffer_output_id(buffer_id), std::move(load_node_ids), 3.0)); + context.net_id_by_name.emplace(net_name, buffer_id + 1U); + } + return context; +} + +auto MakeScaleChanges(std::size_t node_count) -> std::vector +{ + const auto buffer_count = (node_count - 2U) / 2U; + const auto parent_buffer_id = buffer_count / 2U - 1U; + const auto left_child = 2U * parent_buffer_id + 1U; + const auto right_child = left_child + 1U; + return { + {.node_id = 1U + 2U * left_child, .cell_master = "BUF_X2"}, + {.node_id = 1U + 2U * right_child, .cell_master = "BUF_X2"}, + }; +} + +auto TimingStatesMatch(const icts::FastStaClockContext& lhs, const icts::FastStaClockContext& rhs) -> bool +{ + if (lhs.nodes.size() != rhs.nodes.size() || lhs.timing_valid != rhs.timing_valid || lhs.power_valid != rhs.power_valid || lhs.skew.valid != rhs.skew.valid + || lhs.skew.min_sink_node_id != rhs.skew.min_sink_node_id || lhs.skew.max_sink_node_id != rhs.skew.max_sink_node_id + || std::abs(lhs.skew.min_arrival_ns - rhs.skew.min_arrival_ns) > 1e-12 || std::abs(lhs.skew.max_arrival_ns - rhs.skew.max_arrival_ns) > 1e-12 + || std::abs(lhs.skew.skew_ns - rhs.skew.skew_ns) > 1e-12) { + return false; + } + for (std::size_t node_id = 0U; node_id < lhs.nodes.size(); ++node_id) { + const auto& lhs_node = lhs.nodes.at(node_id); + const auto& rhs_node = rhs.nodes.at(node_id); + if (lhs_node.cell_master != rhs_node.cell_master || lhs_node.timing.valid != rhs_node.timing.valid + || std::abs(lhs_node.timing.arrival_ns - rhs_node.timing.arrival_ns) > 1e-12 || std::abs(lhs_node.timing.slew_ns - rhs_node.timing.slew_ns) > 1e-12) { + return false; + } + } + return true; +} + +auto MeasureScaleRoutes(const icts::FastStaClockContext& baseline_context, const std::vector& changes, double& full_replay_us, + double& incremental_replay_us) -> bool +{ + auto full_context = baseline_context; + const auto full_start = std::chrono::steady_clock::now(); + const auto full_ok = icts::FastStaIncremental::changeBufferMasters(full_context, changes) && icts::FastStaTiming::update(full_context); + const auto full_finish = std::chrono::steady_clock::now(); + + auto incremental_context = baseline_context; + const auto incremental_start = std::chrono::steady_clock::now(); + const auto dirty_region = icts::FastStaIncremental::changeBufferMastersIncremental(incremental_context, changes); + const auto incremental_ok = dirty_region.has_value() && icts::FastStaTiming::updateRegion(incremental_context, dirty_region.value()); + const auto incremental_finish = std::chrono::steady_clock::now(); + + full_replay_us = std::chrono::duration(full_finish - full_start).count(); + incremental_replay_us = std::chrono::duration(incremental_finish - incremental_start).count(); + return full_ok && incremental_ok && TimingStatesMatch(full_context, incremental_context); +} + +auto Median(std::vector samples) -> double +{ + std::ranges::sort(samples); + return samples.at(samples.size() / 2U); +} + auto MakeOpenStaAlignmentPathContext() -> icts::FastStaClockContext { icts::FastStaClockContext context; @@ -618,5 +746,133 @@ TEST(FastSTATest, IncrementalMasterChangeMatchesFullRecompute) EXPECT_NEAR(incremental_context.power.area_um2, full_context.power.area_um2, 1e-12); } +TEST(FastSTATest, BatchIncrementalMasterChangeAndRestoreMatchFullRecompute) +{ + auto original_context = MakeTwoLevelContext(); + ASSERT_TRUE(icts::FastStaTiming::update(original_context)); + auto incremental_context = original_context; + auto full_context = original_context; + const std::vector changes{ + {.node_id = 1U, .cell_master = "BUF_X2"}, + {.node_id = 3U, .cell_master = "BUF_X2"}, + }; + + ASSERT_TRUE(icts::FastStaIncremental::validateBufferMasterChanges(incremental_context, changes)); + const auto changed_region = icts::FastStaIncremental::changeBufferMastersIncremental(incremental_context, changes); + if (!changed_region.has_value()) { + ADD_FAILURE() << "Expected a dirty region for the validated buffer-master batch."; + return; + } + ASSERT_TRUE(icts::FastStaTiming::updateRegion(incremental_context, *changed_region)); + ASSERT_TRUE(icts::FastStaIncremental::changeBufferMasters(full_context, changes)); + ASSERT_TRUE(icts::FastStaTiming::update(full_context)); + EXPECT_TRUE(TimingStatesMatch(incremental_context, full_context)); + + const std::vector restore{ + {.node_id = 1U, .cell_master = "BUF_X1"}, + {.node_id = 3U, .cell_master = "BUF_X1"}, + }; + ASSERT_TRUE(icts::FastStaIncremental::validateBufferMasterChanges(incremental_context, restore)); + const auto restored_region = icts::FastStaIncremental::changeBufferMastersIncremental(incremental_context, restore); + if (!restored_region.has_value()) { + ADD_FAILURE() << "Expected a dirty region when restoring the original buffer masters."; + return; + } + ASSERT_TRUE(icts::FastStaTiming::updateRegion(incremental_context, *restored_region)); + EXPECT_TRUE(TimingStatesMatch(incremental_context, original_context)); +} + +TEST(FastSTATest, MissingBufferPairIndexesUseValidatedFallback) +{ + auto incremental_context = MakeTwoLevelContext(); + incremental_context.buffer_input_node_id_by_inst.clear(); + incremental_context.buffer_output_node_id_by_inst.clear(); + ASSERT_TRUE(icts::FastStaTiming::update(incremental_context)); + + auto full_context = incremental_context; + const std::vector changes{ + {.node_id = 2U, .cell_master = "BUF_X2"}, + {.node_id = 3U, .cell_master = "BUF_X2"}, + }; + + const auto dirty_region = icts::FastStaIncremental::changeBufferMastersIncremental(incremental_context, changes); + if (!dirty_region.has_value()) { + ADD_FAILURE() << "Expected missing buffer-pair indexes to use the validated fallback."; + return; + } + ASSERT_TRUE(icts::FastStaTiming::updateRegion(incremental_context, *dirty_region)); + ASSERT_TRUE(icts::FastStaIncremental::changeBufferMasters(full_context, changes)); + ASSERT_TRUE(icts::FastStaTiming::update(full_context)); + EXPECT_TRUE(TimingStatesMatch(incremental_context, full_context)); + + ASSERT_TRUE(icts::FastStaPower::update(incremental_context)); + ASSERT_TRUE(icts::FastStaPower::update(full_context)); + EXPECT_NEAR(incremental_context.power.total_power_w, full_context.power.total_power_w, 1e-18); + EXPECT_NEAR(incremental_context.power.area_um2, full_context.power.area_um2, 1e-12); +} + +TEST(FastSTATest, BatchPrevalidationRejectsWholeChangeWithoutMutation) +{ + auto context = MakeTwoLevelContext(); + ASSERT_TRUE(icts::FastStaTiming::update(context)); + const auto original_context = context; + const std::vector changes{ + {.node_id = 1U, .cell_master = "BUF_X2"}, + {.node_id = context.nodes.size(), .cell_master = "BUF_X2"}, + }; + + EXPECT_FALSE(icts::FastStaIncremental::validateBufferMasterChanges(context, changes)); + EXPECT_FALSE(icts::FastStaIncremental::changeBufferMastersIncremental(context, changes).has_value()); + EXPECT_TRUE(TimingStatesMatch(context, original_context)); +} + +TEST(FastSTATest, BatchIncrementalTimingScale) +{ + const auto run_scale = [](std::size_t node_count, std::size_t measured_rounds) -> std::pair { + auto baseline_context = MakeScaleContext(node_count); + EXPECT_EQ(baseline_context.nodes.size(), node_count); + EXPECT_TRUE(icts::FastStaTiming::update(baseline_context)); + const auto changes = MakeScaleChanges(node_count); + + double warmup_full_us = 0.0; + double warmup_incremental_us = 0.0; + EXPECT_TRUE(MeasureScaleRoutes(baseline_context, changes, warmup_full_us, warmup_incremental_us)); + + std::vector full_samples_us; + std::vector incremental_samples_us; + full_samples_us.reserve(measured_rounds); + incremental_samples_us.reserve(measured_rounds); + for (std::size_t round = 0U; round < measured_rounds; ++round) { + double full_replay_us = 0.0; + double incremental_replay_us = 0.0; + EXPECT_TRUE(MeasureScaleRoutes(baseline_context, changes, full_replay_us, incremental_replay_us)) << "node_count=" << node_count << " round=" << round; + full_samples_us.push_back(full_replay_us); + incremental_samples_us.push_back(incremental_replay_us); + } + + const auto full_median_us = Median(full_samples_us); + const auto incremental_median_us = Median(incremental_samples_us); + std::cout << "FASTSTA_SCALE node_count=" << node_count << " full_us="; + for (const auto sample : full_samples_us) { + std::cout << sample << ','; + } + std::cout << " incremental_us="; + for (const auto sample : incremental_samples_us) { + std::cout << sample << ','; + } + std::cout << " full_median_us=" << full_median_us << " incremental_median_us=" << incremental_median_us + << " ratio=" << incremental_median_us / full_median_us << '\n'; + return {full_median_us, incremental_median_us}; + }; + + const auto [full_10k_us, incremental_10k_us] = run_scale(10'000U, 5U); + EXPECT_GT(full_10k_us, 0.0); + EXPECT_GT(incremental_10k_us, 0.0); + const auto [full_100k_us, incremental_100k_us] = run_scale(100'000U, 3U); + EXPECT_GT(full_100k_us, 0.0); + EXPECT_GT(incremental_100k_us, 0.0); + EXPECT_LE(incremental_100k_us, full_100k_us * 0.80); +} + } // namespace } // namespace icts_test From b1f961a197263014d669c55873f553840919e00c Mon Sep 17 00:00:00 2001 From: dawnli139 Date: Tue, 11 Aug 2026 11:03:15 +0800 Subject: [PATCH 3/3] fix(idb): make high-fanout disconnect linear High-fanout pin insertion already uses the lazy pointer index from ac04f3400, but bulk detach still called disconnectPinFromNet() once per pin. Each call searched/erased a vector and rescanned remaining instance pins, so removeNetSafe(), mergeNetInto(), platform disconnect, and CTS writeback could grow quadratically. Add an iDB-owned disconnectAllPinsFromNet() operation that updates pin-side regular/special net state in one pass, then clears the net-side pin/index and borrowed-instance collections once. Reuse the existing instance-name map before vector fallback, and make platform/iCTS bulk callers delegate to the invariant owner. Keep single-pin behavior and ac04f3400 unchanged. This changes bulk detach from O(P^2) to O(P) average without deleting borrowed pins or instances. iDB owns this operation because its public removeNetSafe() and mergeNetInto() APIs promise bulk connectivity changes while the pin-reference index, borrowed instance list, and visible regular/special net-name invariants are private to iDB. Platform DataManager::disconnectNet() and iCTS WrapperClockWriter::DetachIdbNetPins() now delegate instead of duplicating partial per-pin cleanup. The bulk path detaches only pins whose regular net is the target, refreshes each visible name so a surviving special net wins, clears IO/instance reference collections and the existing lazy pointer index once, and resets borrowed instance references without deleting their owners. Reconnect, repeated detach, removeNetSafe(), and mergeNetInto() retain their established semantics. The ac04f3400 threshold-32 pointer index is unchanged and no composite instance/pin-name index is introduced. Tests: the focused data-manager IO suite passed 11/11 GoogleTests, including bulk detach ownership, regular/special naming, remove/merge, reconnect/repeat/null detach, lazy-index rebuild, and scale coverage. The data-manager, FastSTA, lifecycle, characterization, and optimization regression set passed 10/10 CTests. Before this commit, python3 ./.trellis/ecc_dev_tools/check.py check --repo-root ecc-tools --path src/operation/iCTS completed all format, deep tidy/analyzer, header, CMake, and IWYU passes with zero in-scope findings. Performance: with one unmeasured 10,000-pin warm-up, five measured 10,000-pin rounds, and three measured 100,000-pin rounds per route, the pre-commit gate measured 10,000-pin legacy/bulk medians of 5012.230/275.687 us (18.181x) and 100,000-pin medians of 680027.005/2744.273 us (247.799x; bulk is 0.4035% of legacy). Bulk growth was 9.954x for 10x more pins, below the 15x gate. Setup/destruction was excluded and all route postconditions matched. An earlier separately allocated fixture run retained matching postconditions and a 93.597x 100,000-pin speedup, but its noisy 100,000-pin bulk samples produced 16.563x scaling and missed the 15x gate. That evidence is retained; contiguous fixture ownership left the measured API workload unchanged, and subsequent independent runs showed stable near-linear scaling. Tier 2 at commit time: the fresh baseline and post-FastSTA bp_fe_top runs passed with matching inputs and normalized effective configs, byte-identical cts.def/cts.v, and unchanged optimization decisions and QoR. The required fresh final post-iDB candidate remains scheduled after this commit in the task's final integration phase. --- src/database/data/design/IdbDesign.cpp | 40 +- src/database/data/design/IdbDesign.h | 2 + .../data/design/db_design/IdbInstance.cpp | 8 + .../data/design/db_design/IdbPins.cpp | 6 + src/database/data/design/db_design/IdbPins.h | 1 + .../data_manager/io/WrapperClockWriter.cc | 13 +- .../iCTS/test/data_manager/io/CMakeLists.txt | 1 + .../data_manager/io/IdbBulkDisconnectTest.cc | 422 ++++++++++++++++++ src/platform/data_manager/idm_design_net.cpp | 9 +- 9 files changed, 474 insertions(+), 28 deletions(-) create mode 100644 src/operation/iCTS/test/data_manager/io/IdbBulkDisconnectTest.cc diff --git a/src/database/data/design/IdbDesign.cpp b/src/database/data/design/IdbDesign.cpp index 50bc7a280..1bd8a8d8a 100644 --- a/src/database/data/design/IdbDesign.cpp +++ b/src/database/data/design/IdbDesign.cpp @@ -530,6 +530,36 @@ bool IdbDesign::disconnectPinFromNet(IdbPin* pin) return true; } +std::size_t IdbDesign::disconnectAllPinsFromNet(IdbNet* net) +{ + if (net == nullptr) { + return 0U; + } + + std::size_t disconnected_pin_count = 0U; + const auto disconnect_pin_refs = [&](IdbPins* pins) { + if (pins == nullptr) { + return; + } + for (auto* pin : pins->get_pin_list()) { + if (pin == nullptr || pin->get_net() != net) { + continue; + } + pin->remove_net(); + refreshPinNetName(pin); + ++disconnected_pin_count; + } + pins->clear_pin_refs(); + }; + + disconnect_pin_refs(net->get_io_pins()); + disconnect_pin_refs(net->get_instance_pin_list()); + if (net->get_instance_list() != nullptr) { + net->get_instance_list()->reset(false); + } + return disconnected_pin_count; +} + bool IdbDesign::connectPinToNet(IdbPin* pin, IdbNet* net) { if (pin == nullptr || net == nullptr) { @@ -595,14 +625,7 @@ bool IdbDesign::removeNetSafe(const std::string& net_name) return false; } - std::vector pin_list; - auto& io_pins = net->get_io_pins()->get_pin_list(); - auto& inst_pins = net->get_instance_pin_list()->get_pin_list(); - pin_list.insert(pin_list.end(), io_pins.begin(), io_pins.end()); - pin_list.insert(pin_list.end(), inst_pins.begin(), inst_pins.end()); - for (auto* pin : pin_list) { - disconnectPinFromNet(pin); - } + disconnectAllPinsFromNet(net); net->clear_wire_list(); return _net_list->remove_net_only(net_name); @@ -634,6 +657,7 @@ bool IdbDesign::mergeNetInto(const std::string& target_net_name, const std::stri auto& inst_pins = source_net->get_instance_pin_list()->get_pin_list(); pin_list.insert(pin_list.end(), io_pins.begin(), io_pins.end()); pin_list.insert(pin_list.end(), inst_pins.begin(), inst_pins.end()); + disconnectAllPinsFromNet(source_net); for (auto* pin : pin_list) { connectPinToNet(pin, target_net); } diff --git a/src/database/data/design/IdbDesign.h b/src/database/data/design/IdbDesign.h index 8d6b0a107..82154845e 100644 --- a/src/database/data/design/IdbDesign.h +++ b/src/database/data/design/IdbDesign.h @@ -32,6 +32,7 @@ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #include +#include #include #include #include @@ -144,6 +145,7 @@ class IdbDesign bool connectIoPinToNet(const std::string& io_pin_name, const std::string& net_name); bool connectInstancePinToNet(const std::string& inst_name, const std::string& pin_name, const std::string& net_name); bool disconnectPinFromNet(IdbPin* pin); + std::size_t disconnectAllPinsFromNet(IdbNet* net); bool removeNetSafe(const std::string& net_name); bool renameNet(IdbNet* net, const std::string& new_name); bool mergeNetInto(const std::string& target_net_name, const std::string& source_net_name, bool move_wires = true); diff --git a/src/database/data/design/db_design/IdbInstance.cpp b/src/database/data/design/db_design/IdbInstance.cpp index 83a37e4e3..69ac62bd4 100644 --- a/src/database/data/design/db_design/IdbInstance.cpp +++ b/src/database/data/design/db_design/IdbInstance.cpp @@ -456,6 +456,14 @@ bool IdbInstanceList::contains(IdbInstance* instance) return false; } + const auto& name = instance->get_name(); + if (!name.empty()) { + const auto iter = _instance_map.find(name); + if (iter != _instance_map.end() && iter->second == instance) { + return true; + } + } + auto iter = std::find(_instance_list.begin(), _instance_list.end(), instance); return iter != _instance_list.end(); } diff --git a/src/database/data/design/db_design/IdbPins.cpp b/src/database/data/design/db_design/IdbPins.cpp index d21a73e64..6f27451d2 100644 --- a/src/database/data/design/db_design/IdbPins.cpp +++ b/src/database/data/design/db_design/IdbPins.cpp @@ -792,6 +792,12 @@ bool IdbPins::erase_pin_ref(IdbPin* pin_remove) return true; } +void IdbPins::clear_pin_refs() +{ + _pin_list.clear(); + _pin_ref_index.reset(); +} + bool IdbPins::delete_pin(IdbPin* pin_remove) { if (!erase_pin_ref(pin_remove)) { diff --git a/src/database/data/design/db_design/IdbPins.h b/src/database/data/design/db_design/IdbPins.h index 66e93cbd8..475f20a59 100644 --- a/src/database/data/design/db_design/IdbPins.h +++ b/src/database/data/design/db_design/IdbPins.h @@ -163,6 +163,7 @@ class IdbPins // Operate void remove_pin(IdbPin* pin_remove); bool erase_pin_ref(IdbPin* pin_remove); + void clear_pin_refs(); bool delete_pin(IdbPin* pin_remove); int32_t getIOPortWidth(); void checkPins(); diff --git a/src/operation/iCTS/source/data_manager/io/WrapperClockWriter.cc b/src/operation/iCTS/source/data_manager/io/WrapperClockWriter.cc index 7f25d07ca..954421e2c 100644 --- a/src/operation/iCTS/source/data_manager/io/WrapperClockWriter.cc +++ b/src/operation/iCTS/source/data_manager/io/WrapperClockWriter.cc @@ -83,18 +83,7 @@ auto DetachIdbNetPins(idb::IdbDesign* idb_design, idb::IdbNet* idb_net) -> void if (idb_design == nullptr || idb_net == nullptr) { return; } - std::vector pins; - if (idb_net->get_io_pins() != nullptr) { - const auto& io_pins = idb_net->get_io_pins()->get_pin_list(); - pins.insert(pins.end(), io_pins.begin(), io_pins.end()); - } - if (idb_net->get_instance_pin_list() != nullptr) { - const auto& inst_pins = idb_net->get_instance_pin_list()->get_pin_list(); - pins.insert(pins.end(), inst_pins.begin(), inst_pins.end()); - } - for (auto* pin : pins) { - idb_design->disconnectPinFromNet(pin); - } + idb_design->disconnectAllPinsFromNet(idb_net); } auto FindIdbInstPinByCtsPinName(idb::IdbInstance* idb_inst, const std::string& pin_name) -> idb::IdbPin* diff --git a/src/operation/iCTS/test/data_manager/io/CMakeLists.txt b/src/operation/iCTS/test/data_manager/io/CMakeLists.txt index 5c45a5f65..2a5a9b7be 100644 --- a/src/operation/iCTS/test/data_manager/io/CMakeLists.txt +++ b/src/operation/iCTS/test/data_manager/io/CMakeLists.txt @@ -1,5 +1,6 @@ icts_add_test_executable( icts_test_data_manager_io SOURCES + ${ICTS_TEST}/data_manager/io/IdbBulkDisconnectTest.cc ${ICTS_TEST}/data_manager/io/WrapperRCTest.cc ) diff --git a/src/operation/iCTS/test/data_manager/io/IdbBulkDisconnectTest.cc b/src/operation/iCTS/test/data_manager/io/IdbBulkDisconnectTest.cc new file mode 100644 index 000000000..dbe4fe24c --- /dev/null +++ b/src/operation/iCTS/test/data_manager/io/IdbBulkDisconnectTest.cc @@ -0,0 +1,422 @@ +// *************************************************************************************** +// Copyright (c) 2023-2025 Peng Cheng Laboratory +// Copyright (c) 2023-2025 Institute of Computing Technology, Chinese Academy of Sciences +// Copyright (c) 2023-2025 Beijing Institute of Open Source Chip +// +// iEDA is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// +// See the Mulan PSL v2 for more details. +// *************************************************************************************** +/** + * @file IdbBulkDisconnectTest.cc + * @author Dawn Li (dawnli619215645@gmail.com) + * @date 2026-08-11 + * @brief Regression and scale tests for iDB-owned bulk regular-net disconnection. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "IdbDesign.h" +#include "IdbInstance.h" +#include "IdbNet.h" +#include "IdbPins.h" + +namespace icts_test { +namespace { + +using SteadyClock = std::chrono::steady_clock; + +struct DisconnectObservation +{ + std::size_t disconnected_pin_count = 0U; + std::size_t net_pin_count = 0U; + std::size_t net_instance_count = 0U; + bool all_pins_disconnected = false; + bool all_net_names_empty = false; + + auto operator==(const DisconnectObservation&) const -> bool = default; +}; + +struct DisconnectSample +{ + double duration_us = 0.0; + DisconnectObservation observation; +}; + +struct ScaleSamples +{ + std::size_t pin_count = 0U; + std::vector legacy_us; + std::vector bulk_us; +}; + +class HighFanoutIoFixture +{ + public: + explicit HighFanoutIoFixture(std::size_t pin_count) : _owned_pins(pin_count) + { + _net = _design.createOrFindNet("high_fanout", idb::IdbConnectType::kClock); + _pin_refs.reserve(pin_count); + for (std::size_t index = 0U; index < pin_count; ++index) { + auto* pin_ref = &_owned_pins[index]; + pin_ref->set_as_io(); + pin_ref->set_pin_name("P" + std::to_string(index)); + _pin_refs.push_back(pin_ref); + if (!_design.connectPinToNet(pin_ref, _net)) { + _setup_ok = false; + break; + } + } + } + + auto setupOk() const -> bool { return _setup_ok && _net != nullptr && static_cast(_net->get_pin_number()) == _pin_refs.size(); } + auto design() -> idb::IdbDesign& { return _design; } + auto net() -> idb::IdbNet* { return _net; } + auto pinRefs() -> const std::vector& { return _pin_refs; } + + private: + std::vector _owned_pins; + idb::IdbDesign _design; + idb::IdbNet* _net = nullptr; + std::vector _pin_refs; + bool _setup_ok = true; +}; + +auto ObserveDisconnected(HighFanoutIoFixture& fixture, std::size_t disconnected_pin_count) -> DisconnectObservation +{ + auto* net = fixture.net(); + const auto& pins = fixture.pinRefs(); + + DisconnectObservation observation; + observation.disconnected_pin_count = disconnected_pin_count; + observation.net_pin_count = net == nullptr ? 0U : static_cast(net->get_pin_number()); + observation.net_instance_count = net == nullptr || net->get_instance_list() == nullptr ? 0U : net->get_instance_list()->get_instance_list().size(); + observation.all_pins_disconnected = std::ranges::all_of(pins, [](auto* pin) -> bool { return pin != nullptr && pin->get_net() == nullptr; }); + observation.all_net_names_empty = std::ranges::all_of(pins, [](auto* pin) -> bool { return pin != nullptr && pin->get_net_name().empty(); }); + return observation; +} + +auto RunLegacyDisconnect(std::size_t pin_count) -> DisconnectSample +{ + HighFanoutIoFixture fixture(pin_count); + EXPECT_TRUE(fixture.setupOk()); + + std::size_t disconnected_pin_count = 0U; + const auto start = SteadyClock::now(); + for (auto* pin : fixture.pinRefs()) { + disconnected_pin_count += fixture.design().disconnectPinFromNet(pin) ? 1U : 0U; + } + const auto stop = SteadyClock::now(); + + return DisconnectSample{.duration_us = std::chrono::duration(stop - start).count(), + .observation = ObserveDisconnected(fixture, disconnected_pin_count)}; +} + +auto RunBulkDisconnect(std::size_t pin_count) -> DisconnectSample +{ + HighFanoutIoFixture fixture(pin_count); + EXPECT_TRUE(fixture.setupOk()); + + const auto start = SteadyClock::now(); + const std::size_t disconnected_pin_count = fixture.design().disconnectAllPinsFromNet(fixture.net()); + const auto stop = SteadyClock::now(); + + return DisconnectSample{.duration_us = std::chrono::duration(stop - start).count(), + .observation = ObserveDisconnected(fixture, disconnected_pin_count)}; +} + +void ExpectCompleteDisconnect(const DisconnectObservation& observation, std::size_t pin_count) +{ + EXPECT_EQ(observation.disconnected_pin_count, pin_count); + EXPECT_EQ(observation.net_pin_count, 0U); + EXPECT_EQ(observation.net_instance_count, 0U); + EXPECT_TRUE(observation.all_pins_disconnected); + EXPECT_TRUE(observation.all_net_names_empty); +} + +void RecordMeasuredPair(ScaleSamples& samples, bool bulk_first) +{ + DisconnectSample legacy; + DisconnectSample bulk; + if (bulk_first) { + bulk = RunBulkDisconnect(samples.pin_count); + legacy = RunLegacyDisconnect(samples.pin_count); + } else { + legacy = RunLegacyDisconnect(samples.pin_count); + bulk = RunBulkDisconnect(samples.pin_count); + } + + ExpectCompleteDisconnect(legacy.observation, samples.pin_count); + ExpectCompleteDisconnect(bulk.observation, samples.pin_count); + EXPECT_EQ(legacy.observation, bulk.observation); + samples.legacy_us.push_back(legacy.duration_us); + samples.bulk_us.push_back(bulk.duration_us); +} + +auto Median(std::vector samples) -> double +{ + std::ranges::sort(samples); + const std::size_t middle = samples.size() / 2U; + if (samples.size() % 2U != 0U) { + return samples[middle]; + } + return (samples[middle - 1U] + samples[middle]) / 2.0; +} + +void PrintSamples(const ScaleSamples& samples) +{ + const auto print_route = [&](const char* route, const std::vector& durations) -> void { + std::cout << "IDB_BULK_PERF pin_count=" << samples.pin_count << " route=" << route << " samples_us=["; + for (std::size_t index = 0U; index < durations.size(); ++index) { + if (index != 0U) { + std::cout << ','; + } + std::cout << std::fixed << std::setprecision(3) << durations[index]; + } + std::cout << "] median_us=" << Median(durations) << '\n'; + }; + + print_route("legacy", samples.legacy_us); + print_route("bulk", samples.bulk_us); +} + +void WriteSamplesIfRequested(const std::vector& scale_samples) +{ + const char* output_value = std::getenv("ICTS_IDB_PERF_OUTPUT"); + if (output_value == nullptr || *output_value == '\0') { + return; + } + + const std::filesystem::path output_path(output_value); + std::error_code error; + if (!output_path.parent_path().empty()) { + std::filesystem::create_directories(output_path.parent_path(), error); + } + ASSERT_FALSE(error) << "Unable to create iDB performance evidence directory: " << error.message(); + + std::ofstream output(output_path, std::ios::trunc); + ASSERT_TRUE(output.is_open()) << "Unable to write iDB performance evidence: " << output_path; + output << "pin_count,route,round,duration_us\n"; + output << std::fixed << std::setprecision(3); + for (const auto& samples : scale_samples) { + for (std::size_t index = 0U; index < samples.legacy_us.size(); ++index) { + output << samples.pin_count << ",legacy," << (index + 1U) << ',' << samples.legacy_us[index] << '\n'; + } + for (std::size_t index = 0U; index < samples.bulk_us.size(); ++index) { + output << samples.pin_count << ",bulk," << (index + 1U) << ',' << samples.bulk_us[index] << '\n'; + } + } +} + +TEST(IdbPinsTest, ClearPinRefsPreservesBorrowedPinsAndResetsLazyIndex) +{ + idb::IdbPins refs; + std::vector> owned_pins; + owned_pins.reserve(40U); + for (std::size_t index = 0U; index < 40U; ++index) { + auto pin = std::make_unique(); + pin->set_pin_name("P" + std::to_string(index)); + auto* pin_ref = pin.get(); + owned_pins.emplace_back(std::move(pin)); + EXPECT_EQ(refs.add_pin_ref_unique(pin_ref), pin_ref); + } + ASSERT_EQ(refs.get_pin_num(), 40U); + + refs.clear_pin_refs(); + + EXPECT_EQ(refs.get_pin_num(), 0U); + EXPECT_EQ(owned_pins.front()->get_pin_name(), "P0"); + EXPECT_EQ(owned_pins.back()->get_pin_name(), "P39"); + for (const auto& pin : owned_pins) { + EXPECT_EQ(refs.add_pin_ref_unique(pin.get()), pin.get()); + } + EXPECT_EQ(refs.get_pin_num(), 40U); + EXPECT_EQ(refs.add_pin_ref_unique(owned_pins.back().get()), owned_pins.back().get()); + EXPECT_EQ(refs.get_pin_num(), 40U); +} + +TEST(IdbInstanceListTest, ContainsUsesNamedMapWithoutLosingPointerFallback) +{ + idb::IdbDesign owning_design; + auto* first = owning_design.get_instance_list()->add_instance("u_first"); + ASSERT_NE(first, nullptr); + idb::IdbNet borrowed_instance_owner; + auto* instances = borrowed_instance_owner.get_instance_list(); + ASSERT_NE(instances, nullptr); + ASSERT_TRUE(instances->add_instance_ref(first)); + EXPECT_TRUE(instances->contains(first)); + EXPECT_TRUE(instances->add_instance_ref(first)); + EXPECT_EQ(instances->get_instance_list().size(), 1U); + + idb::IdbInstance duplicate_name; + duplicate_name.set_name("u_first"); + EXPECT_FALSE(instances->contains(&duplicate_name)); + EXPECT_FALSE(instances->add_instance_ref(&duplicate_name)); + EXPECT_EQ(instances->get_instance_list().size(), 1U); + + first->set_name("u_renamed"); + EXPECT_TRUE(instances->contains(first)); + EXPECT_TRUE(instances->add_instance_ref(first)); + EXPECT_EQ(instances->get_instance_list().size(), 1U); +} + +TEST(IdbDesignBulkDisconnectTest, PreservesSpecialNameBorrowedObjectsAndRepeatUse) +{ + idb::IdbDesign design; + EXPECT_EQ(design.disconnectAllPinsFromNet(nullptr), 0U); + + auto* instance = design.get_instance_list()->add_instance("u_sink"); + ASSERT_NE(instance, nullptr); + auto* first_inst_pin = instance->get_pin_list()->add_pin_list("A"); + auto* second_inst_pin = instance->get_pin_list()->add_pin_list("B"); + ASSERT_NE(first_inst_pin, nullptr); + ASSERT_NE(second_inst_pin, nullptr); + first_inst_pin->set_instance(instance); + second_inst_pin->set_instance(instance); + + auto* io_pin = design.createOrFindIoPin("clk_in"); + auto* regular_net = design.createOrFindNet("clk", idb::IdbConnectType::kClock); + auto* special_net = design.createOrFindSpecialNet("VDD", idb::IdbConnectType::kPower); + ASSERT_NE(io_pin, nullptr); + ASSERT_NE(regular_net, nullptr); + ASSERT_NE(special_net, nullptr); + ASSERT_TRUE(design.connectPinToNet(io_pin, regular_net)); + ASSERT_TRUE(design.connectPinToNet(first_inst_pin, regular_net)); + ASSERT_TRUE(design.connectPinToNet(second_inst_pin, regular_net)); + ASSERT_TRUE(design.connectPinToSpecialNet(first_inst_pin, special_net)); + + EXPECT_EQ(design.disconnectAllPinsFromNet(regular_net), 3U); + EXPECT_TRUE(regular_net->get_io_pins()->get_pin_list().empty()); + EXPECT_TRUE(regular_net->get_instance_pin_list()->get_pin_list().empty()); + EXPECT_TRUE(regular_net->get_instance_list()->get_instance_list().empty()); + EXPECT_EQ(io_pin->get_net(), nullptr); + EXPECT_TRUE(io_pin->get_net_name().empty()); + EXPECT_EQ(first_inst_pin->get_net(), nullptr); + EXPECT_EQ(first_inst_pin->get_special_net(), special_net); + EXPECT_EQ(first_inst_pin->get_net_name(), "VDD"); + EXPECT_EQ(second_inst_pin->get_net(), nullptr); + EXPECT_TRUE(second_inst_pin->get_net_name().empty()); + EXPECT_EQ(design.get_instance_list()->find_instance("u_sink"), instance); + EXPECT_EQ(instance->get_pin("A"), first_inst_pin); + + ASSERT_TRUE(design.connectPinToNet(io_pin, regular_net)); + ASSERT_TRUE(design.connectPinToNet(first_inst_pin, regular_net)); + ASSERT_TRUE(design.connectPinToNet(second_inst_pin, regular_net)); + EXPECT_EQ(design.disconnectAllPinsFromNet(regular_net), 3U); + EXPECT_EQ(design.disconnectAllPinsFromNet(regular_net), 0U); + EXPECT_EQ(first_inst_pin->get_net_name(), "VDD"); + EXPECT_EQ(design.get_instance_list()->find_instance("u_sink"), instance); +} + +TEST(IdbDesignBulkDisconnectTest, RemoveNetSafePreservesPinAndSpecialNetName) +{ + idb::IdbDesign design; + auto* pin = design.createOrFindIoPin("clk_in"); + auto* regular_net = design.createOrFindNet("clk", idb::IdbConnectType::kClock); + auto* special_net = design.createOrFindSpecialNet("VDD", idb::IdbConnectType::kPower); + ASSERT_NE(pin, nullptr); + ASSERT_NE(regular_net, nullptr); + ASSERT_NE(special_net, nullptr); + ASSERT_TRUE(design.connectPinToNet(pin, regular_net)); + ASSERT_TRUE(design.connectPinToSpecialNet(pin, special_net)); + + EXPECT_TRUE(design.removeNetSafe("clk")); + EXPECT_EQ(design.get_net_list()->find_net("clk"), nullptr); + EXPECT_EQ(design.get_io_pin_list()->find_pin("clk_in"), pin); + EXPECT_EQ(pin->get_net(), nullptr); + EXPECT_EQ(pin->get_special_net(), special_net); + EXPECT_EQ(pin->get_net_name(), "VDD"); +} + +TEST(IdbDesignBulkDisconnectTest, MergeNetIntoMovesPinsAfterOneSourceDetach) +{ + idb::IdbDesign design; + auto* instance = design.get_instance_list()->add_instance("u_sink"); + ASSERT_NE(instance, nullptr); + auto* inst_pin = instance->get_pin_list()->add_pin_list("A"); + ASSERT_NE(inst_pin, nullptr); + inst_pin->set_instance(instance); + + auto* io_pin = design.createOrFindIoPin("clk_in"); + auto* target_net = design.createOrFindNet("target", idb::IdbConnectType::kClock); + auto* source_net = design.createOrFindNet("source", idb::IdbConnectType::kClock); + ASSERT_NE(io_pin, nullptr); + ASSERT_NE(target_net, nullptr); + ASSERT_NE(source_net, nullptr); + ASSERT_TRUE(design.connectPinToNet(io_pin, source_net)); + ASSERT_TRUE(design.connectPinToNet(inst_pin, source_net)); + + EXPECT_TRUE(design.mergeNetInto("target", "source", false)); + EXPECT_EQ(design.get_net_list()->find_net("source"), nullptr); + EXPECT_EQ(io_pin->get_net(), target_net); + EXPECT_EQ(io_pin->get_net_name(), "target"); + EXPECT_EQ(inst_pin->get_net(), target_net); + EXPECT_EQ(inst_pin->get_net_name(), "target"); + EXPECT_TRUE(target_net->has_io_pin(io_pin)); + EXPECT_TRUE(target_net->has_instance_pin(inst_pin)); + EXPECT_TRUE(target_net->has_instance(instance)); +} + +TEST(IdbDesignBulkDisconnectPerformanceTest, LegacyAndBulkPathsMatchAtTenAndHundredThousandPins) +{ + constexpr std::size_t ten_thousand_pin_count = 10000U; + constexpr std::size_t hundred_thousand_pin_count = 100000U; + constexpr std::size_t ten_thousand_round_count = 5U; + constexpr std::size_t hundred_thousand_round_count = 3U; + + const auto warmup_legacy = RunLegacyDisconnect(ten_thousand_pin_count); + const auto warmup_bulk = RunBulkDisconnect(ten_thousand_pin_count); + ExpectCompleteDisconnect(warmup_legacy.observation, ten_thousand_pin_count); + ExpectCompleteDisconnect(warmup_bulk.observation, ten_thousand_pin_count); + EXPECT_EQ(warmup_legacy.observation, warmup_bulk.observation); + + ScaleSamples ten_thousand{.pin_count = ten_thousand_pin_count, .legacy_us = {}, .bulk_us = {}}; + for (std::size_t round = 0U; round < ten_thousand_round_count; ++round) { + RecordMeasuredPair(ten_thousand, round % 2U != 0U); + } + + ScaleSamples hundred_thousand{.pin_count = hundred_thousand_pin_count, .legacy_us = {}, .bulk_us = {}}; + for (std::size_t round = 0U; round < hundred_thousand_round_count; ++round) { + RecordMeasuredPair(hundred_thousand, round % 2U == 0U); + } + + PrintSamples(ten_thousand); + PrintSamples(hundred_thousand); + WriteSamplesIfRequested({ten_thousand, hundred_thousand}); + + const double ten_thousand_legacy_median = Median(ten_thousand.legacy_us); + const double ten_thousand_bulk_median = Median(ten_thousand.bulk_us); + const double hundred_thousand_legacy_median = Median(hundred_thousand.legacy_us); + const double hundred_thousand_bulk_median = Median(hundred_thousand.bulk_us); + std::cout << "IDB_BULK_PERF_SUMMARY pin_count=10000 legacy_median_us=" << ten_thousand_legacy_median << " bulk_median_us=" << ten_thousand_bulk_median + << " speedup=" << (ten_thousand_legacy_median / ten_thousand_bulk_median) << '\n'; + std::cout << "IDB_BULK_PERF_SUMMARY pin_count=100000 legacy_median_us=" << hundred_thousand_legacy_median + << " bulk_median_us=" << hundred_thousand_bulk_median << " speedup=" << (hundred_thousand_legacy_median / hundred_thousand_bulk_median) + << " bulk_scaling=" << (hundred_thousand_bulk_median / ten_thousand_bulk_median) << '\n'; + + EXPECT_LE(hundred_thousand_bulk_median, hundred_thousand_legacy_median * 0.20); + EXPECT_LE(hundred_thousand_bulk_median, ten_thousand_bulk_median * 15.0); +} + +} // namespace +} // namespace icts_test diff --git a/src/platform/data_manager/idm_design_net.cpp b/src/platform/data_manager/idm_design_net.cpp index 9e9cd7ab7..4680897a7 100644 --- a/src/platform/data_manager/idm_design_net.cpp +++ b/src/platform/data_manager/idm_design_net.cpp @@ -231,14 +231,7 @@ bool DataManager::disconnectNet(IdbNet* net) return false; } - std::vector pin_list; - auto& io_pins = net->get_io_pins()->get_pin_list(); - auto& inst_pins = net->get_instance_pin_list()->get_pin_list(); - pin_list.insert(pin_list.end(), io_pins.begin(), io_pins.end()); - pin_list.insert(pin_list.end(), inst_pins.begin(), inst_pins.end()); - for (auto* pin : pin_list) { - _design->disconnectPinFromNet(pin); - } + _design->disconnectAllPinsFromNet(net); return true; }