# HG changeset patch # User Alain Mazy # Date 1773421332 -3600 # Node ID b4f6c00c343f334961e24f8fc8be490ed2c3f9c0 # Parent fce05c8b00fb8cbf962b3db8c3d5e4bf789263ef added 2 metrics for WADO-RS diff -r fce05c8b00fb -r b4f6c00c343f NEWS --- a/NEWS Fri Feb 13 14:18:28 2026 +0100 +++ b/NEWS Fri Mar 13 18:02:12 2026 +0100 @@ -3,6 +3,14 @@ * If calling /rendered route on a Encapsulated PDF, the plugin will now return the pdf file instead of generating a 400 error. +* Added metrics: + - orthanc_dicomweb_wadors_average_bandwidth_per_call_mbytes_per_second_5m is the weighted + average bandwidth of each individual call to any WADO-RS route to retrieve instances, series + or studies. This metrics is only updated at the end of the call and averages the bandwidth + values that are also displayed in the logs if you set "EnablePerformanceLogs" to true. + - orthanc_dicomweb_wadors_total_bytes_transferred counts the number of bytes that have + been returned by any WADO-RS route to retrieve instances, series or studies. This is updated + while the response is returned. Version 1.22 (2025-12-02) diff -r fce05c8b00fb -r b4f6c00c343f Plugin/Plugin.cpp --- a/Plugin/Plugin.cpp Fri Feb 13 14:18:28 2026 +0100 +++ b/Plugin/Plugin.cpp Fri Mar 13 18:02:12 2026 +0100 @@ -466,6 +466,11 @@ #endif +static void RefreshMetricsCallback() +{ + RefreshWadoRsMetrics(); +} + static OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, OrthancPluginResourceType resourceType, const char *resourceId) @@ -711,6 +716,7 @@ OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); + OrthancPluginRegisterRefreshMetricsCallback(context, RefreshMetricsCallback); // Extend the default Orthanc Explorer with custom JavaScript for STOW client std::string explorer; diff -r fce05c8b00fb -r b4f6c00c343f Plugin/WadoRs.cpp --- a/Plugin/WadoRs.cpp Fri Feb 13 14:18:28 2026 +0100 +++ b/Plugin/WadoRs.cpp Fri Mar 13 18:02:12 2026 +0100 @@ -43,6 +43,7 @@ #include #include #include +#include "WeightedAverageMetrics.h" static const std::string SERIES_METADATA_ATTACHMENT_ID = "4301"; static std::string WADO_BASE_PLACEHOLDER = "$WADO_BASE_PLACEHOLDER$"; @@ -59,7 +60,10 @@ static boost::mutex preloaderThreadsCounterMutex; static uint32_t preloaderThreadsCounter = 0; +static WeightedAverageMetrics wadorsAverageBandwidth(300); +static boost::mutex wadoRsTotalBytesTransferredMutex; +static int64_t wadoRsTotalBytesTransferred = 0; void SetPluginCanUseExtendedFind(bool enable) { @@ -81,6 +85,12 @@ return isSystemReadOnly_; } +void RefreshWadoRsMetrics() +{ + OrthancPlugins::SetMetricsValue("orthanc_dicomweb_wadors_average_bandwidth_per_call_mbytes_per_second_5m", wadorsAverageBandwidth.GetAverage()); + OrthancPlugins::SetMetricsValue("orthanc_dicomweb_wadors_total_bytes_transferred", wadoRsTotalBytesTransferred); +} + static std::string GetResourceUri(Orthanc::ResourceType level, const std::string& publicId) { @@ -768,6 +778,9 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } perfTotalSizeInBytes += dicom->GetSize(); + + boost::mutex::scoped_lock lock(wadoRsTotalBytesTransferredMutex); + wadoRsTotalBytesTransferred += static_cast(dicom->GetSize()); } else { @@ -775,9 +788,13 @@ } } + uint64_t elapsedMicrosends = perfTimer.GetElapsedMicroseconds(); + + float bandwidth = float(perfTotalSizeInBytes) / float(elapsedMicrosends) * 8.0f; // this gives a bandwidth in MBps + wadorsAverageBandwidth.AddValue(bandwidth, float(perfTotalSizeInBytes)); + if (OrthancPlugins::Configuration::IsPerformanceLogsEnabled()) { - uint64_t elapsedMicrosends = perfTimer.GetElapsedMicroseconds(); float instancesPerSeconds = float(perfTotalInstancesCount) / (float(elapsedMicrosends) / 1000000.0f); LOG(INFO) << "WADO-RS: elapsed: " << perfTimer.GetElapsedMicroseconds() << " us, rate: " << std::fixed << std::setprecision(2) << instancesPerSeconds << " instances/s, " << Orthanc::Toolbox::GetHumanTransferSpeed(false, perfTotalSizeInBytes, elapsedMicrosends * 1000); } diff -r fce05c8b00fb -r b4f6c00c343f Plugin/WadoRs.h --- a/Plugin/WadoRs.h Fri Feb 13 14:18:28 2026 +0100 +++ b/Plugin/WadoRs.h Fri Mar 13 18:02:12 2026 +0100 @@ -118,4 +118,6 @@ void SetPluginCanUseExtendedFind(bool enable); -void SetSystemIsReadOnly(bool isReadOnly); \ No newline at end of file +void SetSystemIsReadOnly(bool isReadOnly); + +void RefreshWadoRsMetrics(); \ No newline at end of file diff -r fce05c8b00fb -r b4f6c00c343f Plugin/WeightedAverageMetrics.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugin/WeightedAverageMetrics.h Fri Mar 13 18:02:12 2026 +0100 @@ -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-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, 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 . + **/ + + +#pragma once + +#include +#include +#include +#include + + +template +class WeightedAverageMetrics : public boost::noncopyable +{ + struct Value + { + boost::posix_time::ptime time_; + T value_; + T weight_; + + Value(const T& value, const T& weight) : + time_(boost::posix_time::microsec_clock::universal_time()), + value_(value), + weight_(weight) + { + } + }; + +private: + std::deque values_; + T totalWeightedValue_; + T totalWeight_; + int64_t duration_; + boost::mutex mutex_; + + void RemoveOldest() + { + boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); + + if (values_.size() > 0) + { + Value& oldest = values_.front(); + while ((now - oldest.time_).total_seconds() > duration_) + { + totalWeightedValue_ -= oldest.value_ * oldest.weight_; + totalWeight_ -= oldest.weight_; + values_.pop_front(); + if (values_.size() > 0) + { + oldest = values_.front(); + } + } + } + } + +public: + WeightedAverageMetrics(int64_t duration) : + totalWeightedValue_(0), + totalWeight_(0), + duration_(duration) + { + } + + void AddValue(const T& value, const T& weight) + { + boost::mutex::scoped_lock lock(mutex_); + + values_.push_back(Value(value, weight)); + totalWeightedValue_ += value * weight; + totalWeight_ += weight; + boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); + + const Value& oldest = values_.front(); + while ((now - oldest.time_).total_seconds() > duration_) + { + totalWeightedValue_ -= oldest.value_ * oldest.weight_; + totalWeight_ -= oldest.weight_; + values_.pop_front(); + } + } + + T GetAverage() + { + boost::mutex::scoped_lock lock(mutex_); + + RemoveOldest(); + + if (totalWeight_ > 0) + { + return totalWeightedValue_ / totalWeight_; + } + else + { + return 0; + } + } +}; + diff -r fce05c8b00fb -r b4f6c00c343f Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp --- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Fri Feb 13 14:18:28 2026 +0100 +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Fri Mar 13 18:02:12 2026 +0100 @@ -1544,11 +1544,25 @@ #endif /* HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 */ + static void CheckAnswerSizeIsLessThan4GB(const std::string& answer) + { + if (answer.size() > static_cast(std::numeric_limits::max())) + { + #if HAS_ORTHANC_EXCEPTION == 1 + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Cannot send HTTP response larger than 4GB"); + #else + ORTHANC_PLUGINS_LOG_ERROR("Cannot send HTTP response larger than 4GB"); + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + #endif + } + } + void AnswerJson(const Json::Value& value, OrthancPluginRestOutput* output) { std::string bodyString; - WriteStyledJson(bodyString, value); + WriteStyledJson(bodyString, value); + CheckAnswerSizeIsLessThan4GB(bodyString); OrthancPluginAnswerBuffer(GetGlobalContext(), output, bodyString.c_str(), bodyString.size(), "application/json"); } @@ -1556,6 +1570,7 @@ const char* mimeType, OrthancPluginRestOutput* output) { + CheckAnswerSizeIsLessThan4GB(answer); OrthancPluginAnswerBuffer(GetGlobalContext(), output, answer.c_str(), answer.size(), mimeType); } @@ -1564,6 +1579,26 @@ OrthancPluginSendHttpStatusCode(GetGlobalContext(), output, httpError); } + void AnswerHttpError(uint16_t httpError, + OrthancPluginRestOutput* output, + const std::string& answer, + const char* mimeType) + { + CheckAnswerSizeIsLessThan4GB(answer); + + OrthancPluginSetHttpHeader(GetGlobalContext(), + output, + "content-type", + mimeType); + + OrthancPluginSendHttpStatus(GetGlobalContext(), + output, + httpError, + answer.c_str(), + static_cast(answer.size())); + } + + void AnswerMethodNotAllowed(OrthancPluginRestOutput *output, const char* allowedMethods) { OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowedMethods); diff -r fce05c8b00fb -r b4f6c00c343f Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h --- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Fri Feb 13 14:18:28 2026 +0100 +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Fri Mar 13 18:02:12 2026 +0100 @@ -690,6 +690,11 @@ void AnswerHttpError(uint16_t httpError, OrthancPluginRestOutput* output); + void AnswerHttpError(uint16_t httpError, + OrthancPluginRestOutput* output, + const std::string& answer, + const char* mimeType); + void AnswerMethodNotAllowed(OrthancPluginRestOutput* output, const char* allowedMethods); #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0) @@ -1026,6 +1031,13 @@ value, OrthancPluginMetricsType_Default); } + inline void SetMetricsValue(const char* name, + int64_t value) + { + OrthancPluginSetMetricsIntegerValue(GetGlobalContext(), name, + value, OrthancPluginMetricsType_Default); + } + class MetricsTimer : public boost::noncopyable { private: