Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ find_package(span-lite CONFIG REQUIRED)
find_package(Eigen3 REQUIRED)
find_package(boost_mp11 CONFIG REQUIRED)
find_package(nod CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)

#------------------------------------------------------------------------------
# Find the Image IO libraries
Expand Down Expand Up @@ -279,7 +278,6 @@ target_link_libraries(simplnx
HDF5::HDF5
Boost::mp11
nod::nod
spdlog::spdlog
TIFF::TIFF
)

Expand Down Expand Up @@ -539,7 +537,6 @@ set(SIMPLNX_HDRS
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/NodeRemovedMessage.hpp
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/NodeStatusMessage.hpp
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/OutputRenamedMessage.hpp
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/PipelineFilterMessage.hpp
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/PipelineNodeMessage.hpp
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/PipelineNodeObserver.hpp
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/RenamedMessage.hpp
Expand Down Expand Up @@ -567,7 +564,7 @@ set(SIMPLNX_HDRS
${SIMPLNX_SOURCE_DIR}/Utilities/HistogramUtilities.hpp
${SIMPLNX_SOURCE_DIR}/Utilities/MaskCompareUtilities.hpp
${SIMPLNX_SOURCE_DIR}/Utilities/MemoryUtilities.hpp
${SIMPLNX_SOURCE_DIR}/Utilities/MessageHelper.hpp
${SIMPLNX_SOURCE_DIR}/Utilities/ThrottledMessageHandler.hpp
${SIMPLNX_SOURCE_DIR}/Utilities/StringUtilities.hpp
${SIMPLNX_SOURCE_DIR}/Utilities/StringInterpretationUtilities.hpp
${SIMPLNX_SOURCE_DIR}/Utilities/IntersectionUtilities.hpp
Expand Down Expand Up @@ -761,7 +758,6 @@ set(SIMPLNX_SRCS
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/NodeRemovedMessage.cpp
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/NodeStatusMessage.cpp
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/OutputRenamedMessage.cpp
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/PipelineFilterMessage.cpp
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/PipelineNodeMessage.cpp
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/PipelineNodeObserver.cpp
${SIMPLNX_SOURCE_DIR}/Pipeline/Messaging/RenamedMessage.cpp
Expand All @@ -782,7 +778,7 @@ set(SIMPLNX_SRCS
${SIMPLNX_SOURCE_DIR}/Utilities/DataStoreUtilities.cpp
${SIMPLNX_SOURCE_DIR}/Utilities/MaskCompareUtilities.cpp
${SIMPLNX_SOURCE_DIR}/Utilities/MemoryUtilities.cpp
${SIMPLNX_SOURCE_DIR}/Utilities/MessageHelper.cpp
${SIMPLNX_SOURCE_DIR}/Utilities/ThrottledMessageHandler.cpp
${SIMPLNX_SOURCE_DIR}/Utilities/IParallelAlgorithm.cpp
${SIMPLNX_SOURCE_DIR}/Utilities/ParallelDataAlgorithm.cpp
${SIMPLNX_SOURCE_DIR}/Utilities/ParallelData2DAlgorithm.cpp
Expand Down
210 changes: 159 additions & 51 deletions docs/PortingFilters.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,104 +245,212 @@ dataAlg.execute(::FilterNameImpl(object, argument));

With out of core functionality on the way, it is now a requirement for each and every filter to have progress updates and checks for cancel. This section shows threadsafe progress updating and message structuring.

### Serial Messaging API
### Sending Messages

Modern API offers a `MessageHelper` wrapper class that offers an object that has assorted helper functions, such as a throttled messenger.
Every filter's algorithm class holds a `const IFilter::MessageHandler& m_MessageHandler`. Send through
it directly using the named methods, which say what kind of message you are sending:

Include it with the following:
```cpp
m_MessageHandler.sendInfoMessage("Initializing working grid");
m_MessageHandler.sendWarningMessage(fmt::format("Phase {} has an unknown crystal structure", phase));
m_MessageHandler.sendErrorMessage("Could not open file");
m_MessageHandler.sendDebugMessage("cache miss");
```

These are sent immediately and are never dropped. Use them for section headers and one-time
statements. **Do not call them from a loop** — an unthrottled message per iteration is expensive and
floods the log.

For a one-off progress report, pick the call that matches what you want the user to read:

```cpp
#include "simplnx/Utilities/MessageHelper.hpp"
// counts, when the numbers mean something to a user: "Reading slices: 3/200"
m_MessageHandler.sendProgressCount("Reading slices", slice, totalSlices);

// percentage, when the counts are too large to be readable: "Analyzing voxels: 37.81%"
m_MessageHandler.sendProgressPercent("Analyzing voxels", voxel, totalVoxels);
```

You create it in the algorithms execution body like this
Both also deliver the percentage as a separate numeric field, which is what drives the progress bar in
DREAM3D-NX and the percent column in `nxrunner`. Choosing between them records your intent for whoever
maintains the filter next, so prefer them over assembling the text yourself.

### Throttled Messaging In A Loop

`ThrottledMessageHandler` rate-limits messages so a tight loop can report without measurable cost. The
loop body only reads an atomic flag, which is roughly 60x cheaper than reading the clock on every
iteration, and the message is only formatted when one is actually due.

```cpp
MessageHelper messageHelper(m_MessageHandler);
#include "simplnx/Utilities/ThrottledMessageHandler.hpp"
```

To send a message immediately to the console, use the following syntax:
Create one in the algorithm's execution body. It emits at most one message per second by default:

```cpp
messageHelper.sendMessage("Header Here:");
ThrottledMessageHandler throttle(m_MessageHandler);
ThrottledMessageHandler throttle(m_MessageHandler, std::chrono::milliseconds(2000)); // or a custom interval
```

In practice this should be used to print a section header for updates or a one-time message. **Avoid using this as a progress messenger in a loop since it can incur a significant compute-cost penalty in the form of excessive output OR brach check cost**
Report progress with the same two intents as above, supplying the label and denominator at the call
site:

```cpp
for(usize i = 0; i < totalTuples; i++)
{
if(m_ShouldCancel)
{
return {};
}
// ... work ...
throttle.updateCount(" - Searching", i, totalTuples); // " - Searching: 3400/10000"
throttle.updatePercent(" - Searching", i, totalTuples); // " - Searching: 34.10%"
}
```

For progress messaging a throttled messenger class is provided. To initialize it do the following:
For status text that is not a progress fraction, use `queueMessage()`. The format string is checked at
compile time and the arguments are only formatted when a message is due:

```cpp
ThrottledMessenger throttledMessenger = messageHelper.createThrottledMessenger();
throttle.queueMessage(" Iteration {}: {} voxels remaining", iteration, count);
```

The throttled messenger will print every 1000 milliseconds (1 second) by default, to change this initialize it as follows:
Note that `queueMessage()` evaluates its *arguments* on every call and defers only the formatting. If
building the message is itself expensive, or has a side effect that must happen only when a message is
sent, pass a functor instead so the whole body is deferred:

```cpp
ThrottledMessenger throttledMessenger = messageHelper.createThrottledMessenger(static_cast<std::chrono::milliseconds>(number_in_milliseconds));
throttle.queueMessage([&]() {
auto now = std::chrono::steady_clock::now(); // runs only when a message is due
lastReportTime = now;
return fmt::format("Processed {} triangles, est. {} remaining", count, estimate);
});
```

Then provide the statement to be printed wrapped in a lambda as follows:
If you cannot pass an absolute position — for example you only know how many items you just finished —
set the denominator and label once, then accumulate:

```cpp
throttledMessenger.sendThrottledMessage([&]() { return fmt::format(" - Your Message || {:.2f}% Complete", CalculatePercentComplete(current_position, max_position));
throttle.reset(totalTuples, "Converting Orientations");
throttle.incrementCount(chunkSize); // or incrementPercent(chunkSize)
```

Here is a complete MRE:
Call `reset()` again between phases; it also reopens the gate so each phase reports immediately.

Here is a complete example:

```cpp
#include "simplnx/Utilities/MessageHelper.hpp"
#include "simplnx/Utilities/ThrottledMessageHandler.hpp"
...
/// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
Result<> SomeAlgorithmClass::operator()()
{
// Create wrapper messing class
MessageHelper messageHelper(m_MessageHandler);

// Send Header
messageHelper.sendMessage("Parsing Input Data:");

m_MessageHandler.sendInfoMessage("Parsing Input Data:");

ThrottledMessenger throttledMessenger = messageHelper.createThrottledMessenger(static_cast<std::chrono::milliseconds>(2000));
ThrottledMessageHandler throttle(m_MessageHandler);
for(usize i = 0; i < 10000; i++)
{
if(m_ShouldCancel)
{
return {};
}
// Do stuff
throttledMessenger.sendThrottledMessage([&]() { return fmt::format(" - Searching || {:.2f}% Complete", CalculatePercentComplete(i, 1000));
throttle.updatePercent(" - Searching", i, 10000);
}

return {};
}
```

This would produce something similar to the following output
This produces output similar to the following:

```console
Parsing Input Data:
- Searching || 1.2% Complete
- Searching || 15.7% Complete
- Searching || 34.1% Complete
- Searching: 1.20%
- Searching: 15.70%
- Searching: 34.10%
```

### ThreadSafe Progress Messaging

!!! THIS SECTION IS OUT OF DATE

This is an example that aims to reduce the number of times a mutex lock is called.

> void updateThreadSafeProgress(size_t counter)
> {
> std::lock_guard<std::mutex> guard(m_ProgressMessage_Mutex);
>
> m_ProgressCounter += counter;
>
> auto now = std::chrono::steady_clock::now();
> if(std::chrono::duration_cast<std::chrono::milliseconds>(now - m_InitialTime).count() > 1000) // every second update
> {
> size_t progressInt = static_cast<size_t>((static_cast<double>(m_ProgressCounter) / m_TotalElements) * 100.0);
> std::string progressMessage = "Calculating... ";
> m_MessageHandler(IFilter::ProgressMessage{IFilter::Message::Type::Progress, progressMessage, static_cast<int32_t>(progressInt)});
> m_InitialTime = std::chrono::steady_clock::now();
> }
> }

This function should avoid being called too many times in a thread as it would significantly slow it down.
`ThrottledMessageHandler` is **not** internally thread-safe, and it owns a thread, so do not create one
per worker. Once an algorithm starts its own parallel work, the algorithm owns a single throttle behind
a mutex and exposes a thread-safe seam. Workers call the seam and never touch an
`IFilter::MessageHandler` directly — a worker holding a message handler is a bug.

In the algorithm header:

```cpp
class SomeAlgorithm
{
public:
/**
* @brief Thread-safe progress update. Safe to call from ParallelDataAlgorithm workers.
* @param counter Items completed since the previous call
*/
void sendThreadSafeProgressMessage(usize counter);

private:
const IFilter::MessageHandler& m_MessageHandler;
mutable std::mutex m_ProgressMessage_Mutex;
ThrottledMessageHandler m_Throttle; // non-movable: initialise in the constructor
};
```

In the source, initialise the throttle in the constructor's initialiser list, set the denominator
before dispatch, and keep the seam to one line:

```cpp
SomeAlgorithm::SomeAlgorithm(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, ...)
: m_MessageHandler(mesgHandler)
, m_Throttle(mesgHandler)
{
}

void SomeAlgorithm::sendThreadSafeProgressMessage(usize counter)
{
std::lock_guard<std::mutex> guard(m_ProgressMessage_Mutex);
m_Throttle.incrementCount(counter);
}

Result<> SomeAlgorithm::operator()()
{
m_Throttle.reset(totalElements, "Converting Orientations");

ParallelDataAlgorithm dataAlg;
dataAlg.setRange(0, totalElements);
dataAlg.execute(SomeAlgorithmImpl(this, ...)); // the worker holds the algorithm pointer
return {};
}
```

The worker then reports through the algorithm:

```cpp
void operator()(const Range& range) const
{
for(usize i = range.min(); i < range.max(); i++)
{
// ... work ...
m_Filter->sendThreadSafeProgressMessage(1);
}
}
```

Do not call the seam on every iteration of a very tight loop, because it takes the mutex each time.
Pre-gate on a count so the lock is taken roughly a hundred times per worker instead:

```cpp
const usize progressIncrement = std::max(totalPoints / 100ULL, 1ULL);
if(i % progressIncrement == 0)
{
m_Filter->sendThreadSafeProgressMessage(progressIncrement);
}
```

If each worker needs its own message text — for example when every worker handles a different data
array — give the seam a `const std::string&` parameter and forward it to
`m_Throttle.trySendMessage(message)` instead of accumulating a shared counter.

### Message Structuring

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class ProgressObserver : public itk::Command
ss = fmt::format("{} : {}", m_MessagePrefix, progressStr);
}

m_MessageHandler(nx::core::IFilter::Message::Type::Info, ss);
m_MessageHandler.sendInfoMessage(ss);
m_StartTime = std::chrono::steady_clock::now();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ FijiCache& ITKImportFijiMontage::getCache()
// -----------------------------------------------------------------------------
void ITKImportFijiMontage::sendUpdate(const std::string& message)
{
m_MessageHandler({IFilter::Message::Type::Info, message});
m_MessageHandler.sendInfoMessage(message);
}

// -----------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A
return copyResult;
}
const fs::path outputFilePath = cxITKImageWriterFilter::GenerateOutputFilePath(filePath, slice + indexOffset, dims.getZ(), totalDigits, fillChar);
messageHandler(fmt::format("Writing file {} of {}: \"{}\"", slice + 1, dims.getZ(), outputFilePath.string()));
messageHandler.sendInfoMessage(fmt::format("Writing file {} of {}: \"{}\"", slice + 1, dims.getZ(), outputFilePath.string()));
Result<> result = cxITKImageWriterFilter::SaveImageData(outputFilePath, *sliceData, newImageGeom);
if(result.invalid())
{
Expand Down Expand Up @@ -433,7 +433,7 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A
return copyResult;
}
const fs::path outputFilePath = cxITKImageWriterFilter::GenerateOutputFilePath(filePath, slice + indexOffset, dims.getY(), totalDigits, fillChar);
messageHandler(fmt::format("Writing file {} of {}: \"{}\"", slice + 1, dims.getY(), outputFilePath.string()));
messageHandler.sendInfoMessage(fmt::format("Writing file {} of {}: \"{}\"", slice + 1, dims.getY(), outputFilePath.string()));
Result<> result = cxITKImageWriterFilter::SaveImageData(outputFilePath, *sliceData, newImageGeom);
if(result.invalid())
{
Expand Down Expand Up @@ -464,7 +464,7 @@ Result<> ITKImageWriterFilter::executeImpl(DataStructure& dataStructure, const A
return copyResult;
}
const fs::path outputFilePath = cxITKImageWriterFilter::GenerateOutputFilePath(filePath, slice + indexOffset, dims.getX(), totalDigits, fillChar);
messageHandler(fmt::format("Writing file {} of {}: \"{}\"", slice + 1, dims.getX(), outputFilePath.string()));
messageHandler.sendInfoMessage(fmt::format("Writing file {} of {}: \"{}\"", slice + 1, dims.getX(), outputFilePath.string()));
Result<> result = cxITKImageWriterFilter::SaveImageData(outputFilePath, *sliceData, newImageGeom);
if(result.invalid())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ Result<> ReadImageStack(DataStructure& dataStructure, const DataPath& imageGeomP
for(usize i = startSlice; i <= endSlice; i++)
{
const std::string& filePath = files[i];
messageHandler(IFilter::Message::Type::Info, fmt::format("Importing: {}", filePath));
messageHandler.sendInfoMessage(fmt::format("Importing: {}", filePath));

DataStructure importedDataStructure;
{
Expand Down
Loading
Loading