Skip to content

Commit 6457ed3

Browse files
committed
DOC: Add thorough Doxygen and inline documentation for OOC architecture
Add comprehensive documentation to all new methods, type aliases, classes, and algorithms introduced in the OOC architecture rewrite. Every new public API now has Doxygen explaining what it does, how it works, and why it is needed. Algorithm implementations have step-by- step inline comments explaining the logic. Signed-off-by: Joey Kleingers <joey.kleingers@bluequartz.net>
1 parent 1e9efff commit 6457ed3

25 files changed

Lines changed: 1324 additions & 305 deletions

src/Plugins/SimplnxCore/src/SimplnxCore/utils/VtkUtilities.hpp

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ std::string TypeForPrimitive(const IFilter::MessageHandler& messageHandler)
134134
return "";
135135
}
136136

137+
// -----------------------------------------------------------------------------
138+
// Functor for writing a DataArray to a VTK legacy file via FILE* I/O.
139+
// Supports both binary (big-endian) and ASCII output modes.
137140
// -----------------------------------------------------------------------------
138141
struct WriteVtkDataArrayFunctor
139142
{
@@ -161,15 +164,31 @@ struct WriteVtkDataArrayFunctor
161164
fprintf(outputFile, "LOOKUP_TABLE default\n");
162165
if(binary)
163166
{
164-
// Read data into buffer, byte-swap in memory, and write.
165-
// This avoids modifying the DataStore (critical for OOC stores
166-
// where per-element byte-swap via setValue would be very slow).
167+
// ---------------------------------------------------------------
168+
// Chunked binary write pattern for OOC compatibility
169+
// ---------------------------------------------------------------
170+
// The original code used dataArray->data() for a single fwrite,
171+
// which requires the entire array to be resident in memory. This
172+
// fails for OOC stores where data lives on disk and data() is
173+
// not available.
174+
//
175+
// Instead, we read 4096 elements at a time into a local buffer
176+
// via copyIntoBuffer (the OOC-compatible bulk read API), perform
177+
// an in-place byte swap to big-endian (VTK legacy binary format
178+
// requires big-endian), and fwrite the buffer. This keeps memory
179+
// usage constant regardless of array size.
180+
//
181+
// For bool arrays, copyIntoBuffer is not available (bool is not
182+
// a supported span type), so we use per-element getValue() and
183+
// convert to uint8 (0 or 1).
184+
// ---------------------------------------------------------------
167185
constexpr usize k_ChunkSize = 4096;
168186
for(usize offset = 0; offset < totalElements; offset += k_ChunkSize)
169187
{
170188
usize count = std::min(k_ChunkSize, totalElements - offset);
171189
if constexpr(std::is_same_v<T, bool>)
172190
{
191+
// Bool special case: convert to uint8 via per-element access.
173192
std::vector<uint8> buf(count);
174193
for(usize i = 0; i < count; i++)
175194
{
@@ -179,10 +198,14 @@ struct WriteVtkDataArrayFunctor
179198
}
180199
else
181200
{
201+
// General case: bulk read into buffer, byte-swap, then write.
182202
std::vector<T> buf(count);
183203
dataStore.copyIntoBuffer(offset, nonstd::span<T>(buf.data(), count));
184204
if constexpr(endian::little == endian::native)
185205
{
206+
// VTK legacy binary requires big-endian. Swap in the local
207+
// buffer rather than mutating the DataStore, which would be
208+
// slow for OOC stores and would modify shared data.
186209
for(usize i = 0; i < count; i++)
187210
{
188211
buf[i] = nx::core::byteswap(buf[i]);

src/simplnx/Core/Preferences.cpp

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,17 @@ void Preferences::setLargeDataFormat(std::string dataFormat)
120120
if(dataFormat.empty())
121121
{
122122
// Remove the key so the default (set by plugins) can take effect.
123-
// An empty string means "not configured", not "in-core" — use
124-
// k_InMemoryFormat for an explicit in-core choice.
123+
// An empty string means "not configured", not "in-core". To explicitly
124+
// request in-core storage, pass k_InMemoryFormat instead. This distinction
125+
// matters because the SimplnxOoc plugin sets a default OOC format on startup,
126+
// and erasing the user value lets that default take effect.
125127
m_Values.erase(k_PreferredLargeDataFormat_Key);
126128
}
127129
else
128130
{
129131
m_Values[k_PreferredLargeDataFormat_Key] = dataFormat;
130132
}
133+
// Recompute the cached m_UseOoc flag after any format change
131134
checkUseOoc();
132135
}
133136

@@ -271,9 +274,14 @@ Result<> Preferences::loadFromFile(const std::filesystem::path& filepath)
271274

272275
m_Values = parsedResult;
273276

274-
// Migrate legacy format strings from saved preferences:
275-
// - Empty string: legacy "not configured" state → remove so plugin defaults take effect
276-
// - "In-Memory": legacy explicit in-core choice → remove so plugin defaults take effect
277+
// Migrate legacy format strings from saved preferences files that were
278+
// written before the OOC architecture was finalized. Two legacy values
279+
// need cleanup:
280+
// - Empty string (""): Old "not configured" state. Removing the key
281+
// lets the plugin-supplied default (e.g., "HDF5-OOC") take effect.
282+
// - "In-Memory": Old explicit in-core sentinel. Replaced by
283+
// k_InMemoryFormat ("Simplnx-Default-In-Memory"). Removing the key
284+
// avoids confusion with the new sentinel value.
277285
if(m_Values.contains(k_PreferredLargeDataFormat_Key) && m_Values[k_PreferredLargeDataFormat_Key].is_string())
278286
{
279287
const std::string savedFormat = m_Values[k_PreferredLargeDataFormat_Key].get<std::string>();
@@ -283,19 +291,27 @@ Result<> Preferences::loadFromFile(const std::filesystem::path& filepath)
283291
}
284292
}
285293

294+
// Recompute derived state from the loaded (and possibly migrated) values
286295
checkUseOoc();
287296
updateMemoryDefaults();
288297
return {};
289298
}
290299

291300
void Preferences::checkUseOoc()
292301
{
302+
// Resolve the format from user values first, then default values (via value())
293303
auto formatJson = value(k_PreferredLargeDataFormat_Key);
304+
305+
// If no format is configured (null/non-string), OOC is not active
294306
if(formatJson.is_null() || !formatJson.is_string())
295307
{
296308
m_UseOoc = false;
297309
return;
298310
}
311+
312+
// OOC is active when the format is a non-empty string that is NOT the
313+
// explicit in-memory sentinel. This means a plugin (e.g., SimplnxOoc)
314+
// has registered a real OOC format like "HDF5-OOC".
299315
const std::string format = formatJson.get<std::string>();
300316
m_UseOoc = !format.empty() && format != k_InMemoryFormat;
301317
}
@@ -325,9 +341,15 @@ void Preferences::setForceOocData(bool forceOoc)
325341

326342
void Preferences::updateMemoryDefaults()
327343
{
344+
// Reserve headroom equal to 2x the single-array large-data threshold.
345+
// This leaves room for the OS, the application, and at least one large
346+
// array being constructed while the DataStructure holds existing data.
328347
const uint64 minimumRemaining = 2 * defaultValueAs<uint64>(k_LargeDataSize_Key);
329348
const uint64 totalMemory = Memory::GetTotalMemory();
330349
uint64 targetValue = totalMemory - minimumRemaining;
350+
351+
// On low-memory systems where the reservation exceeds total RAM,
352+
// fall back to using half of total RAM as the threshold
331353
if(minimumRemaining >= totalMemory)
332354
{
333355
targetValue = totalMemory / 2;
@@ -368,8 +390,12 @@ void Preferences::setOocRangeScanTimeoutSeconds(uint32 seconds)
368390

369391
uint64 Preferences::oocMemoryBudgetBytes() const
370392
{
371-
// Default: 8 GB (the application will set the real default from
372-
// OocMemoryBudgetManager::defaultBudgetBytes() on startup)
393+
// Hard-coded fallback of 8 GB. This conservative default is used when:
394+
// 1. The user has never saved an explicit budget preference, AND
395+
// 2. The SimplnxOoc plugin has not yet called setOocMemoryBudgetBytes()
396+
// with its computed default (50% of system RAM).
397+
// Using m_Values.value() (not the value() member) reads directly from
398+
// user-set values with the fallback, bypassing the default-value layer.
373399
static constexpr uint64 k_DefaultBudget = 8ULL * 1024 * 1024 * 1024;
374400
return m_Values.value(k_OocMemoryBudgetBytes_Key, k_DefaultBudget);
375401
}

src/simplnx/Core/Preferences.hpp

Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,49 @@ class SIMPLNX_EXPORT Preferences
2424
friend class AbstractPlugin;
2525

2626
public:
27-
static inline constexpr StringLiteral k_LargeDataSize_Key = "large_data_size"; // bytes
28-
static inline constexpr StringLiteral k_PreferredLargeDataFormat_Key = "large_data_format"; // string
29-
static inline constexpr StringLiteral k_InMemoryFormat = "Simplnx-Default-In-Memory"; // explicit in-core; empty string means "not configured"
30-
static inline constexpr StringLiteral k_LargeDataStructureSize_Key = "large_datastructure_size"; // bytes
31-
static inline constexpr StringLiteral k_ForceOocData_Key = "force_ooc_data"; // boolean
32-
static inline constexpr nx::core::StringLiteral k_OoCTempDirectory_ID = "ooc_temp_directory"; // Out-of-Core temp directory
33-
static inline constexpr StringLiteral k_OocRangeScanTimeoutSeconds_Key = "ooc_range_scan_timeout_seconds"; // uint32, seconds
34-
static inline constexpr StringLiteral k_OocMemoryBudgetBytes_Key = "ooc_memory_budget_bytes"; // uint64, bytes
27+
/// @name Preference Keys
28+
/// JSON keys used to store and retrieve preference values. These keys appear
29+
/// in the serialized preferences.json file and are used internally by the
30+
/// getter/setter methods below.
31+
/// @{
32+
33+
/// Byte-size threshold above which a single DataArray is considered "large"
34+
/// and may be written to an OOC-capable format instead of in-memory storage.
35+
static inline constexpr StringLiteral k_LargeDataSize_Key = "large_data_size";
36+
37+
/// Name of the preferred storage format for large DataArrays (e.g., "HDF5-OOC").
38+
/// An empty string means "not yet configured by the user or plugin".
39+
static inline constexpr StringLiteral k_PreferredLargeDataFormat_Key = "large_data_format";
40+
41+
/// Sentinel value for k_PreferredLargeDataFormat_Key that explicitly requests
42+
/// in-memory storage. This is distinct from an empty string, which means
43+
/// "not configured" and falls back to plugin-supplied defaults.
44+
static inline constexpr StringLiteral k_InMemoryFormat = "Simplnx-Default-In-Memory";
45+
46+
/// Byte-size threshold for the entire DataStructure. When total memory usage
47+
/// approaches this value, the application may switch to OOC storage for new arrays.
48+
/// The default is computed dynamically by updateMemoryDefaults() based on system RAM.
49+
static inline constexpr StringLiteral k_LargeDataStructureSize_Key = "large_datastructure_size";
50+
51+
/// Boolean flag that, when true, forces all new DataArrays to use OOC storage
52+
/// regardless of their size. Only takes effect when an OOC format is active.
53+
static inline constexpr StringLiteral k_ForceOocData_Key = "force_ooc_data";
54+
55+
/// Filesystem path to the directory where OOC temporary files (chunk stores,
56+
/// backing HDF5 files) are created during filter execution.
57+
static inline constexpr nx::core::StringLiteral k_OoCTempDirectory_ID = "ooc_temp_directory";
58+
59+
/// Timeout in seconds for the background thread that scans OOC DataArrays to
60+
/// compute their value ranges (min/max). If the scan does not complete within
61+
/// this window, the range is reported as unknown. Default: 30 seconds.
62+
static inline constexpr StringLiteral k_OocRangeScanTimeoutSeconds_Key = "ooc_range_scan_timeout_seconds";
63+
64+
/// Total memory budget in bytes shared across all OOC caching subsystems
65+
/// (chunk cache, stride cache, partition cache). The OOC memory budget manager
66+
/// distributes this budget via global LRU eviction. Default: 8 GB.
67+
static inline constexpr StringLiteral k_OocMemoryBudgetBytes_Key = "ooc_memory_budget_bytes";
68+
69+
/// @}
3570

3671
/**
3772
* @brief Returns the default file path for storing preferences based on the application name.
@@ -230,7 +265,15 @@ class SIMPLNX_EXPORT Preferences
230265
void setForceOocData(bool forceOoc);
231266

232267
/**
233-
* @brief Updates memory-related default values based on system capabilities.
268+
* @brief Recomputes the default value for k_LargeDataStructureSize_Key based
269+
* on the current system's total physical RAM.
270+
*
271+
* The target value is (totalRAM - 2 * k_LargeDataSize), which reserves
272+
* headroom for the OS and the application itself. If the reservation would
273+
* exceed total RAM (e.g., on a low-memory system), the fallback is totalRAM / 2.
274+
*
275+
* Called automatically during construction, after loadFromFile(), and after
276+
* clear(). Can also be called explicitly after changing k_LargeDataSize_Key.
234277
*/
235278
void updateMemoryDefaults();
236279

@@ -253,32 +296,54 @@ class SIMPLNX_EXPORT Preferences
253296
void setOocTempDirectory(const std::string& path);
254297

255298
/**
256-
* @brief Gets the timeout (in seconds) for the background OOC range scan.
299+
* @brief Gets the timeout for the background OOC range scan.
300+
*
301+
* The range scan runs on a background thread after an OOC DataArray is loaded,
302+
* computing min/max values by reading through all chunks sequentially. If the
303+
* scan does not complete within this timeout, the range is reported as unknown
304+
* and the UI shows "N/A" for the array's value range.
305+
*
257306
* @return Timeout in seconds (default 30)
258307
*/
259308
uint32 oocRangeScanTimeoutSeconds() const;
260309

261310
/**
262-
* @brief Sets the timeout (in seconds) for the background OOC range scan.
263-
* @param seconds Timeout value in seconds
311+
* @brief Sets the timeout for the background OOC range scan.
312+
* @param seconds Timeout value in seconds. A value of 0 effectively disables
313+
* the range scan by expiring it immediately.
264314
*/
265315
void setOocRangeScanTimeoutSeconds(uint32 seconds);
266316

267317
/**
268-
* @brief Gets the total memory budget for all OOC caching (chunk cache, stride cache, partition cache).
318+
* @brief Gets the total memory budget for all OOC caching subsystems.
319+
*
320+
* The OOC memory budget manager distributes this budget across the chunk
321+
* cache, stride cache, and partition cache using global LRU eviction. When
322+
* the combined memory usage of all caches exceeds this budget, the least
323+
* recently used entries are evicted to make room for new data.
269324
*
270-
* The budget manager distributes this across subsystems via global LRU eviction.
271-
* The default (8 GB) is a safe fallback; the SimplnxOoc plugin overrides this on
272-
* startup with OocMemoryBudgetManager::defaultBudgetBytes() (50% of system RAM)
273-
* if the user has not saved an explicit preference.
325+
* The default value (8 GB) is a conservative fallback used when no plugin
326+
* has configured a more appropriate value. On startup, the SimplnxOoc plugin
327+
* calls OocMemoryBudgetManager::defaultBudgetBytes() (50% of system RAM) and
328+
* sets that as the budget, unless the user has already saved an explicit
329+
* preference via the UI.
274330
*
275-
* @return Budget in bytes (default 8 GB)
331+
* @note This reads from m_Values (user-set) directly, NOT from m_DefaultValues,
332+
* because the default is hard-coded as a compile-time constant.
333+
*
334+
* @return Budget in bytes (default 8 GB if not explicitly set)
276335
*/
277336
uint64 oocMemoryBudgetBytes() const;
278337

279338
/**
280-
* @brief Sets the total memory budget for all OOC caching.
281-
* @param bytes Budget in bytes
339+
* @brief Sets the total memory budget for all OOC caching subsystems.
340+
*
341+
* The new budget takes effect immediately for subsequent cache eviction
342+
* decisions. Existing cached data that exceeds the new budget will be
343+
* evicted lazily as new cache entries are requested.
344+
*
345+
* @param bytes Budget in bytes. Must be > 0; passing 0 would effectively
346+
* disable caching.
282347
*/
283348
void setOocMemoryBudgetBytes(uint64 bytes);
284349

@@ -297,7 +362,13 @@ class SIMPLNX_EXPORT Preferences
297362
void addDefaultValues(std::string pluginName, std::string valueName, const nlohmann::json& value);
298363

299364
/**
300-
* @brief Checks and updates whether out-of-core mode should be used based on current settings.
365+
* @brief Recomputes the cached m_UseOoc flag based on the current value of
366+
* k_PreferredLargeDataFormat_Key.
367+
*
368+
* OOC mode is considered active when the resolved format string is non-empty
369+
* and is not the sentinel value k_InMemoryFormat. This method is called after
370+
* any operation that could change the format: construction, loadFromFile(),
371+
* setLargeDataFormat(), and setDefaultLargeDataFormat().
301372
*/
302373
void checkUseOoc();
303374

src/simplnx/DataStructure/AbstractDataStore.hpp

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -412,22 +412,59 @@ class AbstractDataStore : public IDataStore
412412

413413
/**
414414
* @brief Copies a contiguous range of values from this data store into the
415-
* provided buffer. The buffer must be large enough to hold the requested
416-
* range. Each store subclass implements its own optimal version: in-memory
417-
* stores use std::copy; out-of-core stores use bulk chunk I/O.
415+
* provided caller-owned buffer.
416+
*
417+
* This is the primary bulk-read API for algorithms that need to process data
418+
* in contiguous blocks. It replaces the earlier chunk-based API and provides
419+
* a single uniform interface that works identically for both in-memory and
420+
* out-of-core (OOC) data stores:
421+
*
422+
* - **In-memory (DataStore):** Performs a direct std::copy from the backing
423+
* array into the buffer. This is essentially zero-overhead.
424+
* - **Out-of-core (OOC stores):** The OOC subclass translates the flat
425+
* element range into the appropriate chunk reads from the backing HDF5
426+
* file, coalescing I/O where possible. The caller does not need to know
427+
* the chunk layout.
428+
* - **Empty (EmptyDataStore):** Throws std::runtime_error because no data
429+
* exists.
430+
*
431+
* The number of elements to copy is determined by `buffer.size()`. The caller
432+
* is responsible for ensuring the buffer is large enough and that the range
433+
* `[startIndex, startIndex + buffer.size())` does not exceed `getSize()`.
418434
*
419435
* @param startIndex The starting flat element index to read from
420-
* @param buffer A span to receive the copied values (buffer.size() determines count)
436+
* @param buffer A span to receive the copied values; its size determines how
437+
* many elements are read
438+
* @throw std::out_of_range If the requested range exceeds the store's size
439+
* @throw std::runtime_error If called on an EmptyDataStore
421440
*/
422441
virtual void copyIntoBuffer(usize startIndex, nonstd::span<T> buffer) const = 0;
423442

424443
/**
425-
* @brief Copies values from the provided buffer into a contiguous range of
426-
* this data store. Each store subclass implements its own optimal version:
427-
* in-memory stores use std::copy; out-of-core stores use bulk chunk I/O.
444+
* @brief Copies values from the provided caller-owned buffer into a
445+
* contiguous range of this data store.
446+
*
447+
* This is the primary bulk-write API, the write-side counterpart of
448+
* copyIntoBuffer(). It provides a single uniform interface for both
449+
* in-memory and out-of-core (OOC) data stores:
450+
*
451+
* - **In-memory (DataStore):** Performs a direct std::copy from the buffer
452+
* into the backing array.
453+
* - **Out-of-core (OOC stores):** The OOC subclass translates the flat
454+
* element range into the appropriate chunk writes to the backing HDF5
455+
* file.
456+
* - **Empty (EmptyDataStore):** Throws std::runtime_error because no data
457+
* exists.
458+
*
459+
* The number of elements to copy is determined by `buffer.size()`. The caller
460+
* is responsible for ensuring the range `[startIndex, startIndex + buffer.size())`
461+
* does not exceed `getSize()`.
428462
*
429463
* @param startIndex The starting flat element index to write to
430-
* @param buffer A span containing the values to copy into the store
464+
* @param buffer A span containing the values to copy into the store; its
465+
* size determines how many elements are written
466+
* @throw std::out_of_range If the requested range exceeds the store's size
467+
* @throw std::runtime_error If called on an EmptyDataStore
431468
*/
432469
virtual void copyFromBuffer(usize startIndex, nonstd::span<const T> buffer) = 0;
433470

0 commit comments

Comments
 (0)