Mercurial > hg > orthanc-wsi
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);