mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +00:00
feat: faster startup by lazy-loading main window panels on idle or first use (#15811)
This commit is contained in:
@@ -5,6 +5,9 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_creality_cfs_match.cpp
|
||||
test_dev_mapping.cpp
|
||||
test_filament_bitmap_utils.cpp
|
||||
test_lazy.cpp
|
||||
test_prebuild_queue.cpp
|
||||
test_staged_build.cpp
|
||||
test_network_versions.cpp
|
||||
test_action_source.cpp
|
||||
test_plugin_host_api.cpp
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#include "slic3r/GUI/Lazy.hpp"
|
||||
|
||||
using Slic3r::GUI::Lazy;
|
||||
using Slic3r::GUI::LazyBase;
|
||||
using Slic3r::GUI::LazyInstance;
|
||||
using Slic3r::GUI::StagedBuild;
|
||||
|
||||
namespace {
|
||||
|
||||
struct Plain
|
||||
{
|
||||
int value{ 1 };
|
||||
};
|
||||
|
||||
struct One : LazyInstance<One>
|
||||
{
|
||||
int value{ 2 };
|
||||
};
|
||||
|
||||
// Two steps after the constructor.
|
||||
struct Staged : StagedBuild, LazyInstance<Staged>
|
||||
{
|
||||
std::vector<int> ran;
|
||||
Staged()
|
||||
{
|
||||
add_build_step([this] { ran.push_back(1); });
|
||||
add_build_step([this] { ran.push_back(2); });
|
||||
}
|
||||
void add_step(std::function<void()> step) { add_build_step(std::move(step)); }
|
||||
};
|
||||
|
||||
// Owns what the factories make, since a Lazy does not.
|
||||
template <class T>
|
||||
struct Made
|
||||
{
|
||||
std::vector<std::unique_ptr<T>> objects;
|
||||
T* make()
|
||||
{
|
||||
objects.push_back(std::make_unique<T>());
|
||||
return objects.back().get();
|
||||
}
|
||||
typename Lazy<T>::Factory factory()
|
||||
{
|
||||
return [this] { return make(); };
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("The factory runs on the first unit, not at construction", "[Lazy]")
|
||||
{
|
||||
Made<Plain> made;
|
||||
Lazy<Plain> lazy("plain", 0, made.factory());
|
||||
REQUIRE(made.objects.empty());
|
||||
REQUIRE_FALSE(lazy.built());
|
||||
REQUIRE(lazy.pending());
|
||||
REQUIRE(lazy.get() == nullptr);
|
||||
|
||||
REQUIRE_FALSE(lazy.build_step()); // the only unit
|
||||
REQUIRE(made.objects.size() == 1);
|
||||
REQUIRE(lazy.built());
|
||||
REQUIRE_FALSE(lazy.pending());
|
||||
REQUIRE(lazy.get() == made.objects[0].get());
|
||||
REQUIRE_FALSE(lazy.build_step());
|
||||
REQUIRE(made.objects.size() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("A staged type takes one unit for the constructor and one per step", "[Lazy]")
|
||||
{
|
||||
Made<Staged> made;
|
||||
Lazy<Staged> lazy("staged", 0, made.factory());
|
||||
REQUIRE(lazy.build_step());
|
||||
REQUIRE(made.objects.size() == 1);
|
||||
REQUIRE(lazy.get() == nullptr); // exists but incomplete
|
||||
REQUIRE(lazy.build_step());
|
||||
REQUIRE(made.objects[0]->ran == std::vector<int>{1});
|
||||
REQUIRE_FALSE(lazy.build_step());
|
||||
REQUIRE(made.objects[0]->ran == std::vector<int>{1, 2});
|
||||
REQUIRE(lazy.get() == made.objects[0].get());
|
||||
}
|
||||
|
||||
TEST_CASE("ensure builds whatever is left and is a no-op afterwards", "[Lazy]")
|
||||
{
|
||||
Made<Staged> made;
|
||||
Lazy<Staged> lazy("staged", 0, made.factory());
|
||||
lazy.build_step();
|
||||
Staged* s = lazy.ensure();
|
||||
REQUIRE(s == made.objects[0].get());
|
||||
REQUIRE(s->ran == std::vector<int>{1, 2});
|
||||
REQUIRE(lazy.ensure() == s);
|
||||
REQUIRE(made.objects.size() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("when_built waits for completion, then runs at once", "[Lazy]")
|
||||
{
|
||||
Made<Staged> made;
|
||||
Lazy<Staged> lazy("staged", 0, made.factory());
|
||||
std::vector<int> seen;
|
||||
lazy.when_built([&](Staged& s) { seen.push_back(int(s.ran.size())); });
|
||||
lazy.build_step();
|
||||
lazy.build_step();
|
||||
REQUIRE(seen.empty());
|
||||
lazy.build_step();
|
||||
REQUIRE(seen == std::vector<int>{2});
|
||||
lazy.when_built([&](Staged&) { seen.push_back(9); });
|
||||
REQUIRE(seen == std::vector<int>{2, 9});
|
||||
}
|
||||
|
||||
TEST_CASE("A LazyInstance type reaches its holder through the statics", "[Lazy]")
|
||||
{
|
||||
REQUIRE(One::if_built() == nullptr);
|
||||
REQUIRE(One::ensure() == nullptr);
|
||||
Made<One> made;
|
||||
{
|
||||
Lazy<One> lazy("one", 0, made.factory());
|
||||
REQUIRE(One::if_built() == nullptr);
|
||||
One* one = One::ensure();
|
||||
REQUIRE(one == made.objects[0].get());
|
||||
REQUIRE(One::if_built() == one);
|
||||
int seen = 0;
|
||||
One::when_built([&](One& o) { seen = o.value; });
|
||||
REQUIRE(seen == 2);
|
||||
}
|
||||
REQUIRE(One::if_built() == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("A newer holder replaces the registration; the older one leaves it alone", "[Lazy]")
|
||||
{
|
||||
Made<One> made;
|
||||
auto first = std::make_unique<Lazy<One>>("first", 0, made.factory());
|
||||
first->ensure();
|
||||
Lazy<One> second("second", 0, made.factory());
|
||||
REQUIRE(One::if_built() == nullptr); // the new holder has not built yet
|
||||
second.ensure();
|
||||
REQUIRE(One::if_built() == made.objects[1].get());
|
||||
first.reset();
|
||||
REQUIRE(One::if_built() == made.objects[1].get());
|
||||
}
|
||||
|
||||
TEST_CASE("The holder reports the name and order it was given", "[Lazy]")
|
||||
{
|
||||
Made<Plain> made;
|
||||
Lazy<Plain> lazy("plain", 7, made.factory());
|
||||
LazyBase& base = lazy;
|
||||
REQUIRE(base.name() == "plain");
|
||||
REQUIRE(base.prebuild_order() == 7);
|
||||
}
|
||||
|
||||
TEST_CASE("A unit that re-enters the holder builds nothing twice", "[Lazy]")
|
||||
{
|
||||
Made<Plain> made;
|
||||
Lazy<Plain>* self = nullptr;
|
||||
int nested_units = 0;
|
||||
Lazy<Plain> lazy("plain", 0, [&] {
|
||||
if (self->build_step()) // as if the constructor pumped the event loop into a slice
|
||||
++nested_units;
|
||||
return made.make();
|
||||
});
|
||||
self = &lazy;
|
||||
REQUIRE_FALSE(lazy.build_step());
|
||||
REQUIRE(nested_units == 0);
|
||||
REQUIRE(made.objects.size() == 1);
|
||||
REQUIRE(lazy.built());
|
||||
}
|
||||
|
||||
TEST_CASE("A factory that returns null leaves the holder unbuilt and not pending", "[Lazy]")
|
||||
{
|
||||
int calls = 0;
|
||||
Lazy<Plain> lazy("plain", 0, [&] { ++calls; return static_cast<Plain*>(nullptr); });
|
||||
REQUIRE_FALSE(lazy.build_step());
|
||||
REQUIRE_FALSE(lazy.built());
|
||||
REQUIRE_FALSE(lazy.pending());
|
||||
REQUIRE(lazy.get() == nullptr);
|
||||
REQUIRE_FALSE(lazy.build_step()); // not retried
|
||||
REQUIRE(calls == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("ensure returns null for a factory that returned null", "[Lazy]")
|
||||
{
|
||||
Lazy<Plain> lazy("plain", 0, [] { return static_cast<Plain*>(nullptr); });
|
||||
REQUIRE(lazy.ensure() == nullptr);
|
||||
REQUIRE_FALSE(lazy.built());
|
||||
}
|
||||
|
||||
TEST_CASE("A nested ensure inside the factory returns null", "[Lazy]")
|
||||
{
|
||||
Made<Plain> made;
|
||||
Lazy<Plain>* self = nullptr;
|
||||
Plain* nested = reinterpret_cast<Plain*>(1);
|
||||
Lazy<Plain> lazy("plain", 0, [&] {
|
||||
nested = self->ensure(); // as if the constructor pumped the event loop into a caller
|
||||
return made.make();
|
||||
});
|
||||
self = &lazy;
|
||||
Plain* built = lazy.ensure();
|
||||
REQUIRE(built == made.objects[0].get());
|
||||
REQUIRE(nested == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("A nested ensure during a staged step returns null", "[Lazy]")
|
||||
{
|
||||
Made<Staged> made;
|
||||
Lazy<Staged>* self = nullptr;
|
||||
Staged* nested = reinterpret_cast<Staged*>(1);
|
||||
Lazy<Staged> lazy("staged", 0, [&] {
|
||||
Staged* s = made.make();
|
||||
s->add_step([&] { nested = self->ensure(); }); // as if a step pumped the event loop into a caller
|
||||
return s;
|
||||
});
|
||||
self = &lazy;
|
||||
Staged* built = lazy.ensure();
|
||||
REQUIRE(built == made.objects[0].get());
|
||||
REQUIRE(nested == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("A unit that throws leaves the holder free to build the rest", "[Lazy]")
|
||||
{
|
||||
Made<Staged> made;
|
||||
bool thrown = false;
|
||||
Lazy<Staged> lazy("staged", 0, [&] {
|
||||
Staged* s = made.make();
|
||||
s->add_step([&] { thrown = true; throw std::runtime_error("step"); });
|
||||
return s;
|
||||
});
|
||||
lazy.build_step();
|
||||
lazy.build_step();
|
||||
lazy.build_step();
|
||||
REQUIRE_THROWS(lazy.build_step());
|
||||
REQUIRE(thrown);
|
||||
REQUIRE(lazy.pending());
|
||||
REQUIRE_FALSE(lazy.build_step()); // the next unit runs
|
||||
REQUIRE(lazy.built());
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "slic3r/GUI/PrebuildQueue.hpp"
|
||||
|
||||
using Slic3r::GUI::LazyBase;
|
||||
using Slic3r::GUI::PrebuildQueue;
|
||||
|
||||
namespace {
|
||||
|
||||
// A task with `left` units, each taking `unit_ms` of the shared fake clock and logging its id.
|
||||
struct Counter : LazyBase
|
||||
{
|
||||
std::string id;
|
||||
int left;
|
||||
int order;
|
||||
long long unit_ms{ 1 };
|
||||
inline static std::vector<int> log;
|
||||
inline static long long now = 0;
|
||||
|
||||
Counter(int id, int left, int order, long long unit_ms = 1) : id(std::to_string(id)), left(left), order(order), unit_ms(unit_ms) {}
|
||||
|
||||
const std::string& name() const override { return id; }
|
||||
bool built() const override { return left == 0; }
|
||||
bool build_step() override
|
||||
{
|
||||
now += unit_ms;
|
||||
log.push_back(std::stoi(id));
|
||||
return --left > 0;
|
||||
}
|
||||
int prebuild_order() const override { return order; }
|
||||
};
|
||||
|
||||
// Resets the shared log and clock at the start of a case.
|
||||
struct Reset
|
||||
{
|
||||
Reset() { Counter::log.clear(); Counter::now = 0; }
|
||||
};
|
||||
|
||||
const auto fake_clock = [] { return Counter::now; };
|
||||
const auto no_input = [] { return false; };
|
||||
|
||||
// Runs slices with an unlimited budget until nothing is pending; one task per slice.
|
||||
void drain(PrebuildQueue& q)
|
||||
{
|
||||
while (q.pending())
|
||||
q.run_slice(1000000, fake_clock, no_input);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Tasks run lowest order first, equal order in the order added", "[PrebuildQueue]")
|
||||
{
|
||||
Reset reset;
|
||||
Counter a{ 1, 1, 10 }, b{ 2, 1, 10 }, c{ 3, 1, 50 }, d{ 4, 1, 100 };
|
||||
PrebuildQueue q;
|
||||
q.add(c);
|
||||
q.add(a);
|
||||
q.add(b);
|
||||
q.add(d);
|
||||
REQUIRE(q.names() == "1, 2, 3, 4");
|
||||
drain(q);
|
||||
REQUIRE(Counter::log == std::vector<int>{1, 2, 3, 4});
|
||||
}
|
||||
|
||||
TEST_CASE("A task with nothing pending is skipped, not removed", "[PrebuildQueue]")
|
||||
{
|
||||
Reset reset;
|
||||
Counter a{ 1, 0, 0 }, b{ 2, 2, 1 };
|
||||
PrebuildQueue q;
|
||||
q.add(a);
|
||||
q.add(b);
|
||||
REQUIRE(q.pending());
|
||||
auto slice = q.run_slice(1, fake_clock, no_input); // one unit of b
|
||||
REQUIRE(slice.units == 1);
|
||||
REQUIRE(Counter::log == std::vector<int>{2});
|
||||
|
||||
a.left = 1; // a's work returned; it comes first again
|
||||
q.run_slice(1, fake_clock, no_input);
|
||||
REQUIRE(Counter::log == std::vector<int>{2, 1});
|
||||
}
|
||||
|
||||
TEST_CASE("A slice with nothing pending runs no unit", "[PrebuildQueue]")
|
||||
{
|
||||
Reset reset;
|
||||
PrebuildQueue q;
|
||||
REQUIRE_FALSE(q.pending());
|
||||
auto slice = q.run_slice(40, fake_clock, no_input);
|
||||
REQUIRE(slice.units == 0);
|
||||
REQUIRE_FALSE(slice.completed);
|
||||
REQUIRE_FALSE(slice.remaining);
|
||||
}
|
||||
|
||||
TEST_CASE("A slice stops once its budget is spent, after the unit that crossed it", "[PrebuildQueue]")
|
||||
{
|
||||
Reset reset;
|
||||
Counter a{ 1, 10, 0, 15 };
|
||||
PrebuildQueue q;
|
||||
q.add(a);
|
||||
auto slice = q.run_slice(40, fake_clock, no_input);
|
||||
REQUIRE(slice.units == 3); // units end at 15, 30 and 45 ms; the one crossing 40 is the last
|
||||
REQUIRE(slice.ms == 45);
|
||||
REQUIRE_FALSE(slice.completed);
|
||||
REQUIRE(slice.remaining);
|
||||
REQUIRE(a.left == 7);
|
||||
}
|
||||
|
||||
TEST_CASE("A slice stops after the unit during which input arrived", "[PrebuildQueue]")
|
||||
{
|
||||
Reset reset;
|
||||
Counter a{ 1, 10, 0 };
|
||||
bool input = false;
|
||||
PrebuildQueue q;
|
||||
q.add(a);
|
||||
auto slice = q.run_slice(40, fake_clock, [&] { input = a.left == 8; return input; });
|
||||
REQUIRE(slice.units == 2);
|
||||
REQUIRE_FALSE(slice.completed);
|
||||
REQUIRE(slice.remaining);
|
||||
}
|
||||
|
||||
TEST_CASE("A slice reports completion, whether work remains, and each unit's time", "[PrebuildQueue]")
|
||||
{
|
||||
Reset reset;
|
||||
Counter a{ 1, 2, 0, 5 }, b{ 2, 1, 1 };
|
||||
std::vector<long long> unit_ms;
|
||||
PrebuildQueue q;
|
||||
q.add(a);
|
||||
q.add(b);
|
||||
auto slice = q.run_slice(40, fake_clock, no_input, [&](const std::string& name, long long ms) {
|
||||
REQUIRE(name == "1");
|
||||
unit_ms.push_back(ms);
|
||||
});
|
||||
REQUIRE(slice.units == 2);
|
||||
REQUIRE(slice.completed);
|
||||
REQUIRE(slice.name == "1");
|
||||
REQUIRE(slice.remaining); // b
|
||||
REQUIRE(unit_ms == std::vector<long long>{5, 5});
|
||||
slice = q.run_slice(40, fake_clock, no_input);
|
||||
REQUIRE(slice.completed);
|
||||
REQUIRE_FALSE(slice.remaining);
|
||||
REQUIRE_FALSE(q.pending());
|
||||
}
|
||||
|
||||
TEST_CASE("A unit may add a task to the queue it runs from", "[PrebuildQueue]")
|
||||
{
|
||||
Reset reset;
|
||||
PrebuildQueue q;
|
||||
Counter later{ 2, 1, 5 };
|
||||
struct Adder : LazyBase
|
||||
{
|
||||
PrebuildQueue& q;
|
||||
Counter& later;
|
||||
std::string id{ "1" };
|
||||
bool done{ false };
|
||||
Adder(PrebuildQueue& q, Counter& later) : q(q), later(later) {}
|
||||
const std::string& name() const override { return id; }
|
||||
bool built() const override { return done; }
|
||||
bool build_step() override
|
||||
{
|
||||
Counter::log.push_back(1);
|
||||
done = true;
|
||||
q.add(later);
|
||||
return false;
|
||||
}
|
||||
int prebuild_order() const override { return 0; }
|
||||
} first{ q, later };
|
||||
q.add(first);
|
||||
drain(q);
|
||||
REQUIRE(Counter::log == std::vector<int>{1, 2});
|
||||
}
|
||||
|
||||
TEST_CASE("clear drops every task", "[PrebuildQueue]")
|
||||
{
|
||||
Reset reset;
|
||||
Counter a{ 1, 1, 0 };
|
||||
PrebuildQueue q;
|
||||
q.add(a);
|
||||
q.clear();
|
||||
REQUIRE_FALSE(q.pending());
|
||||
REQUIRE(q.run_slice(40, fake_clock, no_input).units == 0);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "slic3r/GUI/StagedBuild.hpp"
|
||||
|
||||
using Slic3r::GUI::StagedBuild;
|
||||
|
||||
namespace {
|
||||
|
||||
// Exposes the protected queueing calls and records the order steps ran in.
|
||||
struct Staged : StagedBuild
|
||||
{
|
||||
std::vector<int> ran;
|
||||
void queue(int id) { add_build_step([this, id] { ran.push_back(id); }); }
|
||||
void queue_child(Staged& child) { add_build_steps_of(child); }
|
||||
void queue_nested(int id, int nested)
|
||||
{
|
||||
add_build_step([this, id, nested] {
|
||||
ran.push_back(id);
|
||||
queue(nested);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Steps run in the order they were queued, one per build_step", "[StagedBuild]")
|
||||
{
|
||||
Staged s;
|
||||
s.queue(1);
|
||||
s.queue(2);
|
||||
s.queue(3);
|
||||
REQUIRE_FALSE(s.built());
|
||||
|
||||
REQUIRE(s.build_step());
|
||||
REQUIRE(s.ran == std::vector<int>{1});
|
||||
REQUIRE(s.build_step());
|
||||
REQUIRE(s.ran == std::vector<int>{1, 2});
|
||||
REQUIRE_FALSE(s.build_step());
|
||||
REQUIRE(s.ran == std::vector<int>{1, 2, 3});
|
||||
REQUIRE(s.built());
|
||||
REQUIRE_FALSE(s.build_step());
|
||||
REQUIRE(s.ran.size() == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("A panel with no steps is built from the start", "[StagedBuild]")
|
||||
{
|
||||
Staged s;
|
||||
REQUIRE(s.built());
|
||||
REQUIRE_FALSE(s.build_step());
|
||||
}
|
||||
|
||||
TEST_CASE("A step may queue another step, which runs after the ones already queued", "[StagedBuild]")
|
||||
{
|
||||
Staged s;
|
||||
s.queue_nested(1, 3);
|
||||
s.queue(2);
|
||||
|
||||
REQUIRE(s.build_step());
|
||||
REQUIRE_FALSE(s.built());
|
||||
REQUIRE(s.build_step());
|
||||
REQUIRE_FALSE(s.build_step());
|
||||
REQUIRE(s.ran == std::vector<int>{1, 2, 3});
|
||||
REQUIRE(s.built());
|
||||
}
|
||||
|
||||
TEST_CASE("A parent waits for steps a child queues after being adopted", "[StagedBuild]")
|
||||
{
|
||||
Staged child;
|
||||
child.queue_nested(1, 2); // step 1 queues step 2 while it runs
|
||||
Staged parent;
|
||||
parent.queue_child(child); // one forwarder, for step 1
|
||||
parent.queue(10);
|
||||
|
||||
REQUIRE(parent.build_step()); // child step 1, which queues step 2
|
||||
REQUIRE(parent.build_step()); // 10; own steps exhausted, the child still has 2
|
||||
REQUIRE_FALSE(parent.built());
|
||||
REQUIRE_FALSE(parent.build_step()); // child step 2
|
||||
REQUIRE(child.ran == std::vector<int>{1, 2});
|
||||
REQUIRE(parent.ran == std::vector<int>{10});
|
||||
REQUIRE(parent.built());
|
||||
}
|
||||
|
||||
TEST_CASE("A child's remaining steps are forwarded one per parent step", "[StagedBuild]")
|
||||
{
|
||||
Staged child;
|
||||
child.queue(1);
|
||||
child.queue(2);
|
||||
child.queue(3);
|
||||
REQUIRE(child.build_step()); // the parent adopts only what is left
|
||||
|
||||
Staged parent;
|
||||
parent.queue_child(child);
|
||||
parent.queue(10);
|
||||
|
||||
REQUIRE(parent.build_step());
|
||||
REQUIRE(child.ran == std::vector<int>{1, 2});
|
||||
REQUIRE(parent.build_step());
|
||||
REQUIRE(child.ran == std::vector<int>{1, 2, 3});
|
||||
REQUIRE(child.built());
|
||||
REQUIRE_FALSE(parent.build_step());
|
||||
REQUIRE(parent.ran == std::vector<int>{10});
|
||||
REQUIRE(parent.built());
|
||||
}
|
||||
Reference in New Issue
Block a user