From 5a136a25d10a565ed09c91e736d3b48e9da1f1e4 Mon Sep 17 00:00:00 2001 From: gedanke Date: Sat, 16 May 2026 08:42:25 +0200 Subject: [PATCH] perf: speed up startup and show progress in splash screen (#13667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- src/libslic3r/PresetBundle.cpp | 108 ++++++++++++++++++++++----------- src/slic3r/GUI/GUI_App.cpp | 17 +++++- 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 691d7aec81..7d56bbe5e1 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -27,6 +27,8 @@ #include #include #include +#include +#include // Mark string for localization and translate. #define L(s) Slic3r::I18N::translate(s) @@ -2180,48 +2182,82 @@ std::pair 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 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 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> parallel_bundles(other_vendors.size()); + std::vector parallel_substitutions(other_vendors.size()); + std::vector parallel_errors(other_vendors.size()); + + tbb::parallel_for(tbb::blocked_range(0, other_vendors.size()), + [&](const tbb::blocked_range& range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + auto bundle = std::make_unique(); + 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 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 + ": "; } } } diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 38c9199d56..c12fa964f8 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -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