changeset 4792:434843934307 storage-cache

Added a StorageCache in the StorageAccessor
author Alain Mazy <am@osimis.io>
date Thu, 30 Sep 2021 12:14:19 +0200
parents 9754d5f2f38a
children 4e765c18ace7
files NEWS OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake OrthancFramework/Sources/Cache/MemoryObjectCache.h OrthancFramework/Sources/Cache/MemoryStringCache.cpp OrthancFramework/Sources/Cache/MemoryStringCache.h OrthancFramework/Sources/FileStorage/StorageAccessor.cpp OrthancFramework/Sources/FileStorage/StorageAccessor.h OrthancFramework/Sources/FileStorage/StorageCache.cpp OrthancFramework/Sources/FileStorage/StorageCache.h OrthancFramework/Sources/StringMemoryBuffer.cpp OrthancFramework/Sources/StringMemoryBuffer.h OrthancFramework/UnitTestsSources/FileStorageTests.cpp OrthancServer/Resources/Configuration.json OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerToolbox.cpp OrthancServer/Sources/main.cpp
diffstat 18 files changed, 375 insertions(+), 25 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Sep 14 14:51:12 2021 +0200
+++ b/NEWS	Thu Sep 30 12:14:19 2021 +0200
@@ -1,6 +1,19 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* Added a storage cache in RAM to avoid reading the same files multiple times from 
+  the storage.  This greatly improves, among other things, the performance of WADO-RS
+  retrieval of individual frames of multiframe instances.
+* New configuration option "MaximumStorageCacheSize" to configure the size of
+  the new storage cache.
+
+
+Maintenance
+-----------
+
 * Fix handling of option "DeidentifyLogs", notably for tags (0010,0010) and (0010,0020)
 
 
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Thu Sep 30 12:14:19 2021 +0200
@@ -385,6 +385,7 @@
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/HierarchicalZipWriter.cpp
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/ZipWriter.cpp
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/StorageAccessor.cpp
+      ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/StorageCache.cpp
       )
   endif()
 endif()
--- a/OrthancFramework/Sources/Cache/MemoryObjectCache.h	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.h	Thu Sep 30 12:14:19 2021 +0200
@@ -37,6 +37,9 @@
 
 namespace Orthanc
 {
+  /**
+   *  Note: this class is thread safe
+   **/
   class ORTHANC_PUBLIC MemoryObjectCache : public boost::noncopyable
   {
   private:
--- a/OrthancFramework/Sources/Cache/MemoryStringCache.cpp	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.cpp	Thu Sep 30 12:14:19 2021 +0200
@@ -35,7 +35,12 @@
       content_(content)
     {
     }
-      
+
+    explicit StringValue(const char* buffer, size_t size) :
+      content_(buffer, size)
+    {
+    }
+
     const std::string& GetContent() const
     {
       return content_;
@@ -63,6 +68,13 @@
     cache_.Acquire(key, new StringValue(value));
   }
 
+  void MemoryStringCache::Add(const std::string& key,
+                              const void* buffer,
+                              size_t size)
+  {
+    cache_.Acquire(key, new StringValue(reinterpret_cast<const char*>(buffer), size));
+  }
+
   void MemoryStringCache::Invalidate(const std::string &key)
   {
     cache_.Invalidate(key);
--- a/OrthancFramework/Sources/Cache/MemoryStringCache.h	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.h	Thu Sep 30 12:14:19 2021 +0200
@@ -29,6 +29,8 @@
   /**
    * Facade object around "MemoryObjectCache" that caches a dictionary
    * of strings, using the "fetch/add" paradigm of memcached.
+   * 
+   * Note: this class is thread safe
    **/
   class ORTHANC_PUBLIC MemoryStringCache : public boost::noncopyable
   {
@@ -44,7 +46,11 @@
 
     void Add(const std::string& key,
              const std::string& value);
-    
+
+    void Add(const std::string& key,
+             const void* buffer,
+             size_t size);
+
     void Invalidate(const std::string& key);
 
     bool Fetch(std::string& value,
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Thu Sep 30 12:14:19 2021 +0200
@@ -22,7 +22,10 @@
 
 #include "../PrecompiledHeaders.h"
 #include "StorageAccessor.h"
+#include "StorageCache.h"
 
+#include "../Logging.h"
+#include "../StringMemoryBuffer.h"
 #include "../Compatibility.h"
 #include "../Compression/ZlibCompressor.h"
 #include "../MetricsRegistry.h"
@@ -58,14 +61,18 @@
   };
 
 
-  StorageAccessor::StorageAccessor(IStorageArea &area) :
+  StorageAccessor::StorageAccessor(IStorageArea &area, StorageCache& cache) :
     area_(area),
+    cache_(cache),
     metrics_(NULL)
   {
   }
 
-  StorageAccessor::StorageAccessor(IStorageArea &area, MetricsRegistry &metrics) :
+  StorageAccessor::StorageAccessor(IStorageArea &area, 
+                                   StorageCache& cache,
+                                   MetricsRegistry &metrics) :
     area_(area),
+    cache_(cache),
     metrics_(&metrics)
   {
   }
@@ -93,6 +100,8 @@
         MetricsTimer timer(*this, METRICS_CREATE);
 
         area_.Create(uuid, data, size, type);
+        cache_.Add(uuid, type, data, size);
+
         return FileInfo(uuid, type, size, md5);
       }
 
@@ -123,6 +132,7 @@
           }
         }
 
+        cache_.Add(uuid, type, data, size);
         return FileInfo(uuid, type, size, md5,
                         CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
       }
@@ -145,6 +155,13 @@
   void StorageAccessor::Read(std::string& content,
                              const FileInfo& info)
   {
+    if (cache_.Fetch(content, info.GetUuid(), info.GetContentType()))
+    {
+      LOG(INFO) << "Read attachment \"" << info.GetUuid() << "\" "
+                << "content type from cache";
+      return;
+    }
+
     switch (info.GetCompressionType())
     {
       case CompressionType_None:
@@ -152,7 +169,9 @@
         MetricsTimer timer(*this, METRICS_READ);
 
         std::unique_ptr<IMemoryBuffer> buffer(area_.Read(info.GetUuid(), info.GetContentType()));
-        buffer->MoveToString(content);        
+        buffer->MoveToString(content);
+
+        cache_.Add(info.GetUuid(), info.GetContentType(), content);
         break;
       }
 
@@ -168,6 +187,8 @@
         }
 
         zlib.Uncompress(content, compressed->GetData(), compressed->GetSize());
+
+        cache_.Add(info.GetUuid(), info.GetContentType(), content);
         break;
       }
 
@@ -196,6 +217,14 @@
   {
     MetricsTimer timer(*this, METRICS_REMOVE);
     area_.Remove(fileUuid, type);
+
+    cache_.Invalidate(fileUuid, type);
+    
+    // in ReadStartRange, we might have cached only the start of the file -> try to remove it
+    if (type == FileContentType_Dicom)
+    {
+      cache_.Invalidate(fileUuid, FileContentType_DicomUntilPixelData);
+    }
   }
 
   void StorageAccessor::Remove(const FileInfo &info)
@@ -203,15 +232,56 @@
     Remove(info.GetUuid(), info.GetContentType());
   }
 
+  IMemoryBuffer* StorageAccessor::ReadStartRange(const std::string& fileUuid,
+                                                 FileContentType contentType,
+                                                 uint64_t end /* exclusive */,
+                                                 FileContentType startFileContentType)
+  {
+    std::string content;
+    if (cache_.Fetch(content, fileUuid, contentType))
+    {
+      LOG(INFO) << "Read attachment \"" << fileUuid << "\" "
+                << "(range from " << 0 << " to " << end << ") from cache";
+
+      return StringMemoryBuffer::CreateFromCopy(content, 0, end);
+    }
+
+    if (cache_.Fetch(content, fileUuid, startFileContentType))
+    {
+      LOG(INFO) << "Read attachment \"" << fileUuid << "\" "
+                << "(range from " << 0 << " to " << end << ") from cache";
+
+      assert(content.size() == end);
+      return StringMemoryBuffer::CreateFromCopy(content);
+    }
+
+    std::unique_ptr<IMemoryBuffer> buffer(area_.ReadRange(fileUuid, contentType, 0, end));
+
+    // we've read only the first part of the file -> add an entry in the cache
+    // note the uuid is still the uuid of the full file but the type is the type of the start of the file !
+    assert(buffer->GetSize() == end);
+    cache_.Add(fileUuid, startFileContentType, buffer->GetData(), buffer->GetSize());
+    return buffer.release();
+  }
+
+
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
   void StorageAccessor::SetupSender(BufferHttpSender& sender,
                                     const FileInfo& info,
                                     const std::string& mime)
   {
+    if (cache_.Fetch(sender.GetBuffer(), info.GetUuid(), info.GetContentType()))
+    {
+      LOG(INFO) << "Read attachment \"" << info.GetUuid() << "\" "
+                << "content type from cache";
+    }
+    else
     {
       MetricsTimer timer(*this, METRICS_READ);
       std::unique_ptr<IMemoryBuffer> buffer(area_.Read(info.GetUuid(), info.GetContentType()));
       buffer->MoveToString(sender.GetBuffer());
+
+      cache_.Add(info.GetUuid(), info.GetContentType(), sender.GetBuffer());
     }
 
     sender.SetContentType(mime);
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Thu Sep 30 12:14:19 2021 +0200
@@ -54,6 +54,7 @@
 namespace Orthanc
 {
   class MetricsRegistry;
+  class StorageCache;
 
   /**
    * This class handles the compression/decompression of the raw files
@@ -66,6 +67,7 @@
     class MetricsTimer;
 
     IStorageArea&     area_;
+    StorageCache&     cache_;
     MetricsRegistry*  metrics_;
 
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
@@ -75,9 +77,11 @@
 #endif
 
   public:
-    explicit StorageAccessor(IStorageArea& area);
+    explicit StorageAccessor(IStorageArea& area,
+                             StorageCache& cache);
 
     StorageAccessor(IStorageArea& area,
+                    StorageCache& cache,
                     MetricsRegistry& metrics);
 
     FileInfo Write(const void* data,
@@ -97,6 +101,11 @@
     void ReadRaw(std::string& content,
                  const FileInfo& info);
 
+    IMemoryBuffer* ReadStartRange(const std::string& fileUuid,
+                                  FileContentType fullFileContentType,
+                                  uint64_t end /* exclusive */,
+                                  FileContentType startFileContentType);
+
     void Remove(const std::string& fileUuid,
                 FileContentType type);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/StorageCache.cpp	Thu Sep 30 12:14:19 2021 +0200
@@ -0,0 +1,118 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * 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 3 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
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "StorageCache.h"
+
+#include "../Compatibility.h"
+#include "../OrthancException.h"
+
+
+
+namespace Orthanc
+{
+  bool IsAcceptedContentType(FileContentType contentType)
+  {
+    return contentType == FileContentType_Dicom ||
+      contentType == FileContentType_DicomUntilPixelData ||
+      contentType == FileContentType_DicomAsJson;
+  }
+
+  const char* ToString(FileContentType contentType)
+  {
+    switch (contentType)
+    {
+      case FileContentType_Dicom:
+        return "dicom";
+      case FileContentType_DicomUntilPixelData:
+        return "dicom-header";
+      case FileContentType_DicomAsJson:
+        return "dicom-json";
+      default:
+        throw OrthancException(ErrorCode_InternalError,
+                               "ContentType not supported in StorageCache");
+    }
+  }
+
+  void GetCacheKey(std::string& key, const std::string& uuid, FileContentType contentType)
+  {
+    key = uuid + ":" + std::string(ToString(contentType));
+  }
+  
+  void StorageCache::SetMaximumSize(size_t size)
+  {
+    cache_.SetMaximumSize(size);
+  }
+
+  void StorageCache::Add(const std::string& uuid, 
+                         FileContentType contentType,
+                         const std::string& value)
+  {
+    if (!IsAcceptedContentType(contentType))
+    {
+      return;
+    }
+
+    std::string key;
+    GetCacheKey(key, uuid, contentType);
+    cache_.Add(key, value);
+  }
+
+  void StorageCache::Add(const std::string& uuid, 
+                         FileContentType contentType,
+                         const void* buffer,
+                         size_t size)
+  {
+    if (!IsAcceptedContentType(contentType))
+    {
+      return;
+    }
+    
+    std::string key;
+    GetCacheKey(key, uuid, contentType);
+    cache_.Add(key, buffer, size);
+  }
+
+  void StorageCache::Invalidate(const std::string& uuid, FileContentType contentType)
+  {
+    std::string key;
+    GetCacheKey(key, uuid, contentType);
+    cache_.Invalidate(key);
+  }
+
+  bool StorageCache::Fetch(std::string& value, 
+                           const std::string& uuid,
+                           FileContentType contentType)
+  {
+    if (!IsAcceptedContentType(contentType))
+    {
+      return false;
+    }
+
+    std::string key;
+    GetCacheKey(key, uuid, contentType);
+
+    return cache_.Fetch(value, key);
+  }
+
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/StorageCache.h	Thu Sep 30 12:14:19 2021 +0200
@@ -0,0 +1,59 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * 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 3 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
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Cache/MemoryStringCache.h"
+
+#include "../Compatibility.h"  // For ORTHANC_OVERRIDE
+
+#include <boost/thread/mutex.hpp>
+#include <map>
+
+namespace Orthanc
+{
+   /**
+   *  Note: this class is thread safe
+   **/
+   class ORTHANC_PUBLIC StorageCache : public boost::noncopyable
+    {
+      MemoryStringCache   cache_;
+    public:
+      void SetMaximumSize(size_t size);
+
+      void Add(const std::string& uuid, 
+               FileContentType contentType,
+               const std::string& value);
+
+      void Add(const std::string& uuid, 
+               FileContentType contentType,
+               const void* buffer,
+               size_t size);
+
+      void Invalidate(const std::string& uuid, FileContentType contentType);
+
+      bool Fetch(std::string& value, 
+                 const std::string& uuid,
+                 FileContentType contentType);
+
+    };
+}
--- a/OrthancFramework/Sources/StringMemoryBuffer.cpp	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancFramework/Sources/StringMemoryBuffer.cpp	Thu Sep 30 12:14:19 2021 +0200
@@ -47,4 +47,14 @@
     result->Copy(buffer);
     return result.release();
   }
+
+
+  IMemoryBuffer* StringMemoryBuffer::CreateFromCopy(const std::string& buffer, 
+                                                    size_t start /* inclusive */, 
+                                                    size_t end /* exclusive */)
+  {
+    std::unique_ptr<StringMemoryBuffer> result(new StringMemoryBuffer);
+    result->Copy(buffer, start, end);
+    return result.release();
+  }
 }
--- a/OrthancFramework/Sources/StringMemoryBuffer.h	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancFramework/Sources/StringMemoryBuffer.h	Thu Sep 30 12:14:19 2021 +0200
@@ -38,6 +38,11 @@
       buffer_ = buffer;
     }
 
+    void Copy(const std::string& buffer, size_t start /* inclusive */, size_t end /* exclusive */)
+    {
+      buffer_.assign(buffer, start, end - start);
+    }
+
     void Swap(std::string& buffer)
     {
       buffer_.swap(buffer);
@@ -58,5 +63,7 @@
     static IMemoryBuffer* CreateFromSwap(std::string& buffer);
 
     static IMemoryBuffer* CreateFromCopy(const std::string& buffer);
+
+    static IMemoryBuffer* CreateFromCopy(const std::string& buffer, size_t start /* inclusive */, size_t end /* exclusive */);
   };
 }
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Thu Sep 30 12:14:19 2021 +0200
@@ -29,6 +29,7 @@
 
 #include "../Sources/FileStorage/FilesystemStorage.h"
 #include "../Sources/FileStorage/StorageAccessor.h"
+#include "../Sources/FileStorage/StorageCache.h"
 #include "../Sources/HttpServer/BufferHttpSender.h"
 #include "../Sources/HttpServer/FilesystemHttpSender.h"
 #include "../Sources/Logging.h"
@@ -124,7 +125,8 @@
 TEST(StorageAccessor, NoCompression)
 {
   FilesystemStorage s("UnitTestsStorage");
-  StorageAccessor accessor(s);
+  StorageCache cache;
+  StorageAccessor accessor(s, cache);
 
   std::string data = "Hello world";
   FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_None, true);
@@ -145,7 +147,8 @@
 TEST(StorageAccessor, Compression)
 {
   FilesystemStorage s("UnitTestsStorage");
-  StorageAccessor accessor(s);
+  StorageCache cache;
+  StorageAccessor accessor(s, cache);
 
   std::string data = "Hello world";
   FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_ZlibWithSize, true);
@@ -165,7 +168,8 @@
 TEST(StorageAccessor, Mix)
 {
   FilesystemStorage s("UnitTestsStorage");
-  StorageAccessor accessor(s);
+  StorageCache cache;
+  StorageAccessor accessor(s, cache);
 
   std::string r;
   std::string compressedData = "Hello";
--- a/OrthancServer/Resources/Configuration.json	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancServer/Resources/Configuration.json	Thu Sep 30 12:14:19 2021 +0200
@@ -40,7 +40,13 @@
   // in the storage (a value of "0" indicates no limit on the number
   // of patients)
   "MaximumPatientCount" : 0,
-  
+
+  // Maximum size of the storage cache in MB.  The storage cache
+  // is stored in RAM and contains a copy of recently accessed
+  // files (written or read).  A value of "0" indicates the cache
+  // is disabled.  (new in Orthanc 1.9.8)
+  "MaximumStorageCacheSize" : 128,
+
   // List of paths to the custom Lua scripts that are to be loaded
   // into this instance of Orthanc
   "LuaScripts" : [
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Sep 30 12:14:19 2021 +0200
@@ -2969,7 +2969,7 @@
     std::string publicId = call.GetUriComponent("id", "");
 
     std::string dicomContent;
-    context.ReadDicom(dicomContent, publicId);
+    context.ReadDicomForHeader(dicomContent, publicId);
 
     // TODO Consider using "DicomMap::ParseDicomMetaInformation()" to
     // speed up things here
--- a/OrthancServer/Sources/ServerContext.cpp	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Thu Sep 30 12:14:19 2021 +0200
@@ -484,7 +484,7 @@
   void ServerContext::RemoveFile(const std::string& fileUuid,
                                  FileContentType type)
   {
-    StorageAccessor accessor(area_, GetMetricsRegistry());
+    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
     accessor.Remove(fileUuid, type);
   }
 
@@ -526,7 +526,7 @@
     try
     {
       MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_store_dicom_duration_ms");
-      StorageAccessor accessor(area_, GetMetricsRegistry());
+      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
 
       DicomInstanceHasher hasher(summary);
       resultPublicId = hasher.HashInstance();
@@ -760,7 +760,7 @@
     }
     else
     {
-      StorageAccessor accessor(area_, GetMetricsRegistry());
+      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
       accessor.AnswerFile(output, attachment, GetFileContentMime(content));
     }
   }
@@ -790,7 +790,7 @@
 
     std::string content;
 
-    StorageAccessor accessor(area_, GetMetricsRegistry());
+    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
     accessor.Read(content, attachment);
 
     FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(),
@@ -846,7 +846,7 @@
       std::string dicom;
 
       {
-        StorageAccessor accessor(area_, GetMetricsRegistry());
+        StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
         accessor.Read(dicom, attachment);
       }
 
@@ -911,8 +911,8 @@
       
         std::unique_ptr<IMemoryBuffer> dicom;
         {
-          MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_storage_read_range_duration_ms");
-          dicom.reset(area_.ReadRange(attachment.GetUuid(), FileContentType_Dicom, 0, pixelDataOffset));
+          StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
+          dicom.reset(accessor.ReadStartRange(attachment.GetUuid(), FileContentType_Dicom, pixelDataOffset, FileContentType_DicomUntilPixelData));
         }
 
         if (dicom.get() == NULL)
@@ -941,7 +941,7 @@
         std::string dicomAsJson;
 
         {
-          StorageAccessor accessor(area_, GetMetricsRegistry());
+          StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
           accessor.Read(dicomAsJson, attachment);
         }
 
@@ -1010,7 +1010,15 @@
     int64_t revision;
     ReadAttachment(dicom, revision, instancePublicId, FileContentType_Dicom, true /* uncompress */);
   }
-    
+
+  void ServerContext::ReadDicomForHeader(std::string& dicom,
+                                         const std::string& instancePublicId)
+  {
+    if (!ReadDicomUntilPixelData(dicom, instancePublicId))
+    {
+      ReadDicom(dicom, instancePublicId);
+    }
+  }
 
   bool ServerContext::ReadDicomUntilPixelData(std::string& dicom,
                                               const std::string& instancePublicId)
@@ -1039,8 +1047,10 @@
       {
         uint64_t pixelDataOffset = boost::lexical_cast<uint64_t>(s);
 
+        StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
+
         std::unique_ptr<IMemoryBuffer> buffer(
-          area_.ReadRange(attachment.GetUuid(), attachment.GetContentType(), 0, pixelDataOffset));
+          accessor.ReadStartRange(attachment.GetUuid(), attachment.GetContentType(), pixelDataOffset, FileContentType_DicomUntilPixelData));
         buffer->MoveToString(dicom);
         return true;   // Success
       }
@@ -1071,7 +1081,7 @@
     assert(attachment.GetContentType() == content);
 
     {
-      StorageAccessor accessor(area_, GetMetricsRegistry());
+      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
 
       if (uncompressIfNeeded)
       {
@@ -1171,7 +1181,7 @@
     // TODO Should we use "gzip" instead?
     CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
 
-    StorageAccessor accessor(area_, GetMetricsRegistry());
+    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
     FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_);
 
     try
--- a/OrthancServer/Sources/ServerContext.h	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Thu Sep 30 12:14:19 2021 +0200
@@ -43,6 +43,7 @@
 #include "../../OrthancFramework/Sources/DicomParsing/DicomModification.h"
 #include "../../OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h"
 #include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h"
+#include "../../OrthancFramework/Sources/FileStorage/StorageCache.h"
 #include "../../OrthancFramework/Sources/MultiThreading/Semaphore.h"
 
 
@@ -169,6 +170,7 @@
 
     ServerIndex index_;
     IStorageArea& area_;
+    StorageCache storageCache_;
 
     bool compressionEnabled_;
     bool storeMD5_;
@@ -288,6 +290,11 @@
       return index_;
     }
 
+    void SetMaximumStorageCacheSize(size_t size)
+    {
+      return storageCache_.SetMaximumSize(size);
+    }
+
     void SetCompressionEnabled(bool enabled);
 
     bool IsCompressionEnabled() const
@@ -325,7 +332,10 @@
 
     void ReadDicom(std::string& dicom,
                    const std::string& instancePublicId);
-    
+
+    void ReadDicomForHeader(std::string& dicom,
+                            const std::string& instancePublicId);
+
     bool ReadDicomUntilPixelData(std::string& dicom,
                                  const std::string& instancePublicId);
 
--- a/OrthancServer/Sources/ServerToolbox.cpp	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Thu Sep 30 12:14:19 2021 +0200
@@ -36,6 +36,7 @@
 
 #include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h"
 #include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h"
+#include "../../OrthancFramework/Sources/FileStorage/StorageCache.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/OrthancException.h"
 #include "Database/IDatabaseWrapper.h"
@@ -176,7 +177,8 @@
         try
         {
           // Read and parse the content of the DICOM file
-          StorageAccessor accessor(storageArea);
+          StorageCache cache; // we create a temporary cache for this operation (required by the StorageAccessor)
+          StorageAccessor accessor(storageArea, cache);
 
           std::string content;
           accessor.Read(content, attachment);
--- a/OrthancServer/Sources/main.cpp	Tue Sep 14 14:51:12 2021 +0200
+++ b/OrthancServer/Sources/main.cpp	Thu Sep 30 12:14:19 2021 +0200
@@ -1521,6 +1521,16 @@
     {
       context.GetIndex().SetMaximumStorageSize(0);
     }
+
+    try
+    {
+      uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageCacheSize", 128);
+      context.SetMaximumStorageCacheSize(size * 1024 * 1024);
+    }
+    catch (...)
+    {
+      context.SetMaximumStorageCacheSize(128);
+    }
   }
 
   {