Refuse loads and renders that would not fit in memory
Opening a project of ninety 17 MP photos got Effector killed by the kernel's OOM killer: 4.6 GB of decoded RGB on a machine with 3.6 GB free, and no core dump to show for it. Bulk allocations are now sized first and refused with an OutOfMemoryError past three quarters of the free memory (MemAvailable, or less under a cgroup memory.max). loadImages sizes the batch from the file headers and refuses it before decoding anything, and holds the decodes in flight to what is left; renderChain and renderFrames refuse a canvas at 24 bytes a pixel, plus any picture a Path param loads. The interface keeps a refused batch's paths, so saving the project never drops them. Version 0.6.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,8 @@
|
||||
#include <vector>
|
||||
|
||||
#include "cli/options.hpp"
|
||||
#include "core/image.hpp"
|
||||
#include "core/memory.hpp"
|
||||
#include "core/project.hpp"
|
||||
#include "core/registry.hpp"
|
||||
#include "core/version.hpp"
|
||||
@@ -514,6 +516,45 @@ void testVideo(Gtk::Window& window, const std::filesystem::path& dir) {
|
||||
stack->set_visible_child("chain");
|
||||
}
|
||||
|
||||
// A project whose images will not fit is refused before any is decoded, and
|
||||
// saving it then must not quietly drop them from the file.
|
||||
void testMemoryGuard(Gtk::Window& window, const std::filesystem::path& dir) {
|
||||
std::cout << "memory guard\n";
|
||||
std::filesystem::create_directories(dir);
|
||||
effector::Project project;
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const auto source = dir / ("big" + std::to_string(i) + ".png");
|
||||
effector::Image(600, 600).save(source.string());
|
||||
project.sources.push_back(source.string());
|
||||
}
|
||||
project.stages.push_back({effector::effectByName("scanlines"), {}});
|
||||
const auto path = dir / "big.json";
|
||||
effector::saveProject(path.string(), project);
|
||||
|
||||
effector::limitAvailableMemory(std::size_t{1} << 20); // 2.1 MB of images, 1 MB free
|
||||
window.activate_action("win.open-recent",
|
||||
Glib::Variant<Glib::ustring>::create(Glib::filename_to_uri(path.string())));
|
||||
pump(500);
|
||||
auto* sources = named<Gtk::Widget>(window, "sources");
|
||||
auto lists = sources != nullptr ? all<Gtk::ListBox>(*sources) : std::vector<Gtk::ListBox*>{};
|
||||
check(!lists.empty() && rowCount(*lists.front()) == 0, "images too large for the memory are not loaded");
|
||||
bool told = false;
|
||||
for (auto* label : all<Gtk::Label>(window)) {
|
||||
const std::string text = label->get_text();
|
||||
if (label->is_drawable() && text.find("Loading these 2 images needs about") != std::string::npos &&
|
||||
text.find("stay in the project") != std::string::npos) {
|
||||
told = true;
|
||||
}
|
||||
}
|
||||
check(told, "and the window says why");
|
||||
|
||||
window.activate_action("win.save-project");
|
||||
pump(100);
|
||||
check(effector::loadProject(path.string()).sources.size() == 2,
|
||||
"saving the project keeps the images it could not load");
|
||||
effector::limitAvailableMemory(0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -542,6 +583,7 @@ int main(int argc, char** argv) {
|
||||
testHelpWindows(*window);
|
||||
testRecentProjects(*window, dataDir / "projects");
|
||||
testVideo(*window, dataDir / "video");
|
||||
testMemoryGuard(*window, dataDir / "memory");
|
||||
app->remove_window(*window);
|
||||
});
|
||||
const int status = app->run(argc > 0 ? 1 : 0, argv);
|
||||
|
||||
Reference in New Issue
Block a user