changeset 73:a8c90aa32ca6

LRU caching of pyramids, OrthancWSIClearCache script
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 28 Nov 2016 16:51:19 +0100
parents ea6309f70f1f
children 4089a3dfc612 fc7b03eaeece 7037851e7244
files NEWS Resources/OrthancWSIClearCache.py TODO ViewerPlugin/CMakeLists.txt ViewerPlugin/DicomPyramidCache.cpp ViewerPlugin/DicomPyramidCache.h ViewerPlugin/Plugin.cpp
diffstat 7 files changed, 333 insertions(+), 79 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Nov 28 10:40:48 2016 +0100
+++ b/NEWS	Mon Nov 28 16:51:19 2016 +0100
@@ -4,6 +4,8 @@
 * Huge speed-up in the whole-slide imaging Web viewer plugin:
   - Reduction of the number of calls to the Orthanc REST API
   - Cache pre-computed information for each instance as metadata
+  - Larger cache with LRU recycling to improve viewer performance
+* "OrthancWSIClearCache.py" companion script to clear the WSI cache
 * Refactorings
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/OrthancWSIClearCache.py	Mon Nov 28 16:51:19 2016 +0100
@@ -0,0 +1,77 @@
+#!/usr/bin/python
+
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero 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
+# Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import base64
+import httplib2
+import json
+import os
+import sys
+
+if len(sys.argv) != 3 and len(sys.argv) != 5:
+    print("""
+Script to reinitialize the cache of the whole-slide imaging plugin for
+Orthanc. Please make sure that Orthanc is running before starting this
+script.
+
+Usage: %s [hostname] [HTTP port]
+Usage: %s [hostname] [HTTP port] [username] [password]
+For instance: %s 127.0.0.1 8042
+""" % (sys.argv[0], sys.argv[0], sys.argv[0]))
+    exit(-1)
+
+
+METADATA=4200
+
+
+def RunHttpRequest(uri, method, body = None):
+    http = httplib2.Http()
+    headers = { }
+
+    if len(sys.argv) == 5:
+        username = sys.argv[4]
+        password = sys.argv[5]
+
+        # h.add_credentials(username, password)
+
+        # This is a custom reimplementation of the
+        # "Http.add_credentials()" method for Basic HTTP Access
+        # Authentication (for some weird reason, this method does not
+        # always work)
+        # http://en.wikipedia.org/wiki/Basic_access_authentication
+        headers['authorization'] = 'Basic ' + base64.b64encode(username + ':' + password)       
+
+    url = 'http://%s:%d/%s' % (sys.argv[1], int(sys.argv[2]), uri)
+    resp, content = http.request(url, method,
+                                 body = body,
+                                 headers = headers)
+
+    if resp.status != 200:
+        raise Exception('Cannot %s on URL %s, HTTP status %d '
+                        '(Is Orthanc running? Is there a password?)' % 
+                        (method, url, resp.status))
+    else:
+        return content.decode('utf8')
+
+
+for instance in json.loads(RunHttpRequest('/instances', 'GET')):
+    print('Clearing cache for instance %s' % instance)
+    RunHttpRequest('/instances/%s/metadata/%s' % (instance, METADATA), 'DELETE')
+
+print('The WSI cache was successfully cleared')
--- a/TODO	Mon Nov 28 10:40:48 2016 +0100
+++ b/TODO	Mon Nov 28 16:51:19 2016 +0100
@@ -15,7 +15,6 @@
 Performance
 -----------
 
-* Larger cache with LRU recycling to improve viewer performance
 * Check out rapidjson: https://github.com/miloyip/nativejson-benchmark
 
 
--- a/ViewerPlugin/CMakeLists.txt	Mon Nov 28 10:40:48 2016 +0100
+++ b/ViewerPlugin/CMakeLists.txt	Mon Nov 28 16:51:19 2016 +0100
@@ -158,6 +158,7 @@
 #####################################################################
 
 set(ORTHANC_WSI_SOURCES
+  DicomPyramidCache.cpp
   Plugin.cpp
   ${ORTHANC_WSI_DIR}/Framework/DicomToolbox.cpp
   ${ORTHANC_WSI_DIR}/Framework/Enumerations.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/DicomPyramidCache.cpp	Mon Nov 28 16:51:19 2016 +0100
@@ -0,0 +1,156 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero 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
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../Framework/PrecompiledHeadersWSI.h"
+#include "DicomPyramidCache.h"
+
+#include <cassert>
+
+namespace OrthancWSI
+{
+  DicomPyramid* DicomPyramidCache::GetCachedPyramid(const std::string& seriesId)
+  {
+    // Mutex is assumed to be locked
+    DicomPyramid* pyramid = NULL;
+
+    // Is the series of interest already cached as a pyramid?
+    if (cache_.Contains(seriesId, pyramid))
+    {
+      if (pyramid == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      // Tag the series as the most recently used
+      cache_.MakeMostRecent(seriesId);
+    }
+
+    return pyramid;
+  }
+
+
+  DicomPyramid& DicomPyramidCache::GetPyramid(const std::string& seriesId,
+                                              boost::mutex::scoped_lock& lock)
+  {
+    // Mutex is assumed to be locked
+
+    {
+      DicomPyramid* pyramid = GetCachedPyramid(seriesId);
+      if (pyramid != NULL)
+      {
+        return *pyramid;
+      }
+    }
+
+    // Unlock the mutex to construct the pyramid (this is a
+    // time-consuming operation, we don't want it to block other clients)
+    lock.unlock();
+
+    std::auto_ptr<DicomPyramid> pyramid
+      (new DicomPyramid(orthanc_, seriesId, true /* use metadata cache */));
+
+    {
+      // The pyramid is constructed: Store it into the cache
+      lock.lock();
+
+      DicomPyramid* cached = GetCachedPyramid(seriesId);
+      if (cached != NULL)
+      {
+        // The pyramid was already constructed by another request in
+        // between, reuse the cached value (the auto_ptr destroys
+        // the just-constructed pyramid)
+        return *cached;
+      }
+
+      if (cache_.GetSize() == maxSize_)
+      {
+        // The cache has grown too large: First delete the least
+        // recently used entry
+        DicomPyramid* oldest = NULL;
+        cache_.RemoveOldest(oldest);
+
+        if (oldest == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+        else
+        {
+          delete oldest;
+        }
+      }
+
+      // Now we have at least one free entry in the cache
+      assert(cache_.GetSize() < maxSize_);
+
+      // Add a new element to the cache and make it the most
+      // recently used entry
+      DicomPyramid* payload = pyramid.release();
+      cache_.Add(seriesId, payload);
+      return *payload;
+    }
+  }
+
+
+  DicomPyramidCache::DicomPyramidCache(OrthancPlugins::IOrthancConnection& orthanc,
+                                       size_t maxSize) :
+    orthanc_(orthanc),
+    maxSize_(maxSize)
+  {
+  }
+
+
+  DicomPyramidCache::~DicomPyramidCache()
+  {
+    while (!cache_.IsEmpty())
+    {
+      DicomPyramid* pyramid = NULL;
+      std::string seriesId = cache_.RemoveOldest(pyramid);
+
+      if (pyramid != NULL)
+      {
+        delete pyramid;
+      }        
+    }
+  }
+
+
+  void DicomPyramidCache::Invalidate(const std::string& seriesId)
+  {
+    boost::mutex::scoped_lock  lock(mutex_);
+
+    if (cache_.Contains(seriesId))
+    {
+      std::auto_ptr<DicomPyramid> pyramid(cache_.Invalidate(seriesId));
+
+      if (pyramid.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  DicomPyramidCache::Locker::Locker(DicomPyramidCache& cache,
+                                    const std::string& seriesId) :
+    lock_(cache.mutex_),
+    pyramid_(cache.GetPyramid(seriesId, lock_))
+  {
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/DicomPyramidCache.h	Mon Nov 28 16:51:19 2016 +0100
@@ -0,0 +1,70 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero 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
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Framework/Inputs/DicomPyramid.h"
+#include "../Resources/Orthanc/Core/Cache/LeastRecentlyUsedIndex.h"
+
+#include <boost/thread/mutex.hpp>
+
+namespace OrthancWSI
+{
+  class DicomPyramidCache : public boost::noncopyable
+  {
+  private:
+    typedef Orthanc::LeastRecentlyUsedIndex<std::string, DicomPyramid*>  Cache;
+
+    boost::mutex                         mutex_;
+    OrthancPlugins::IOrthancConnection&  orthanc_;
+    size_t                               maxSize_;
+    Cache                                cache_;
+
+
+    DicomPyramid* GetCachedPyramid(const std::string& seriesId);
+
+    DicomPyramid& GetPyramid(const std::string& seriesId,
+                             boost::mutex::scoped_lock& lock);
+
+  public:
+    DicomPyramidCache(OrthancPlugins::IOrthancConnection& orthanc,
+                      size_t maxSize);
+
+    ~DicomPyramidCache();
+
+    void Invalidate(const std::string& seriesId);
+
+    class Locker : public boost::noncopyable
+    {
+    private:
+      boost::mutex::scoped_lock  lock_;
+      DicomPyramid&              pyramid_;
+
+    public:
+      Locker(DicomPyramidCache& cache,
+             const std::string& seriesId);
+
+      DicomPyramid& GetPyramid() const
+      {
+        return pyramid_;
+      }
+    };
+  };
+}
--- a/ViewerPlugin/Plugin.cpp	Mon Nov 28 10:40:48 2016 +0100
+++ b/ViewerPlugin/Plugin.cpp	Mon Nov 28 16:51:19 2016 +0100
@@ -19,7 +19,8 @@
 
 
 #include "../Framework/PrecompiledHeadersWSI.h"
-#include "../Framework/Inputs/DicomPyramid.h"
+
+#include "DicomPyramidCache.h"
 #include "../Framework/Jpeg2000Reader.h"
 
 #include "../Resources/Orthanc/Core/Images/ImageProcessing.h"
@@ -33,76 +34,6 @@
 
 #include <cassert>
 
-
-
-namespace OrthancWSI
-{
-  // TODO Add LRU recycling policy
-  class DicomPyramidCache : public boost::noncopyable
-  {
-  private:
-    boost::mutex                         mutex_;
-    OrthancPlugins::IOrthancConnection&  orthanc_;
-    std::auto_ptr<DicomPyramid>          pyramid_;
-
-    DicomPyramid& GetPyramid(const std::string& seriesId)
-    {
-      // Mutex is assumed to be locked
-
-      if (pyramid_.get() == NULL ||
-          pyramid_->GetSeriesId() != seriesId)
-      {
-        pyramid_.reset(new DicomPyramid(orthanc_, seriesId, true /* use metadata cache */));
-      }
-
-      if (pyramid_.get() == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-      }
-
-      return *pyramid_;
-    }
-
-  public:
-    DicomPyramidCache(OrthancPlugins::IOrthancConnection& orthanc) :
-      orthanc_(orthanc)
-    {
-    }
-
-    void Invalidate(const std::string& seriesId)
-    {
-      boost::mutex::scoped_lock  lock(mutex_);
-
-      if (pyramid_.get() != NULL &&
-          pyramid_->GetSeriesId() == seriesId)
-      {
-        pyramid_.reset(NULL);
-      }
-    }
-
-    class Locker : public boost::noncopyable
-    {
-    private:
-      boost::mutex::scoped_lock  lock_;
-      DicomPyramid&              pyramid_;
-
-    public:
-      Locker(DicomPyramidCache& cache,
-             const std::string& seriesId) :
-        lock_(cache.mutex_),
-        pyramid_(cache.GetPyramid(seriesId))
-      {
-      }
-
-      DicomPyramid& GetPyramid() const
-      {
-        return pyramid_;
-      }
-    };
-  };
-}
-
-
 OrthancPluginContext* context_ = NULL;
 
 std::auto_ptr<OrthancPlugins::OrthancPluginConnection>  orthanc_;
@@ -151,23 +82,41 @@
 
   OrthancWSI::DicomPyramidCache::Locker locker(*cache_, seriesId);
 
+  unsigned int tileWidth = locker.GetPyramid().GetTileWidth();
+  unsigned int tileHeight = locker.GetPyramid().GetTileHeight();
   unsigned int totalWidth = locker.GetPyramid().GetLevelWidth(0);
   unsigned int totalHeight = locker.GetPyramid().GetLevelHeight(0);
 
+  Json::Value sizes = Json::arrayValue;
   Json::Value resolutions = Json::arrayValue;
+  Json::Value tilesCount = Json::arrayValue;
   for (unsigned int i = 0; i < locker.GetPyramid().GetLevelCount(); i++)
   {
-    resolutions.append(static_cast<float>(totalWidth) /
-                       static_cast<float>(locker.GetPyramid().GetLevelWidth(i)));
+    unsigned int levelWidth = locker.GetPyramid().GetLevelWidth(i);
+    unsigned int levelHeight = locker.GetPyramid().GetLevelHeight(i);
+
+    resolutions.append(static_cast<float>(totalWidth) / static_cast<float>(levelWidth));
+    
+    Json::Value s = Json::arrayValue;
+    s.append(levelWidth);
+    s.append(levelHeight);
+    sizes.append(s);
+
+    s = Json::arrayValue;
+    s.append(OrthancWSI::CeilingDivision(levelWidth, tileWidth));
+    s.append(OrthancWSI::CeilingDivision(levelHeight, tileHeight));
+    tilesCount.append(s);
   }
 
   Json::Value result;
   result["ID"] = seriesId;
-  result["TotalWidth"] = totalWidth;
+  result["Resolutions"] = resolutions;
+  result["Sizes"] = sizes;
+  result["TileHeight"] = tileHeight;
+  result["TileWidth"] = tileWidth;
+  result["TilesCount"] = tilesCount;
   result["TotalHeight"] = totalHeight;
-  result["TileWidth"] = locker.GetPyramid().GetTileWidth();
-  result["TileHeight"] = locker.GetPyramid().GetTileHeight();
-  result["Resolutions"] = resolutions;
+  result["TotalWidth"] = totalWidth;
 
   std::string s = result.toStyledString();
   OrthancPluginAnswerBuffer(context_, output, s.c_str(), s.size(), "application/json");
@@ -378,7 +327,7 @@
     OrthancPluginSetDescription(context, "Provides a Web viewer of whole-slide microscopic images within Orthanc.");
 
     orthanc_.reset(new OrthancPlugins::OrthancPluginConnection(context));
-    cache_.reset(new OrthancWSI::DicomPyramidCache(*orthanc_));
+    cache_.reset(new OrthancWSI::DicomPyramidCache(*orthanc_, 10 /* Number of pyramids to be cached - TODO parameter */));
 
     OrthancPluginRegisterOnChangeCallback(context_, OnChangeCallback);