diff --git a/Sofa/framework/Simulation/Core/test/CMakeLists.txt b/Sofa/framework/Simulation/Core/test/CMakeLists.txt index 3f58b89edc9..24833b05591 100644 --- a/Sofa/framework/Simulation/Core/test/CMakeLists.txt +++ b/Sofa/framework/Simulation/Core/test/CMakeLists.txt @@ -7,6 +7,7 @@ set(SOURCE_FILES MappingGraph_test.cpp ParallelForEach_test.cpp RequiredPlugin_test.cpp + TaskDestructorLeak_test.cpp SceneCheckRegistry_test.cpp Simulation_test.cpp ParallelSparseMatrixProduct_test.cpp diff --git a/Sofa/framework/Simulation/Core/test/TaskDestructorLeak_test.cpp b/Sofa/framework/Simulation/Core/test/TaskDestructorLeak_test.cpp new file mode 100644 index 00000000000..0de72c61768 --- /dev/null +++ b/Sofa/framework/Simulation/Core/test/TaskDestructorLeak_test.cpp @@ -0,0 +1,128 @@ +/****************************************************************************** +* SOFA, Simulation Open-Framework Architecture * +* (c) 2006 INRIA, USTL, UJF, CNRS, MGH * +* * +* This program is free software; you can redistribute it and/or modify it * +* under the terms of the GNU Lesser General Public License as published by * +* the Free Software Foundation; either version 2.1 of the License, or (at * +* your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, but WITHOUT * +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * +* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * +* for more details. * +* * +* You should have received a copy of the GNU Lesser General Public License * +* along with this program. If not, see . * +******************************************************************************* +* Authors: The SOFA Team and external contributors (see Authors.txt) * +* * +* Contact information: contact@sofa-framework.org * +******************************************************************************/ +#include +#include +#include +#include +#include + +#include +#include + +namespace sofa +{ + +// Reproduction for a leak in WorkerThread::runTask. After running a task that +// returns MemoryAlloc::Dynamic, the framework calls Task::operator delete to +// release the memory but does NOT call the task's destructor. From +// WorkerThread::runTask: +// +// if (task->run() & Task::MemoryAlloc::Dynamic) { +// // pooled memory: call destructor and free +// //task->~Task(); <-- commented out +// task->operator delete(task, sizeof(*task)); +// } +// +// As a result, any task with non-trivially-destructible members (std::function, +// std::shared_ptr, std::vector, ...) leaks those members' resources every time +// it runs. The std::function-based addTask(Status&, lambda) overload always +// triggers this because CallableTask wraps the lambda in a std::function. +// +// We demonstrate the leak with a custom CpuTask holding a std::shared_ptr. +// If the destructor ran, every task copy of the shared_ptr would release a +// reference and the original's use_count would return to 1 after the burst. +// On the buggy code, use_count stays at 1 + (number of dispatched tasks). + +namespace +{ + +class SharedPtrHoldingTask : public simulation::CpuTask +{ +public: + SharedPtrHoldingTask(simulation::CpuTask::Status* status, + std::shared_ptr resource, + std::atomic* counter) + : simulation::CpuTask(status) + , m_resource(std::move(resource)) + , m_counter(counter) + {} + + sofa::simulation::Task::MemoryAlloc run() final + { + // Touch the resource so the compiler can't elide the member. + if (m_resource) + { + m_counter->fetch_add(*m_resource, std::memory_order_relaxed); + } + return sofa::simulation::Task::MemoryAlloc::Dynamic; + } + +private: + std::shared_ptr m_resource; + std::atomic* m_counter; +}; + +} // namespace + +// Dispatch many tasks, each holding a copy of the same shared_ptr. After +// workUntilDone, all task instances must have been destroyed; the only +// remaining holder is the test's local `resource`, so use_count must be 1. +// +// On the buggy code, the destructors are skipped and use_count equals +// 1 + kNumTasks. +TEST(TaskDestructorLeak, SharedPtrTasksReleaseTheirReference) +{ + constexpr int kNumTasks = 64; + + auto* scheduler = simulation::MainTaskSchedulerFactory::createInRegistry( + simulation::DefaultTaskScheduler::name()); + ASSERT_NE(scheduler, nullptr); + + scheduler->init(0); + if (scheduler->getThreadCount() < 2) + { + GTEST_SKIP() << "scheduler has fewer than 2 threads; skipping"; + } + + auto resource = std::make_shared(1); + std::atomic counter { 0 }; + + { + simulation::CpuTaskStatus status; + for (int i = 0; i < kNumTasks; ++i) + { + // Each task holds its own shared_ptr copy. + scheduler->addTask(new SharedPtrHoldingTask(&status, resource, &counter)); + } + scheduler->workUntilDone(&status); + } + + EXPECT_EQ(counter.load(std::memory_order_relaxed), kNumTasks); + // After all tasks have been disposed of, only `resource` should hold the + // shared_ptr. If the framework skipped the destructor, every task's copy + // is leaked and use_count == 1 + kNumTasks. + EXPECT_EQ(resource.use_count(), 1); + + scheduler->stop(); +} + +} // namespace sofa