viewer: fix HTTP live-follow OOM via datasetLoaded backpressure + shared pixel mask
The viewer could grow to ~100 GB RAM when live-following an HTTP broker. The rc.153 images_in_flight backpressure only throttled imageLoaded; the heavy per-frame payload rides datasetLoaded, fanned out over ~10 queued cross-thread connections with no cap. In HTTPSyncDataset follow mode (entered when an operator clicks an image while following live) RefreshDatasetOnly_i emits a fresh full dataset every autoload tick with no imageLoaded, so the gate never engaged and the queued events - each pinning a full JFJochReaderDataset (full-detector PixelMask + per-image plots) - accumulated without bound. Backpressure datasetLoaded the same way as imageLoaded: a datasets_in_flight counter (cap 2), all emits routed through EmitDatasetLoaded_i, and AutoLoadTimerExpired gated on it (covers HTTPSyncDataset). The window routes the worker's datasetLoaded through a single OnDatasetReady sink that fans out synchronously via datasetReady and acks with datasetConsumed. Under load stale datasets are dropped; the next tick sends the latest. Share the pixel mask instead of deep-copying it: JFJochReaderDataset::pixel_mask is now shared_ptr<const PixelMask>, so per-frame dataset copies share the ~72 MB mask. UpdateUserMask does copy-on-write; JFJochHttpReader caches the mask by arm_date so a live refresh reuses one shared mask per acquisition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -293,7 +293,7 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_
|
||||
UpdateAzint_i(dataset.get());
|
||||
}
|
||||
|
||||
emit datasetLoaded(dataset);
|
||||
EmitDatasetLoaded_i(dataset);
|
||||
EmitReferenceInfo_i(); // refresh the reference's consistency check against the new dataset
|
||||
|
||||
// Reset the run collection for the freshly opened file (file mode only has snapshots).
|
||||
@@ -313,7 +313,7 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_
|
||||
emit fileLoadError("File Load Error", QString::fromStdString(e.what()));
|
||||
ResetFileOpenRetry_i();
|
||||
|
||||
emit datasetLoaded({});
|
||||
EmitDatasetLoaded_i({});
|
||||
EmitImageLoaded_i({});
|
||||
}
|
||||
}
|
||||
@@ -345,7 +345,7 @@ void JFJochImageReadingWorker::CloseFile() {
|
||||
total_images = 0;
|
||||
current_file = "";
|
||||
EmitImageLoaded_i({});
|
||||
emit datasetLoaded({});
|
||||
EmitDatasetLoaded_i({});
|
||||
}
|
||||
|
||||
void JFJochImageReadingWorker::LoadImage(int64_t image_number, int64_t summation) {
|
||||
@@ -363,9 +363,9 @@ void JFJochImageReadingWorker::LoadImage(int64_t image_number, int64_t summation
|
||||
|
||||
void JFJochImageReadingWorker::UpdateAzint_i(const JFJochReaderDataset *dataset) {
|
||||
if (dataset) {
|
||||
azint_mapping = std::make_unique<AzimuthalIntegrationMapping>(curr_experiment, dataset->pixel_mask);
|
||||
azint_mapping = std::make_unique<AzimuthalIntegrationMapping>(curr_experiment, *dataset->pixel_mask);
|
||||
index_and_refine = std::make_unique<IndexAndRefine>(curr_experiment, indexing.get());
|
||||
image_analysis = std::make_unique<MXAnalysisWithoutFPGA>(curr_experiment, *azint_mapping, dataset->pixel_mask,
|
||||
image_analysis = std::make_unique<MXAnalysisWithoutFPGA>(curr_experiment, *azint_mapping, *dataset->pixel_mask,
|
||||
*index_and_refine.get());
|
||||
|
||||
last_profile_.reset();
|
||||
@@ -391,7 +391,7 @@ void JFJochImageReadingWorker::LoadImage_i(int64_t image_number, int64_t summati
|
||||
}
|
||||
current_image_ptr = tmp_image_ptr;
|
||||
total_images = http_reader.GetNumberOfImages();
|
||||
emit datasetLoaded(http_reader.GetDataset());
|
||||
EmitDatasetLoaded_i(http_reader.GetDataset());
|
||||
} else {
|
||||
if (image_number < 0 || image_number + summation > total_images)
|
||||
return;
|
||||
@@ -483,7 +483,7 @@ void JFJochImageReadingWorker::UpdateDataset_i(const std::optional<DiffractionEx
|
||||
curr_experiment.ImportScalingSettings(scaling_settings);
|
||||
UpdateAzint_i(dataset.get());
|
||||
|
||||
emit datasetLoaded(dataset);
|
||||
EmitDatasetLoaded_i(dataset);
|
||||
|
||||
current_image_ptr = std::make_shared<JFJochReaderImage>(current_image_ptr->ImageData(), dataset);
|
||||
ApplyROIOverrideToImage_i();
|
||||
@@ -701,7 +701,7 @@ void JFJochImageReadingWorker::MaskFromSelectedROI(QString name, bool add) {
|
||||
if (!elem)
|
||||
return;
|
||||
|
||||
auto user_mask = current_image_ptr->Dataset().pixel_mask.GetUserMask();
|
||||
auto user_mask = current_image_ptr->Dataset().pixel_mask->GetUserMask();
|
||||
auto geom = exp.GetDiffractionGeometry();
|
||||
const int64_t width = exp.GetXPixelsNum();
|
||||
const int64_t height = exp.GetYPixelsNum();
|
||||
@@ -765,7 +765,7 @@ void JFJochImageReadingWorker::SetROIDefinition_i(const ROIDefinition &rois) {
|
||||
}
|
||||
}
|
||||
|
||||
emit datasetLoaded(dataset);
|
||||
EmitDatasetLoaded_i(dataset);
|
||||
EmitImageLoaded_i(current_image_ptr);
|
||||
}
|
||||
|
||||
@@ -795,7 +795,7 @@ void JFJochImageReadingWorker::UpdateUserMask_i(const std::vector<uint32_t> &mas
|
||||
|
||||
UpdateAzint_i(dataset.get());
|
||||
|
||||
emit datasetLoaded(dataset);
|
||||
EmitDatasetLoaded_i(dataset);
|
||||
|
||||
current_image_ptr = std::make_shared<JFJochReaderImage>(current_image_ptr->ImageData(), dataset);
|
||||
|
||||
@@ -817,7 +817,7 @@ void JFJochImageReadingWorker::SaveUserMaskTIFF(QString filename) {
|
||||
QMutexLocker locker(&m);
|
||||
if (!current_image_ptr)
|
||||
return;
|
||||
auto user_mask = current_image_ptr->Dataset().pixel_mask.GetUserMask();
|
||||
auto user_mask = current_image_ptr->Dataset().pixel_mask->GetUserMask();
|
||||
CompressedImage mask_image(user_mask, current_image_ptr->Dataset().experiment.GetXPixelsNum(), current_image_ptr->Dataset().experiment.GetYPixelsNum());
|
||||
WriteTIFFToFile(filename.toStdString(), mask_image);
|
||||
}
|
||||
@@ -848,7 +848,7 @@ void JFJochImageReadingWorker::LoadUserMaskTIFF(QString filename, bool replace)
|
||||
|
||||
// replace: start from an empty mask; add: keep the current one. A non-zero TIFF pixel masks.
|
||||
auto user_mask = replace ? std::vector<uint32_t>(width * height, 0)
|
||||
: current_image_ptr->Dataset().pixel_mask.GetUserMask();
|
||||
: current_image_ptr->Dataset().pixel_mask->GetUserMask();
|
||||
for (size_t i = 0; i < tiff.size(); i++)
|
||||
if (tiff[i] != 0)
|
||||
user_mask[i] = 1;
|
||||
@@ -861,7 +861,7 @@ void JFJochImageReadingWorker::UploadUserMask() {
|
||||
if (!current_image_ptr)
|
||||
return;
|
||||
|
||||
auto user_mask = current_image_ptr->Dataset().pixel_mask.GetUserMask();
|
||||
auto user_mask = current_image_ptr->Dataset().pixel_mask->GetUserMask();
|
||||
if (http_mode)
|
||||
http_reader.UploadUserMask(user_mask);
|
||||
}
|
||||
@@ -891,6 +891,10 @@ void JFJochImageReadingWorker::AutoLoadTimerExpired() {
|
||||
// skipped, which for a live monitor is exactly right (the next fetch grabs the newest image).
|
||||
if (images_in_flight >= max_images_in_flight)
|
||||
return;
|
||||
// Same gate for datasets: covers HTTPSyncDataset (which emits no image), so its per-tick
|
||||
// datasetLoaded flood is throttled to the GUI's dataset drain rate instead of piling up.
|
||||
if (datasets_in_flight >= max_datasets_in_flight)
|
||||
return;
|
||||
|
||||
switch (autoload_mode) {
|
||||
case AutoloadMode::HTTPSync:
|
||||
@@ -925,6 +929,19 @@ void JFJochImageReadingWorker::ImageConsumed() {
|
||||
--images_in_flight;
|
||||
}
|
||||
|
||||
void JFJochImageReadingWorker::EmitDatasetLoaded_i(const std::shared_ptr<const JFJochReaderDataset>& dataset) {
|
||||
// Assumes m locked! Dataset-stream analogue of EmitImageLoaded_i. Each queued datasetLoaded event
|
||||
// pins a whole dataset; DatasetConsumed() decrements once the GUI has fanned it out and rendered
|
||||
// it, and AutoLoadTimerExpired refuses to produce another live refresh while the cap is reached.
|
||||
++datasets_in_flight;
|
||||
emit datasetLoaded(dataset);
|
||||
}
|
||||
|
||||
void JFJochImageReadingWorker::DatasetConsumed() {
|
||||
QMutexLocker locker(&m);
|
||||
--datasets_in_flight;
|
||||
}
|
||||
|
||||
void JFJochImageReadingWorker::SetHttpConnected_i(bool connected, const QString &addr) {
|
||||
// Assumes m locked! Only signals on a real change so the status bar does not flicker.
|
||||
if (connected == http_connected)
|
||||
@@ -948,7 +965,7 @@ void JFJochImageReadingWorker::RefreshDatasetOnly_i() {
|
||||
emit imageNumberChanged(total_images, current_image.value());
|
||||
}
|
||||
if (dataset)
|
||||
emit datasetLoaded(dataset);
|
||||
EmitDatasetLoaded_i(dataset);
|
||||
} catch (std::exception &e) {
|
||||
logger.Debug("Dataset refresh failed: {}", e.what());
|
||||
SetHttpConnected_i(false, current_file);
|
||||
@@ -1022,7 +1039,7 @@ ReprocessingInputs JFJochImageReadingWorker::GetReprocessingInputs() const {
|
||||
return in;
|
||||
in.file = current_file;
|
||||
in.experiment = curr_experiment;
|
||||
in.pixel_mask = current_image_ptr->Dataset().pixel_mask;
|
||||
in.pixel_mask = *current_image_ptr->Dataset().pixel_mask;
|
||||
in.spot_finding = spot_finding_settings;
|
||||
if (reference_)
|
||||
in.reference_data = reference_->reflections;
|
||||
@@ -1041,7 +1058,7 @@ void JFJochImageReadingWorker::ActivateSnapshot_i(const QString &name) {
|
||||
curr_experiment.ImportIndexingSettings(indexing_settings);
|
||||
curr_experiment.ImportAzimuthalIntegrationSettings(azint_settings);
|
||||
UpdateAzint_i(dataset.get());
|
||||
emit datasetLoaded(dataset);
|
||||
EmitDatasetLoaded_i(dataset);
|
||||
|
||||
QStringList names;
|
||||
for (const auto &n: file_reader.SnapshotNames())
|
||||
|
||||
Reference in New Issue
Block a user