mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-04 22:31:02 +00:00
fix: Home start shows Prepare or a blank window, and Prepare opens slowly (#15878)
This commit is contained in:
@@ -2,219 +2,136 @@
|
||||
|
||||
## Why it exists
|
||||
|
||||
The main window is a notebook of tabs. When the first frame appears, one tab is on
|
||||
screen and the others are not; some of them are opened later in the session, some
|
||||
never, and some only exist for certain printers. Building a tab before the first frame
|
||||
adds its cost to every startup, whether or not the tab is used.
|
||||
The main window is a notebook of tabs, and only one of them is on screen when the first
|
||||
frame appears. Others are opened later in the session, some never, and some only exist
|
||||
for certain printers. Building every tab before the first frame makes each startup pay for
|
||||
tabs the user may never open.
|
||||
|
||||
This subsystem builds a tab's panel the first time the tab is shown. It also builds the
|
||||
remaining tabs while the user is idle after startup, in units of tens of milliseconds,
|
||||
so a click that lands in the middle of one waits for that unit and no longer. Startup
|
||||
pays only for what the first frame shows, and the other tabs are usually built before
|
||||
anyone opens them.
|
||||
This subsystem builds a tab the first time it is shown, and builds the rest in small units
|
||||
while the user is idle after startup. Startup pays only for what the first frame shows,
|
||||
the other tabs are usually ready before anyone opens them, and a click that lands in the
|
||||
middle of the idle build waits for one unit at most. Work that is not a tab, such as a
|
||||
dialog or the 3D view's GL resources, uses the same machinery.
|
||||
|
||||
## The parts
|
||||
|
||||
`src/slic3r/GUI/Lazy.hpp`, `LazyPage.hpp`, `StagedBuild.hpp` and `IdleScheduler.hpp/.cpp`
|
||||
(with its wx-free `PrebuildQueue.hpp`) are independent. A holder can hold anything a
|
||||
factory makes, a page is a holder with a placeholder widget, a staged object can live
|
||||
outside a holder, and the scheduler knows none of them; it runs tasks, which `MainFrame`
|
||||
makes from the holders.
|
||||
The parts are independent. A holder can hold anything a factory makes, a placeholder page
|
||||
is a holder with a widget, a staged object can live outside a holder, and the scheduler
|
||||
knows none of them; it runs tasks, which the main window makes from the holders.
|
||||
|
||||
### Lazy<T>: the holder
|
||||
### The holder: `Lazy<T>`
|
||||
|
||||
Holds an object that a factory makes on first use, under a name for the log and a place
|
||||
in the idle queue, both given by the owner. The header has no wx dependency; the busy
|
||||
cursor for an on-demand build lives in `Lazy.cpp` and is skipped when there is no app,
|
||||
so the holder is unit-tested.
|
||||
A holder keeps one object and the factory that makes it. The rest of the app reads the
|
||||
object if it exists, makes sure it exists now when about to show or navigate to it, or runs
|
||||
something once it exists. A type with one instance in the app gets these as statics
|
||||
through `LazyInstance<T>`, so callers need no reference to the main window, and all of them
|
||||
are harmless while no holder exists.
|
||||
|
||||
- `get()` is null until the object is completely built; `built()` says the same.
|
||||
- `ensure()` builds whatever is left now, under a busy cursor, and returns the object, or
|
||||
null in the two cases below. A click on an unbuilt tab or a first open of a dialog goes
|
||||
through this, and it logs the units and time it took.
|
||||
- `build_step()` runs one unit of construction and returns true while more remain. The
|
||||
first unit is the factory call, and each later unit is one `StagedBuild` step if the
|
||||
type has them.
|
||||
- `when_built(fn)` runs `fn(object)` now if it exists, otherwise once it is built.
|
||||
- `pending()` says whether the idle prebuild has work here: not built, and the factory
|
||||
has not returned null. A null factory result is logged and the holder stays unbuilt.
|
||||
- A unit that pumps the event loop cannot re-enter the holder; a nested `build_step()`
|
||||
does nothing and a nested `ensure()` returns null.
|
||||
- After a unit throws, the holder and the scheduler still run the next one.
|
||||
The holder guarantees that callers see the object only once it is completely built. A
|
||||
build cannot re-enter itself, so a nested request finds nothing yet, and a factory that
|
||||
returns null or a unit that throws leaves the holder and the scheduler able to carry on.
|
||||
The holder does not own the object; its wx parent does, as for any window. The holder has
|
||||
no wx dependency and is unit-tested.
|
||||
|
||||
The holder does not own the object; its wx parent does, as for any window. A type with
|
||||
one instance in the app derives from `LazyInstance<T>`, which points at that instance's
|
||||
holder (the holder registers itself, and a recreated `MainFrame`'s holder replaces the
|
||||
old frame's) and gives the type the static entry points the rest of the app uses,
|
||||
`T::if_built()`, `T::ensure()` and `T::when_built(fn)`. They return null, or do
|
||||
nothing, while no holder exists, so a caller needs no `mainframe` check.
|
||||
### The placeholder page: `LazyPage<Panel>`
|
||||
|
||||
`built()` is an atomic flag, since a job worker reads it through the statics
|
||||
(`MainFrame::get_calibration_curr_tab()`); it is set after the object is complete.
|
||||
The notebook needs a page object for a tab to exist and for tabs to be inserted and
|
||||
removed by pointer, and the placeholder is that object. It builds the real panel inside
|
||||
itself the first time it is shown and forwards showing and hiding afterwards, so a panel's
|
||||
own show handling stays its activation hook. Nothing builds while the main window is
|
||||
hidden; the window's first show builds the start page. A page that is out of the book is
|
||||
not prebuilt. A panel built while its page is hidden stays hidden, and gets the theming
|
||||
the window applied before the panel existed.
|
||||
|
||||
`LazyBase` is the holder's type-free interface (`name()`, `built()`, `pending()`,
|
||||
`build_step()`, `prebuild_order()`) and is what the scheduler side sees.
|
||||
### Staged construction: `StagedBuild`
|
||||
|
||||
### LazyPage<Panel>: the placeholder
|
||||
A constructor too big to be one unit builds a skeleton and queues the rest as steps, which
|
||||
run one per unit. A child panel's steps can be forwarded to its parent, and the parent is
|
||||
complete only once the child is. Nothing may use what a step builds before the last step
|
||||
has run, so staged panels follow these constraints:
|
||||
|
||||
A `wxPanel` placed in the parent in place of the real panel, and a `Lazy<Panel>` whose
|
||||
factory makes the panel inside it (by default `new Panel(parent)`). The notebook needs a
|
||||
page object for the tab to exist and for `show_device()` to insert and remove tabs by
|
||||
pointer, and the placeholder is that object. `MainFrame` creates every page once, named
|
||||
after its `TAB_ID_*`, and keeps it for the frame's life; `show_device()` only moves
|
||||
pages in and out of the book.
|
||||
|
||||
- `Show()` is forwarded to the panel, so a panel's own `Show()` override stays its
|
||||
activation hook (refresh timers, machine sync) and `SelectPageByName()` works
|
||||
unchanged. A show builds the panel unless the frame itself is still hidden, because
|
||||
the book selects its first page as it is inserted; `MainFrame::Show()` shows the
|
||||
current page again when the frame becomes visible, which builds it.
|
||||
- `in_book()` says whether the parent notebook currently lists the page, and
|
||||
`pending()` is that and not built, so a tab `show_device()` has taken out of the book
|
||||
is not prebuilt.
|
||||
- A panel built while its page is hidden stays hidden, and a completed panel gets the
|
||||
dark-mode pass the frame ran before it existed.
|
||||
|
||||
The Compare presets dialog is a holder without a page. `MainFrame` keeps a
|
||||
`Lazy<DiffPresetDialog>` whose factory constructs the dialog and binds its events, the
|
||||
dialog derives from `LazyInstance`, and its callers use
|
||||
`DiffPresetDialog::ensure()->show()` and `DiffPresetDialog::if_built()` like a tab's
|
||||
callers do. Saving a preset refreshes the dialog only while it is shown, since `show()`
|
||||
reloads the presets.
|
||||
|
||||
### StagedBuild: construction in units
|
||||
|
||||
A mixin for a panel whose constructor is too big to be one unit. The constructor builds
|
||||
the skeleton (sizers, and the parts other code may touch) and queues the rest with
|
||||
`add_build_step()`. `build_step()` runs one step, and `add_build_steps_of(child)`
|
||||
forwards a child's steps so a nested panel is spread the same way; the parent is built
|
||||
only once the child is, including steps the child queues later. Steps run in order, on
|
||||
the main thread. A `Lazy<T>` recognises a staged type at compile time and runs its steps
|
||||
one per unit.
|
||||
|
||||
Nothing may touch what a step builds before the last step has run. In practice:
|
||||
|
||||
- nothing paints the panel before it is complete, since a panel built at idle is hidden
|
||||
with its page and one built on demand finishes inside `ensure()` before the event loop
|
||||
runs again;
|
||||
- members created in steps are initialised to null in the header, so a partially built
|
||||
panel can be destroyed;
|
||||
- timers and event handlers that use step content check `built()` first
|
||||
(`MonitorPanel::update_all()`, `CalibrationPanel::update_all()`), and a child's steps
|
||||
are queued before anything in the constructor can fire such a handler;
|
||||
- a destructor that disconnects from step content checks `built()` first;
|
||||
- a constructor or step does not take focus while the panel is off screen
|
||||
(`IsShownOnScreen()` before `SetFocus()`), since it may run while the user is typing
|
||||
- members created in steps start out null, so a partly built panel can be destroyed;
|
||||
- timers, event handlers and destructors that touch step content check that the panel is
|
||||
complete first;
|
||||
- nothing takes focus while off screen, since a unit may run while the user is typing
|
||||
elsewhere;
|
||||
- where a step's widgets must keep their place in a sizer that later steps also fill, the
|
||||
constructor adds an empty slot sizer in that position and the step fills the slot
|
||||
(`StatusBasePanel`).
|
||||
- a widget added by a step keeps its place in the sizer through an empty slot the skeleton
|
||||
creates.
|
||||
|
||||
### IdleScheduler: when to build
|
||||
### The scheduler: `IdleScheduler` and `PrebuildQueue`
|
||||
|
||||
A task is any `LazyBase`: a name for the log, `pending()`, `build_step()` and
|
||||
`prebuild_order()`. `PrebuildQueue` holds the tasks by order (equal order in the order
|
||||
added) and runs a slice, the units of the first pending task until it completes, the
|
||||
budget is spent on the clock it is given, or the interrupt predicate says input arrived.
|
||||
It has no wx dependency and is unit-tested with a fake clock. A task whose work is gone
|
||||
is passed over and stays in the queue, so a tab that `show_device()` removes and later
|
||||
re-inserts is pending again.
|
||||
The queue holds tasks in order, and a slice runs units of the first pending task until
|
||||
the task finishes, the time budget is spent, or input arrives. It has no wx dependency and
|
||||
is unit-tested with a fake clock. A task whose work goes away, such as a tab removed from
|
||||
the book, is skipped, and becomes pending again if the work comes back.
|
||||
|
||||
`IdleScheduler` drives the queue with the real clock, the app's input timestamp and the
|
||||
Windows queue check, and logs each unit and slice. Each slice is a timer message: a
|
||||
period of 250 ms while waiting for the user to go quiet, and a one-shot of zero after a
|
||||
slice that left work, so the event loop dispatches whatever it has queued (paint,
|
||||
timers, input) before the next slice runs. Chaining slices with `CallAfter` would not do
|
||||
this: wx drains pending events fully before the next native message, on every platform.
|
||||
On GTK the one-shot is 5 ms, because a due GLib timeout runs ahead of the redraw and
|
||||
idle sources that paint and deliver posted events.
|
||||
A slice runs only once the user has been idle for the quiet time, and the timer stops
|
||||
itself once no task is pending. The tick period, quiet time and slice length are
|
||||
constants in `IdleScheduler.cpp`. A unit cannot be interrupted once started, so the
|
||||
largest unit bounds click latency on Windows; on the other platforms a click also waits
|
||||
for the rest of the slice. A unit that pumps the event loop lets the timer fire inside
|
||||
its own slice, and that tick does nothing.
|
||||
A slice runs only once the user has been idle for a short quiet time, and each slice is its
|
||||
own timer message, so paint, timers and input queued in between are handled before the
|
||||
next slice. Posting slices as pending events would not do that, because wx drains every
|
||||
pending event before the next native message. On GTK a timer that is always due starves
|
||||
the lower-priority sources that repaint and deliver posted events, so slices are a few
|
||||
milliseconds apart. On Windows a slice also waits while the native queue holds input,
|
||||
not counting mouse moves, which Windows synthesizes whenever a window appears under the
|
||||
cursor. A slice never runs inside a `wxYield()`, where it would build pages in the middle of
|
||||
the code that yielded. A unit cannot be interrupted, so the largest unit bounds how long a
|
||||
click can wait.
|
||||
When nothing is pending the timer stops and the subsystem costs nothing.
|
||||
|
||||
The tasks are made by their owners. `MainFrame::prebuild_pages_when_idle()` registers
|
||||
every `LazyPage` the frame created, in or out of the book (`pending()` is false for a
|
||||
page out of the book), the Compare presets holder, and the Prepare sidebar's settings
|
||||
page from `ParamsPanel::settings_page_prebuild()`, whose first unit selects the default
|
||||
tab if none is selected yet and whose later units build one option group each.
|
||||
`show_device()` only restarts the timer. `MainFrame` owns the scheduler because it owns
|
||||
everything the tasks build, and clearing the queue with the frame is what keeps a task
|
||||
from outliving its object.
|
||||
The main window owns the scheduler because it owns what the tasks build, and clearing the
|
||||
queue with the window keeps a task from outliving its object. Each owner provides its own
|
||||
tasks, such as a tab, a dialog, the Prepare tab's settings page one option group at a
|
||||
time, the Prepare page's layout at the size the book gives its pages, or the 3D view's GL
|
||||
resources.
|
||||
|
||||
Idle time comes from `GUI_App::FilterEvent`, which timestamps mouse and keyboard events
|
||||
(`wxEVT_CATEGORY_USER_INPUT` minus command events, which all claim that category), and
|
||||
`GUI_App::input_idle_ms()` reports it. On Windows a slice additionally refuses to start
|
||||
when the message queue holds keyboard, button, touch or pen input. Mouse moves are
|
||||
excluded because Windows synthesises one whenever a window appears under the cursor,
|
||||
which every unit does. Other platforms use the timestamp alone.
|
||||
### The 3D view's GL resources
|
||||
|
||||
Once every registered page is built the timer is stopped and the subsystem costs
|
||||
nothing.
|
||||
OpenGL is loaded on the Prepare tab's canvas. When the start page is not Prepare, loading
|
||||
it is an idle task that makes the context current on the hidden canvas, so the start page
|
||||
paints first and Prepare never appears. Loading it on a shown canvas under `Freeze()` holds
|
||||
back the start page's paint, and on GTK `Freeze()` cannot hide the canvas, which is a
|
||||
native child window or a Wayland subsurface drawn outside GTK. A hidden Windows child
|
||||
window keeps its device context, macOS attaches the context to a hidden view, and GTK
|
||||
creates the canvas's surface when the widget is realized, so on GTK the task realizes the
|
||||
canvas first. If the context cannot be made current, the canvas's first render loads the
|
||||
resources.
|
||||
|
||||
## Rules
|
||||
|
||||
**What builds before the first frame.** The Prepare tab's plater, because `post_init()`
|
||||
needs its GL canvas on screen to initialise OpenGL in every startup state, and the start
|
||||
page the user configured. Home is built by the first `MainFrame::Show()`, Prepare's
|
||||
settings page by selecting the tab. `post_init()` passes through the Prepare tab for GL
|
||||
init under `Freeze()` with `MainFrame::select_prepare_for_gl_init()`, which changes the
|
||||
selection without the page-changed event, so nothing else is built for that pass.
|
||||
**Before the first frame.** Only the start page and the Prepare tab's plater are built
|
||||
before the first frame. Everything else goes through a holder.
|
||||
|
||||
**Reaching a lazy object.** A caller uses the type's own statics. `T::if_built()` may
|
||||
return null and is for telling the object something it can live without (a rescale, a
|
||||
colour change, a status update). `T::ensure()` builds the object and is for navigating
|
||||
to it or for a caller that is about to show it. A caller that tells the object something
|
||||
it would not fetch for itself on construction uses `T::when_built()`, which keeps the
|
||||
message until the object exists. A panel that pulls its state when constructed (the Home
|
||||
page requests the recent list on load, the Device tab reads the device manager on show)
|
||||
is reached with `if_built()`; one that cannot pull gets `when_built()`.
|
||||
**Reaching a lazy object.** Callers use the type's statics. Reading it if built is for
|
||||
things the object can live without, such as a rescale, a color change or a status refresh.
|
||||
Making sure it exists is for navigating to it or showing it. Running something once it is
|
||||
built is for state it would not fetch for itself on construction. A panel that pulls its
|
||||
state when constructed only ever needs to be read if built.
|
||||
|
||||
**Unit size.** A unit cannot be interrupted, so it should stay within the slice length on
|
||||
a fast machine. A constructor above that is staged. A single widget above it is the
|
||||
floor unless the widget itself is split.
|
||||
**Unit size.** A unit should fit in one slice on a fast machine. A constructor over that is
|
||||
staged, and a single widget over it is accepted unless the widget itself can be split.
|
||||
|
||||
**Order.** Cheapest and most likely to be opened first, given where `MainFrame` creates
|
||||
each holder. The settings page is 0 (the Prepare tab's own content), Home 10, Device 20
|
||||
(a Bambu user's usual second stop), Calibration 30, Multi-device 40, the web Device
|
||||
view 50, Project 60 (a second WebView2 instance) and the Compare presets dialog 100;
|
||||
steps of ten so a new tab takes a value between its neighbours without renumbering
|
||||
them. `MainFrame::prebuild_pages_when_idle()` registers the tasks once, from
|
||||
`post_init()`.
|
||||
|
||||
**Never prebuilt.** A holder with a negative order, for a tab that few sessions open
|
||||
and that costs more to build unasked than it saves (the Design tab), and plugin-provided
|
||||
tabs, which are Python-side and not lazy pages.
|
||||
**Order.** Tasks run cheapest and most likely to be opened first. Each holder's order is
|
||||
given where it is created, with gaps so a new task fits between its neighbors. A negative
|
||||
order is never prebuilt, for something few sessions open that costs more to build unasked
|
||||
than it saves.
|
||||
|
||||
## Adopting it
|
||||
|
||||
A lazy tab needs:
|
||||
A lazy tab needs a panel type deriving from `LazyInstance`, a placeholder page the main
|
||||
window creates once with its order and registers for the idle build, and every use of the
|
||||
panel outside the main window going through the statics. Its constructor has to cope with
|
||||
the main window already existing and the user being busy elsewhere, so it takes no focus
|
||||
while off screen, and it does all of its own setup, since the main window does nothing to a
|
||||
panel after creating it.
|
||||
|
||||
1. The panel derived from `LazyInstance<Panel>`, since a tab's panel has one instance.
|
||||
2. A `LazyPage<Panel>*` member in `MainFrame`, created once in `init_tabpanel()` with
|
||||
its `TAB_ID_*` as the name, its place by the order rule (and a factory if
|
||||
`new Panel(parent)` is not enough), added to `m_lazy_pages`, and used wherever the
|
||||
tab is added to or looked up in the notebook.
|
||||
3. Every use of the panel outside `MainFrame` going through one of the panel's statics,
|
||||
chosen by the rule above. When converting an existing member, `grep` for it; the
|
||||
compiler finds the rest.
|
||||
4. A constructor that copes with the frame already existing and the user being busy
|
||||
elsewhere, since `wxGetApp().mainframe` is set, the frame may be shown, and the user
|
||||
may be typing when a lazy panel is built: no `SetFocus()` while off screen, and
|
||||
whatever the constructor did through a `MainFrame` accessor before (a mode update, a
|
||||
deferred URL) done in the constructor itself.
|
||||
To stage a heavy constructor, keep the skeleton in the constructor, move the rest into
|
||||
steps in its original order, and follow the staged-construction constraints. Measure the
|
||||
units; a step that is still one big widget is split inside the widget or accepted.
|
||||
|
||||
To stage a heavy constructor, inherit `StagedBuild`, keep the skeleton in the constructor,
|
||||
move the rest into `add_build_step()` lambdas in the original order, and follow the
|
||||
staged-panel rules above. Measure the units. A step that is still one big widget has to
|
||||
be split inside that widget or accepted as the floor.
|
||||
## Verifying
|
||||
|
||||
Verification is by log. `MainFrame::prebuild_pages_when_idle` lists the queue it
|
||||
registered, `IdleScheduler::tick` reports each completed task by name at info level and
|
||||
each slice and unit at debug level, and `Lazy::ensure` reports an object a user built
|
||||
on demand with the units and time it took. A run from the configured start
|
||||
page shows every registered page complete, in order, with no unit longer than intended.
|
||||
A click on a tab mid-prebuild shows the finished panel with an `ensure` line for what was
|
||||
left, and the slices resume for the remaining tasks once the user is idle again.
|
||||
The log lists the queue when it is registered, reports each completed task at info level
|
||||
and each slice and unit at debug level, and reports every on-demand build with its units
|
||||
and time. A task's completion line counts only the slice it finished in. A run from the
|
||||
configured start page should show every registered task complete in order, with no unit
|
||||
longer than intended. A click on a tab during the idle build should show an on-demand
|
||||
build for what was left, with the slices resuming once the user is idle again.
|
||||
|
||||
Reference in New Issue
Block a user