perf: speed up startup and show progress in splash screen (#13667)

* perf: speed up startup and show progress in splash screen

Cold-start was ~15 s. Profiled in GUI_App::on_init_inner: 5 s in
preset_bundle->load_presets (sequential vendor loading of ~3000 JSON
profile files), 3.7 s in new MainFrame(), and a 1.5 s splash screen
that closed long before init finished — so the user stared at a
frozen blank screen for several seconds with no feedback.

Changes:

- PresetBundle::load_system_presets_from_json: parallelize vendor
  loading with TBB. ORCA_FILAMENT_LIBRARY is loaded first
  synchronously (it is the inheritance base for filaments); the
  remaining vendors are loaded in parallel into separate PresetBundle
  instances, then sequentially merged. On a typical setup this drops
  load_presets from ~5 s to ~3.5 s; the saving scales with vendor
  count and CPU cores.

- SplashScreen: remove the wxSPLASH_TIMEOUT flag so the splash stays
  visible until the main window is shown explicitly, and add status
  updates at each slow init phase ("Loading printer and filament
  profiles", "Creating main window", "Loading current preset",
  "Showing main window"). The splash is destroyed right after
  mainframe->Show(true).

- MainFrame: FileHistory::LoadThumbnails moved to a detached
  background thread. The previous synchronous call opened every recent
  3MF file at startup — on macOS that triggered the TCC permission
  dialog for ~/Downloads mid-launch, freezing the whole app until the
  user clicked. Letting it run in the background means the UI is
  responsive from the start and thumbnails populate when ready.

* revert: keep LoadThumbnails on the main thread (review feedback)

SoftFever pointed out the detached thread introduces race conditions
(e.g. MainFrame::open_recent_project() reading while the thread writes
m_thumbnails) and that thumbnails may not appear if the homepage shows
before it finishes. LoadThumbnails already uses tbb::parallel_for
internally, so the call-site offload gave no real speedup. Reverted to
the original synchronous call. The macOS TCC-dialog concern that
motivated this will be handled properly in the lazy-init refactor.

---------

Co-authored-by: SoftFever <softfeverever@gmail.com>
This commit is contained in:
gedanke
2026-05-16 08:42:25 +02:00
committed by GitHub
parent 83179d5978
commit 5a136a25d1
2 changed files with 86 additions and 39 deletions

View File

@@ -27,6 +27,8 @@
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <miniz/miniz.h>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
// Mark string for localization and translate.
#define L(s) Slic3r::I18N::translate(s)
@@ -2180,48 +2182,82 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
vendor_name.erase(vendor_name.size() - 5);
vendor_names.push_back(vendor_name);
}
// Move ORCA_FILAMENT_LIBRARY to the beginning of the list
for (size_t i = 0; i < vendor_names.size(); ++ i) {
if (vendor_names[i] == ORCA_FILAMENT_LIBRARY) {
std::swap(vendor_names[0], vendor_names[i]);
break;
}
// Separate ORCA_FILAMENT_LIBRARY from other vendors. It must be loaded
// first because other vendors' filaments may inherit from it via the
// `base_bundle` lookup in parse_subfile. The remaining vendors are
// independent (no cross-vendor inheritance) and can be loaded in parallel.
std::string orca_lib_vendor;
std::vector<std::string> other_vendors;
other_vendors.reserve(vendor_names.size());
for (auto& vn : vendor_names) {
if (vn == ORCA_FILAMENT_LIBRARY)
orca_lib_vendor = vn;
else if (!(validation_mode && !vendor_to_validate.empty() && vn != vendor_to_validate))
other_vendors.push_back(vn);
}
for (auto &vendor_name : vendor_names)
{
if (validation_mode && !vendor_to_validate.empty() && vendor_name != vendor_to_validate && vendor_name != ORCA_FILAMENT_LIBRARY)
continue;
// Step 1: Load ORCA_FILAMENT_LIBRARY into `this` synchronously.
if (!orca_lib_vendor.empty()) {
try {
// Load the config bundle, flatten it.
if (first) {
// Reset this PresetBundle and load the first vendor config.
append(substitutions, this->load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem, compatibility_rule).first);
first = false;
} else {
// Load the other vendor configs, merge them with this PresetBundle.
// Report duplicate profiles.
PresetBundle other;
append(substitutions, other.load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem, compatibility_rule, this).first);
std::vector<std::string> duplicates = this->merge_presets(std::move(other));
if (!duplicates.empty()) {
errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: ";
for (size_t i = 0; i < duplicates.size(); ++i) {
if (i > 0)
errors_cummulative += ", ";
errors_cummulative += duplicates[i];
++m_errors;
BOOST_LOG_TRIVIAL(error) << "Found duplicated preset: " + duplicates[i] + " in vendor: " + vendor_name + ": ";
}
}
}
append(substitutions, this->load_vendor_configs_from_json(dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule).first);
first = false;
} catch (const std::runtime_error &err) {
if (validation_mode)
throw err;
else {
errors_cummulative += err.what();
errors_cummulative += "\n";
errors_cummulative += err.what();
errors_cummulative += "\n";
}
}
// Step 2: Load remaining vendors in parallel. Each gets its own
// PresetBundle and uses `this` (which contains ORCA_FILAMENT_LIBRARY)
// as the base_bundle for cross-bundle inheritance lookups.
std::vector<std::unique_ptr<PresetBundle>> parallel_bundles(other_vendors.size());
std::vector<PresetsConfigSubstitutions> parallel_substitutions(other_vendors.size());
std::vector<std::string> parallel_errors(other_vendors.size());
tbb::parallel_for(tbb::blocked_range<size_t>(0, other_vendors.size()),
[&](const tbb::blocked_range<size_t>& range) {
for (size_t i = range.begin(); i < range.end(); ++i) {
auto bundle = std::make_unique<PresetBundle>();
try {
auto result = bundle->load_vendor_configs_from_json(
dir.string(), other_vendors[i], PresetBundle::LoadSystem,
compatibility_rule, this);
parallel_substitutions[i] = std::move(result.first);
parallel_bundles[i] = std::move(bundle);
} catch (const std::runtime_error &err) {
parallel_errors[i] = err.what();
}
}
});
// Step 3: Sequentially merge the parallel-loaded bundles into `this`.
// The merge order is the original vendor order so any duplicate-warning
// output stays stable across runs.
for (size_t i = 0; i < other_vendors.size(); ++i) {
if (!parallel_errors[i].empty()) {
if (validation_mode)
throw std::runtime_error(parallel_errors[i]);
errors_cummulative += parallel_errors[i];
errors_cummulative += "\n";
continue;
}
if (!parallel_bundles[i])
continue;
const std::string& vendor_name = other_vendors[i];
append(substitutions, std::move(parallel_substitutions[i]));
std::vector<std::string> duplicates = this->merge_presets(std::move(*parallel_bundles[i]));
first = false;
if (!duplicates.empty()) {
errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: ";
for (size_t j = 0; j < duplicates.size(); ++j) {
if (j > 0)
errors_cummulative += ", ";
errors_cummulative += duplicates[j];
++m_errors;
BOOST_LOG_TRIVIAL(error) << "Found duplicated preset: " + duplicates[j] + " in vendor: " + vendor_name + ": ";
}
}
}

View File

@@ -278,7 +278,11 @@ class SplashScreen : public wxSplashScreen
{
public:
SplashScreen(wxPoint pos = wxDefaultPosition)
: wxSplashScreen(wxBitmap(FromDIP(wxSize(480,480),nullptr)), wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 1500, nullptr, wxID_ANY, wxDefaultPosition, wxDefaultSize,
// No wxSPLASH_TIMEOUT — the splash is closed explicitly once MainFrame
// is shown. The previous 1500 ms auto-timeout closed the splash long
// before init finished, leaving the user staring at a frozen blank
// screen during the slow load_presets / new MainFrame phases.
: wxSplashScreen(wxBitmap(FromDIP(wxSize(480,480),nullptr)), wxSPLASH_CENTRE_ON_SCREEN, 0, nullptr, wxID_ANY, wxDefaultPosition, wxDefaultSize,
#ifdef __APPLE__
wxBORDER_NONE | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP
#else
@@ -2758,7 +2762,7 @@ bool GUI_App::on_init_inner()
//BBS use BBL splashScreen
scrn = new SplashScreen(splashscreen_pos);
wxYield();
//scrn->SetText(_L("Loading configuration")+ dots);
scrn->SetText(_L("Loading configuration") + dots);
}
BOOST_LOG_TRIVIAL(info) << "loading systen presets...";
@@ -2933,6 +2937,7 @@ bool GUI_App::on_init_inner()
// Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only.
// If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force
// installation of a compatible system preset, thus nullifying the system preset substitutions.
if (scrn) { scrn->SetText(_L("Loading printer & filament profiles") + dots); wxYield(); }
init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent);
}
catch (const std::exception& ex) {
@@ -2961,6 +2966,7 @@ bool GUI_App::on_init_inner()
}
#endif
if (scrn) { scrn->SetText(_L("Creating main window") + dots); wxYield(); }
BOOST_LOG_TRIVIAL(info) << "create the main window";
mainframe = new MainFrame();
// hide settings tabs after first Layout
@@ -2985,8 +2991,10 @@ bool GUI_App::on_init_inner()
// ensure the selected technology is ptFFF
plater_->set_printer_technology(ptFFF);
}
else
else {
if (scrn) { scrn->SetText(_L("Loading current preset") + dots); wxYield(); }
load_current_presets();
}
if (plater_ != nullptr) {
plater_->reset_project_dirty_initial_presets();
@@ -2998,7 +3006,10 @@ bool GUI_App::on_init_inner()
#ifdef __WINDOWS__
mainframe->topbar()->SaveNormalRect();
#endif
if (scrn) { scrn->SetText(_L("Showing main window") + dots); wxYield(); }
mainframe->Show(true);
// Close the splash now that the main UI is visible.
if (scrn) { scrn->Destroy(); scrn = nullptr; }
BOOST_LOG_TRIVIAL(info) << "main frame firstly shown";
//#if BBL_HAS_FIRST_PAGE