From 3880658576ee2511f2962b2a839eaba3e115c2d3 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Tue, 4 Aug 2026 13:44:33 -0400 Subject: [PATCH] Use caching to reduce number of requests made to API during acquisition --- xspdApp/src/ADXSPD.cpp | 45 +++++++++++++++++++++++++---------- xspdApp/src/ADXSPD.h | 1 + xspdApp/src/ADXSPDModule.cpp | 8 +++++++ xspdApp/src/ADXSPDModule.h | 1 + xspdApp/src/XSPDAPI.cpp | 41 ++++++++----------------------- xspdApp/src/XSPDAPI.h | 18 ++++++-------- xspdApp/tests/MockXSPDAPI.cpp | 1 + xspdApp/tests/TestXSPDAPI.cpp | 2 ++ 8 files changed, 63 insertions(+), 54 deletions(-) diff --git a/xspdApp/src/ADXSPD.cpp b/xspdApp/src/ADXSPD.cpp index bd21a84..f5d7e7d 100644 --- a/xspdApp/src/ADXSPD.cpp +++ b/xspdApp/src/ADXSPD.cpp @@ -144,15 +144,13 @@ asynStatus ADXSPD::acquireStart() { setIntegerParam(ADNumImagesCounter, 0); try { - int sizeX = this->pDetector->GetActiveDataPort()->GetVar("frame_width"); - int sizeY = this->pDetector->GetActiveDataPort()->GetVar("frame_height"); - setIntegerParam(ADSizeX, sizeX); - setIntegerParam(ADSizeY, sizeY); + // Frame geometry is cached at init and refreshed on roi_rows writes, so no need to + // re-read it from the detector on every acquisition. setIntegerParam(ADStatus, ADStatusAcquire); callParamCallbacks(); - this->pDetector->ExecCommand("start"); + this->pDetector->StartAcquisition(); INFO_TO_STATUS("Acquisition started"); @@ -163,6 +161,26 @@ asynStatus ADXSPD::acquireStart() { return asynSuccess; } +/** + * @brief Reads the data port frame geometry and caches it in the size parameters + * + * @return asynStatus asynSuccess on success, asynError if the geometry could not be read + */ +asynStatus ADXSPD::refreshFrameSize() { + try { + int sizeX = this->pDetector->GetActiveDataPort()->GetVar("frame_width"); + int sizeY = this->pDetector->GetActiveDataPort()->GetVar("frame_height"); + setIntegerParam(ADMaxSizeX, sizeX); + setIntegerParam(ADMaxSizeY, sizeY); + setIntegerParam(ADSizeX, sizeX); + setIntegerParam(ADSizeY, sizeY); + } catch (std::exception& e) { + ERR_ARGS("Failed to refresh frame size: %s", e.what()); + return asynError; + } + return asynSuccess; +} + /** * @brief stops acquisition by aborting exposure and joinging acq thread * @@ -171,7 +189,7 @@ asynStatus ADXSPD::acquireStart() { asynStatus ADXSPD::acquireStop() { setIntegerParam(ADAcquire, 0); try { - this->pDetector->ExecCommand("stop"); + this->pDetector->StopAcquisition(); setIntegerParam(ADStatus, ADStatusIdle); callParamCallbacks(); } catch (std::exception& e) { @@ -400,10 +418,12 @@ void ADXSPD::acquisitionThread() { // shuffle record). The XSPD::ShuffleMode enum has been set up with the same // values for ease of translation, so we can just static_cast it here. If ADCore // gets an enumeration for this in the future, it should be used here. - XSPD::ShuffleMode shuffleMode = - this->pDetector->GetVar("shuffle_mode"); + // Read from the cached parameter rather than the API to avoid a per-frame + // request. + int shuffleMode; + getIntegerParam(ADXSPD_ShuffleMode, &shuffleMode); // TODO: Handle auto shuffle mode correctly. - pArray->codec.shuffle = static_cast(shuffleMode); + pArray->codec.shuffle = shuffleMode; } if (!readoutOk) { @@ -615,8 +635,7 @@ asynStatus ADXSPD::getInitialDetState() { } // Retrieve all remaining initial parameters. - status |= this->getDataPortVar(ADMaxSizeX, "frame_width"); - status |= this->getDataPortVar(ADMaxSizeY, "frame_height"); + status |= this->refreshFrameSize(); status |= this->getDetVar(ADXSPD_SummedFrames, "summed_frames"); status |= this->getDetVar(ADXSPD_CompressLevel, "compression_level"); status |= this->getDetVar(ADXSPD_Compressor, "compressor"); @@ -698,7 +717,7 @@ asynStatus ADXSPD::writeInt32(asynUser* pasynUser, epicsInt32 value) { } else if (function == ADNumImages) { int maxNumImages = INT_MAX; for (auto& module : this->modules) { - int moduleMax = module->getMaxNumImages(); + int moduleMax = module->getCachedMaxNumImages(); if (moduleMax < maxNumImages) { maxNumImages = moduleMax; } @@ -730,6 +749,8 @@ asynStatus ADXSPD::writeInt32(asynUser* pasynUser, epicsInt32 value) { actualValue = this->pDetector->SetVar("summed_frames", value); } else if (function == ADXSPD_RoiRows) { actualValue = static_cast(this->pDetector->SetVar("roi_rows", value)); + // ROI readout may change the data port frame geometry; refresh cached size. + this->refreshFrameSize(); for (auto& module : this->modules) { module->getMaxNumImages(); } diff --git a/xspdApp/src/ADXSPD.h b/xspdApp/src/ADXSPD.h index 8714ca0..25d94b6 100644 --- a/xspdApp/src/ADXSPD.h +++ b/xspdApp/src/ADXSPD.h @@ -186,6 +186,7 @@ class ADXSPD : ADDriver { ADXSPDLogLevel getLogLevel() { return this->logLevel; } asynStatus getInitialDetState(); + asynStatus refreshFrameSize(); asynStatus acquireStart(); asynStatus acquireStop(); diff --git a/xspdApp/src/ADXSPDModule.cpp b/xspdApp/src/ADXSPDModule.cpp index 81f8697..82397cd 100644 --- a/xspdApp/src/ADXSPDModule.cpp +++ b/xspdApp/src/ADXSPDModule.cpp @@ -50,6 +50,14 @@ int ADXSPDModule::getMaxNumImages() { return maxFrames; } +// Returns the last known max_frames without querying the API. Only changes with bit_depth, +// roi_rows, and counter_mode, all of which refresh the cached value on write. +int ADXSPDModule::getCachedMaxNumImages() { + int maxFrames; + getIntegerParam(ADXSPDModule_MaxFrames, &maxFrames); + return maxFrames; +} + void ADXSPDModule::getInitialModuleState() { this->checkStatus(); diff --git a/xspdApp/src/ADXSPDModule.h b/xspdApp/src/ADXSPDModule.h index fc70315..979574d 100644 --- a/xspdApp/src/ADXSPDModule.h +++ b/xspdApp/src/ADXSPDModule.h @@ -28,6 +28,7 @@ class ADXSPDModule : public asynPortDriver { void getInitialModuleState(); void getFlatfieldState(); int getMaxNumImages(); + int getCachedMaxNumImages(); protected: // Module parameters diff --git a/xspdApp/src/XSPDAPI.cpp b/xspdApp/src/XSPDAPI.cpp index 1877039..5ec6a0a 100644 --- a/xspdApp/src/XSPDAPI.cpp +++ b/xspdApp/src/XSPDAPI.cpp @@ -193,6 +193,14 @@ XSPD::Detector* XSPD::API::Initialize(string deviceId) { "Detector information is missing 'detector-id' or 'modules' field for device ID " + this->deviceId); + // Cache available commands on init to avoid repeated API calls during acq + json availableCommands = Get("devices/" + this->deviceId + "/commands"); + for (auto& cmd : availableCommands) { + if (cmd.contains("path")) { + this->availableCommands.push_back(cmd["path"].get()); + } + } + this->detector = make_unique(this, detectorInfo["detector-id"].get()); for (auto& moduleJson : detectorInfo["modules"]) { int numChips = moduleJson["chips"].get(); @@ -344,16 +352,8 @@ json XSPD::API::Put(string endpoint) { * @param command The command to execute */ void XSPD::API::ExecCommand(string command) { - json availableCommands = Get("devices/" + this->deviceId + "/commands"); - bool commandFound = false; - for (auto& cmd : availableCommands) { - if (cmd["path"] == command) { - commandFound = true; - break; - } - } - - if (!commandFound) + if (std::find(this->availableCommands.begin(), this->availableCommands.end(), command) == + this->availableCommands.end()) throw invalid_argument("Command '" + command + "' not found for device ID " + this->deviceId); @@ -436,24 +436,3 @@ string XSPD::Detector::GetSerialNumber() { if (sn.empty()) sn = this->GetAPI()->GetSystemId(); return sn; } - -// XSPD::CompressionSettings XSPD::Detector::GetCompressionSettings() { -// string fullCompressor = this->GetVar("compressor"); -// auto slashPos = fullCompressor.find('/'); -// Compressor baseCompressor = magic_enum::enum_cast((slashPos != string::npos) ? -// fullCompressor.substr(0, slashPos) : fullCompressor).value(); int compressionLevel = -// this->GetVar("compression_level"); if (baseCompressor == Compressor::BLOSC) { -// BloscCompressionSettings settings; -// settings.compressor = baseCompressor; -// settings.compressionLevel = compressionLevel; - -// settings.bloscCompressor = -// magic_enum::enum_cast(fullCompressor.substr(slashPos + 1)).value(); -// settings.shuffleMode = this->GetVar("shuffle_mode"); -// return settings; -// } -// CompressionSettings settings; -// settings.compressor = baseCompressor; -// settings.compressionLevel = compressionLevel; -// return settings; -// } diff --git a/xspdApp/src/XSPDAPI.h b/xspdApp/src/XSPDAPI.h index 752a67e..644c431 100644 --- a/xspdApp/src/XSPDAPI.h +++ b/xspdApp/src/XSPDAPI.h @@ -3,6 +3,7 @@ #include +#include #include #include #include @@ -87,17 +88,6 @@ enum class APIState { INITIALIZED = 4, }; -// struct CompressionSettings { -// Compressor compressor; -// int compressionLevel; -// virtual ~CompressionSettings() = default; -// }; - -// struct BloscCompressionSettings : public CompressionSettings { -// BloscCompressor bloscCompressor; -// ShuffleMode shuffleMode; -// }; - // Default port number for XSPD API constexpr int DEFAULT_PORT = 8008; @@ -123,6 +113,7 @@ class API { string GetSystemId(); bool DeviceExists(string deviceId); + bool IsCommandAvailable(string command); string GetDeviceAtIndex(int deviceIndex); virtual json SubmitRequest(string uri, RequestType reqType); @@ -241,6 +232,7 @@ class API { private: mutex apiMutex; // Mutex to protect API calls and internal state string baseUri, apiVersion, xspdVersion, libxspVersion, deviceId, systemId; + vector availableCommands; unique_ptr detector; }; @@ -440,6 +432,10 @@ class Detector : public APIComponent { DataPort* GetActiveDataPort() { return this->activeDataPort; } void ExecCommand(string command) { this->GetAPI()->ExecCommand(this->GetId() + "/" + command); } + + // Convenience methods for starting and stopping acquisition + void StartAcquisition() { this->ExecCommand("start"); } + void StopAcquisition() { this->ExecCommand("stop"); } // CompressionSettings GetCompressionSettings(); private: diff --git a/xspdApp/tests/MockXSPDAPI.cpp b/xspdApp/tests/MockXSPDAPI.cpp index c91940d..a161564 100644 --- a/xspdApp/tests/MockXSPDAPI.cpp +++ b/xspdApp/tests/MockXSPDAPI.cpp @@ -167,6 +167,7 @@ void MockXSPDAPI::MockInitializationSeq(std::string deviceId) { this->MockAPIVersionCheck(); this->MockGetRequest("devices"); this->MockGetRequest("devices/" + deviceId + "/variables?path=info"); + this->MockGetRequest("devices/" + deviceId + "/commands"); this->MockGetRequest("devices/" + deviceId); } diff --git a/xspdApp/tests/TestXSPDAPI.cpp b/xspdApp/tests/TestXSPDAPI.cpp index 1dbe876..9f33a55 100644 --- a/xspdApp/tests/TestXSPDAPI.cpp +++ b/xspdApp/tests/TestXSPDAPI.cpp @@ -183,6 +183,7 @@ TEST_F(TestXSPDAPI, TestAPIInitNoDataPorts) { this->mapi->MockAPIVersionCheck(); this->mapi->MockGetRequest("devices"); this->mapi->MockGetVarRequest("info"); + this->mapi->MockGetRequest("devices/lambda01/commands"); this->mapi->MockGetRequest("devices/lambda01", &modifiedDeviceInfo); EXPECT_THAT([&]() { this->mapi->Initialize("lambda01"); }, testing::ThrowsMessage( @@ -196,6 +197,7 @@ TEST_F(TestXSPDAPI, TestAPIInitMissingDataPortInfo) { this->mapi->MockAPIVersionCheck(); this->mapi->MockGetRequest("devices"); this->mapi->MockGetVarRequest("info"); + this->mapi->MockGetRequest("devices/lambda01/commands"); this->mapi->MockGetRequest("devices/lambda01", &modifiedDeviceInfo); EXPECT_THAT([&]() { this->mapi->Initialize("lambda01"); }, testing::ThrowsMessage(