Skip to content

Commit cca336e

Browse files
Make spice kernel (un)loading faster and safe in parallel sims
1 parent 984d102 commit cca336e

File tree

3 files changed

+290
-54
lines changed

3 files changed

+290
-54
lines changed

src/simulation/environment/spiceInterface/spiceInterface.cpp

Lines changed: 132 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,49 @@
2323
#include "architecture/utilities/simDefinitions.h"
2424
#include "architecture/utilities/macroDefinitions.h"
2525
#include "architecture/utilities/rigidBodyKinematics.h"
26+
#include "spiceInterface.h"
27+
28+
namespace {
29+
/**
30+
* RAII guard for SPICE error mode.
31+
*
32+
* Sets SPICE error action to RETURN while the guard is alive so that
33+
* calls report failures via failed_c() instead of aborting. Restores
34+
* the previous error action and print settings on destruction.
35+
*/
36+
struct SpiceErrorModeGuard
37+
{
38+
char oldAction[32];
39+
char oldPrint[32];
40+
41+
SpiceErrorModeGuard()
42+
{
43+
erract_c("GET", sizeof(oldAction), oldAction);
44+
errprt_c("GET", sizeof(oldPrint), oldPrint);
45+
46+
// Only override the abort behavior
47+
erract_c("SET", 0, const_cast<char*>("RETURN"));
48+
// DO NOT suppress printing: errprt is left untouched
49+
}
50+
51+
~SpiceErrorModeGuard()
52+
{
53+
erract_c("SET", 0, oldAction);
54+
errprt_c("SET", 0, oldPrint);
55+
}
56+
};
57+
58+
/**
59+
* Normalize a file system path to a canonical absolute string.
60+
*
61+
* Used to key kernels so that one physical file maps to a single
62+
* cache entry even if referenced through different relative paths.
63+
*/
64+
std::string absolutize(const std::filesystem::path& path)
65+
{
66+
return std::filesystem::absolute(path).lexically_normal().string();
67+
}
68+
}
2669

2770
/*! This constructor initializes the variables that spice uses. Most of them are
2871
not intended to be changed, but a couple are user configurable.
@@ -74,11 +117,6 @@ SpiceInterface::~SpiceInterface()
74117
delete this->transRefStateOutMsgs.at(c);
75118
}
76119
delete [] this->spiceBuffer;
77-
// if(this->SPICELoaded)
78-
// {
79-
// this->clearKeeper();
80-
// }
81-
return;
82120
}
83121

84122
void SpiceInterface::clearKeeper()
@@ -426,63 +464,46 @@ void SpiceInterface::pullSpiceData(std::vector<SpicePlanetStateMsgPayload> *spic
426464
}
427465
}
428466

429-
/*! This method loads a requested SPICE kernel into the system memory. It is
430-
its own method because we have to load several SPICE kernels in for our
431-
application. Note that they are stored in the SPICE library and are not
432-
held locally in this object.
433-
@return int Zero for success one for failure
434-
@param kernelName The name of the kernel we are loading
435-
@param dataPath The path to the data area on the filesystem
467+
/**
468+
* Load a SPICE kernel for use by this interface.
469+
*
470+
* This function takes a kernel file name and a base directory and
471+
* ensures that the corresponding SPICE kernel is available to the
472+
* simulation. Internally the module keeps track of which kernels it
473+
* has already loaded so that the same file is not loaded multiple
474+
* times.
475+
*
476+
* @param kernelName File name of the kernel inside dataPath.
477+
* @param dataPath Directory where the kernel is located.
478+
* @return 0 on success, 1 if loading the kernel failed.
436479
*/
437480
int SpiceInterface::loadSpiceKernel(char *kernelName, const char *dataPath)
438481
{
439-
char *fileName = new char[this->charBufferSize];
440-
SpiceChar *name = new SpiceChar[this->charBufferSize];
441-
442-
//! - The required calls come from the SPICE documentation.
443-
//! - The most critical call is furnsh_c
444-
strcpy(name, "REPORT");
445-
erract_c("SET", this->charBufferSize, name);
446-
strcpy(fileName, dataPath);
447-
strcat(fileName, kernelName);
448-
furnsh_c(fileName);
449-
450-
//! - Check to see if we had trouble loading a kernel and alert user if so
451-
strcpy(name, "DEFAULT");
452-
erract_c("SET", this->charBufferSize, name);
453-
delete[] fileName;
454-
delete[] name;
455-
if(failed_c()) {
456-
return 1;
457-
}
482+
std::filesystem::path base(dataPath);
483+
std::filesystem::path fullPath = base / kernelName;
484+
auto kernel = SpiceKernel::request(fullPath.string());
485+
if (!kernel->wasLoadSuccesful()) return 1;
486+
this->loadedKernels[kernel->getPath()] = kernel;
458487
return 0;
459488
}
460489

461-
/*! This method unloads a requested SPICE kernel into the system memory. It is
462-
its own method because we have to load several SPICE kernels in for our
463-
application. Note that they are stored in the SPICE library and are not
464-
held locally in this object.
465-
@return int Zero for success one for failure
466-
@param kernelName The name of the kernel we are unloading
467-
@param dataPath The path to the data area on the filesystem
490+
/**
491+
* Tell this interface that a SPICE kernel is no longer needed.
492+
*
493+
* This function removes the kernel from the set of kernels managed
494+
* by this interface. Once no users remain, the underlying kernel is
495+
* also removed from SPICE so it no longer affects future queries.
496+
*
497+
* @param kernelName File name of the kernel inside dataPath.
498+
* @param dataPath Directory where the kernel is located.
499+
* @return always 0.
468500
*/
469501
int SpiceInterface::unloadSpiceKernel(char *kernelName, const char *dataPath)
470502
{
471-
char *fileName = new char[this->charBufferSize];
472-
SpiceChar *name = new SpiceChar[this->charBufferSize];
473-
474-
//! - The required calls come from the SPICE documentation.
475-
//! - The most critical call is furnsh_c
476-
strcpy(name, "REPORT");
477-
erract_c("SET", this->charBufferSize, name);
478-
strcpy(fileName, dataPath);
479-
strcat(fileName, kernelName);
480-
unload_c(fileName);
481-
delete[] fileName;
482-
delete[] name;
483-
if(failed_c()) {
484-
return 1;
485-
}
503+
std::filesystem::path base(dataPath);
504+
std::filesystem::path fullPath = base / kernelName;
505+
auto key = absolutize(fullPath);
506+
this->loadedKernels.erase(key);
486507
return 0;
487508
}
488509

@@ -506,3 +527,61 @@ std::string SpiceInterface::getCurrentTimeString()
506527
delete[] spiceOutputBuffer;
507528
return(returnTimeString);
508529
}
530+
531+
std::mutex SpiceKernel::mutex;
532+
std::unordered_map<std::string, std::weak_ptr<SpiceKernel>> SpiceKernel::cache;
533+
534+
std::shared_ptr<SpiceKernel>
535+
SpiceKernel::request(const std::filesystem::path& path)
536+
{
537+
const std::string key = absolutize(path);
538+
539+
std::lock_guard<std::mutex> lock(mutex);
540+
541+
auto it = cache.find(key);
542+
if (it != cache.end())
543+
{
544+
if (auto existing = it->second.lock())
545+
{
546+
// Already have a live handle to this kernel
547+
return existing;
548+
}
549+
// Weak pointer expired - fall through and create a new one
550+
}
551+
552+
// First live handle for this absolute path in this process
553+
auto handle = std::shared_ptr<SpiceKernel>(new SpiceKernel(key));
554+
555+
if (handle->loadSucceeded) cache[key] = handle;
556+
557+
return handle;
558+
}
559+
560+
SpiceKernel::~SpiceKernel() noexcept
561+
{
562+
if (!loadSucceeded) return;
563+
564+
SpiceErrorModeGuard guard;
565+
unload_c(path.c_str());
566+
if (failed_c())
567+
{
568+
reset_c(); // SPICE printed its own messages already
569+
}
570+
}
571+
572+
SpiceKernel::SpiceKernel(std::string path_)
573+
: path(std::move(path_))
574+
{
575+
SpiceErrorModeGuard guard;
576+
furnsh_c(path.c_str());
577+
578+
if (failed_c())
579+
{
580+
reset_c(); // SPICE already printed diagnostics
581+
loadSucceeded = false; // destructor will not unload
582+
}
583+
else
584+
{
585+
loadSucceeded = true;
586+
}
587+
}

src/simulation/environment/spiceInterface/spiceInterface.h

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222

2323
#include <vector>
2424
#include <map>
25+
#include <filesystem>
26+
#include <memory>
27+
#include <mutex>
28+
#include <string>
29+
#include <unordered_map>
2530
#include "architecture/_GeneralModuleFiles/sys_model.h"
2631
#include "architecture/utilities/linearAlgebra.h"
2732
#include "architecture/utilities/bskLogging.h"
@@ -35,6 +40,79 @@
3540
#include "architecture/msgPayloadDefC/TransRefMsgPayload.h"
3641
#include "architecture/messaging/messaging.h"
3742

43+
/**
44+
* Thin RAII wrapper around a single SPICE kernel.
45+
*
46+
* The class furnishes a kernel on construction and unloads it on
47+
* destruction, and provides a static request function that caches
48+
* instances by canonical absolute path so that a given kernel file is
49+
* not furnished multiple times.
50+
*/
51+
class SpiceKernel
52+
{
53+
public:
54+
/**
55+
* Request a shared handle for the kernel at the given path.
56+
*
57+
* The first call for a canonical path constructs a SpiceKernel, which
58+
* furnishes the kernel once. Later calls for the same path reuse the
59+
* existing instance as long as it is still alive.
60+
*/
61+
static std::shared_ptr<SpiceKernel> request(const std::filesystem::path& path);
62+
63+
/**
64+
* Destructor unloads the kernel from SPICE if the load succeeded.
65+
*
66+
* This runs once when the last shared_ptr owning this SpiceKernel
67+
* instance is destroyed.
68+
*/
69+
~SpiceKernel();
70+
71+
/// Canonical absolute path used as the cache key and SPICE file name.
72+
const std::string& getPath() const { return path; }
73+
74+
/// True if furnsh_c succeeded for this kernel.
75+
bool wasLoadSuccesful() const {return loadSucceeded; };
76+
77+
// avoid copy operations
78+
SpiceKernel(const SpiceKernel&) = delete;
79+
SpiceKernel& operator=(const SpiceKernel&) = delete;
80+
81+
private:
82+
/**
83+
* Construct a SpiceKernel by furnishing the given canonical path.
84+
*
85+
* The constructor switches SPICE into RETURN mode, calls furnsh_c,
86+
* checks failed_c, and records the load status. The destructor will
87+
* unload only if loadSucceeded is true.
88+
*/
89+
explicit SpiceKernel(std::string path);
90+
91+
/// Canonical absolute path used as the cache key and SPICE file name.
92+
std::string path;
93+
94+
/// True if furnsh_c succeeded for this kernel.
95+
bool loadSucceeded;
96+
97+
/**
98+
* Static mutex guarding the shared kernel cache.
99+
*
100+
* All access to SpiceKernel::cache must take this lock so that repeated
101+
* calls to request from different threads do not race.
102+
*/
103+
static std::mutex mutex;
104+
105+
/**
106+
* Global cache mapping canonical absolute paths to weak pointers.
107+
*
108+
* A non expired weak pointer means there is already a live SpiceKernel
109+
* instance owning that kernel, so request can reuse it instead of
110+
* calling furnsh_c again.
111+
*/
112+
static std::unordered_map<std::string, std::weak_ptr<SpiceKernel>> cache;
113+
};
114+
115+
38116
/*! @brief spice interface class */
39117
class SpiceInterface: public SysModel {
40118
public:
@@ -50,7 +128,17 @@ class SpiceInterface: public SysModel {
50128
void computeGPSData();
51129
void pullSpiceData(std::vector<SpicePlanetStateMsgPayload> *spiceData);
52130
void writeOutputMessages(uint64_t CurrentClock);
53-
void clearKeeper(); //!< class method
131+
132+
/** Resets all data loaded to SPICE.
133+
*
134+
* Calls `kclear_c`, which resets all loaded kernels for all simulations
135+
* in this process. Avoid using this, as it can affect other simulations
136+
* running in parallel. Kernels loaded with `loadSpiceKernel` will be
137+
* automatically cleared when all simulations that need it have closed.
138+
*
139+
* Deprecated, pending removal 11/20/2026.
140+
*/
141+
void clearKeeper();
54142
void addPlanetNames(std::vector<std::string> planetNames);
55143
void addSpacecraftNames(std::vector<std::string> spacecraftNames);
56144

@@ -90,6 +178,15 @@ class SpiceInterface: public SysModel {
90178
std::vector<SpicePlanetStateMsgPayload> planetData;
91179
std::vector<SpicePlanetStateMsgPayload> scData;
92180

181+
/**
182+
* Map of loaded kernel paths to their RAII handles.
183+
*
184+
* As long as an entry is present, the corresponding kernel remains
185+
* furnished in SPICE. Removing an entry allows the SpiceKernel
186+
* destructor to unload the kernel when all shared_ptr copies are
187+
* gone.
188+
*/
189+
std::unordered_map<std::string, std::shared_ptr<SpiceKernel>> loadedKernels;
93190
};
94191

95192

0 commit comments

Comments
 (0)