changeset 115:2691260b08c5 default tip

reduced memory usage
author Alain Mazy <am@orthanc.team>
date Mon, 21 Sep 2026 15:02:15 +0200
parents fc60db3d2681
children
files CMakeLists.txt Framework/DownloadArea.cpp NEWS UnitTests/UnitTestsMain.cpp
diffstat 4 files changed, 84 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Fri Aug 14 08:49:57 2026 +0200
+++ b/CMakeLists.txt	Mon Sep 21 15:02:15 2026 +0200
@@ -143,6 +143,13 @@
   Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
   )
 
+if (STATIC_BUILD)
+  # Needed by Framework/DownloadArea.cpp for boost::iostreams::mapped_file_source:
+  # the framework's own static-build source list only compiles file_descriptor.cpp,
+  # as this plugin is the first to need memory-mapped files.
+  list(APPEND FRAMEWORK_SOURCES ${BOOST_SOURCES_DIR}/libs/iostreams/src/mapped_file.cpp)
+endif()
+
 
 
 add_library(OrthancTransfers SHARED
--- a/Framework/DownloadArea.cpp	Fri Aug 14 08:49:57 2026 +0200
+++ b/Framework/DownloadArea.cpp	Mon Sep 21 15:02:15 2026 +0200
@@ -29,6 +29,7 @@
 
 #include <boost/filesystem.hpp>
 #include <boost/filesystem/fstream.hpp>
+#include <boost/iostreams/device/mapped_file.hpp>
 
 namespace OrthancPlugins
 {
@@ -140,32 +141,59 @@
     }
   }
 
-  
   void DownloadArea::Instance::Commit(bool simulate) const
   {
-    std::string content;
-    Orthanc::SystemToolbox::ReadFile(content, file_.GetPath());
-
+    // Streams the file in fixed-size chunks instead of reading it fully
+    // into a heap-allocated std::string first (this matters for large
+    // instances: that buffer is anonymous memory, which the kernel can
+    // only reclaim under pressure by swapping it out).
     std::string md5;
-    Orthanc::Toolbox::ComputeMD5(md5, content);
+    Orthanc::SystemToolbox::ComputeFileMD5(md5, file_.GetPath());
 
-    if (md5 == info_.GetMD5())
-    {
-      if (!simulate)
-      {
-        Json::Value result;
-        if (!RestApiPost(result, "/instances", 
-                         content.empty() ? NULL : content.c_str(), content.size(),
-                         false))
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_CorruptedFile, "Cannot import a transfered DICOM instance into Orthanc: " + info_.GetId());
-        }
-      }
-    }
-    else
+    if (md5 != info_.GetMD5())
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_CorruptedFile, "Bad MD5 sum in a transfered DICOM instance: " +  info_.GetId());
     }
+
+    if (!simulate)
+    {
+      Json::Value result;
+      bool success;
+
+      if (info_.GetSize() == 0)
+      {
+        success = RestApiPost(result, "/instances", NULL, 0, false);
+      }
+      else
+      {
+        // Memory-map the file instead of copying it into a std::string:
+        // the mapped pages are file-backed, so under memory pressure the
+        // kernel can drop them instantly (no swap write-back needed), and
+        // there is no read()-style kernel-to-userspace copy either.
+        boost::iostreams::mapped_file_source mapped;
+
+        try
+        {
+          mapped.open(file_.GetPath());
+        }
+        catch (const std::exception&)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile, "Cannot memory-map a transfered DICOM instance: " + info_.GetId());
+        }
+
+        if (!mapped.is_open())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile, "Cannot memory-map a transfered DICOM instance: " + info_.GetId());
+        }
+
+        success = RestApiPost(result, "/instances", mapped.data(), mapped.size(), false);
+      }
+
+      if (!success)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_CorruptedFile, "Cannot import a transfered DICOM instance into Orthanc: " + info_.GetId());
+      }
+    }
   }
 
 
--- a/NEWS	Fri Aug 14 08:49:57 2026 +0200
+++ b/NEWS	Mon Sep 21 15:02:15 2026 +0200
@@ -1,3 +1,8 @@
+Pending changes in the mainline
+===============================
+
+* reduced the memory consumption (https://discourse.orthanc-server.org/t/transfers-plugin-reduced-memory-usage-patch/6479).
+
 Version 1.7 (2025-12-15)
 ========================
 
--- a/UnitTests/UnitTestsMain.cpp	Fri Aug 14 08:49:57 2026 +0200
+++ b/UnitTests/UnitTestsMain.cpp	Mon Sep 21 15:02:15 2026 +0200
@@ -431,6 +431,31 @@
 }
 
 
+TEST(DownloadArea, StreamingMD5AcrossChunkBoundary)
+{
+  using namespace OrthancPlugins;
+
+  // Larger than DownloadArea's internal MD5 read-chunk size (1MB), and not
+  // a multiple of it, so an off-by-one in the chunked reading loop would
+  // corrupt the digest.
+  std::string s(3 * 1024 * 1024 + 12345, '\0');
+  for (size_t i = 0; i < s.size(); i++)
+  {
+    s[i] = static_cast<char>((i * 131 + 7) % 256);
+  }
+
+  std::string md5;
+  Orthanc::Toolbox::ComputeMD5(md5, s);
+
+  std::vector<DicomInstanceInfo> instances;
+  instances.push_back(DicomInstanceInfo("large", s.size(), md5));
+
+  DownloadArea area(instances);
+  area.WriteInstance("large", s.c_str(), s.size());
+  area.CheckMD5();
+}
+
+
 
 int main(int argc, char **argv)
 {