Skip to content

Read the first folder and sidebar while the window loads - #89

Open
thisisgm wants to merge 21 commits into
omacom:mainfrom
thisisgm:launch
Open

thisisgm wants to merge 21 commits into
omacom:mainfrom
thisisgm:launch

Conversation

@thisisgm

@thisisgm thisisgm commented Nov 12, 2026 •

Copy link
Copy Markdown

Stacked on #26: its eighteen commits come first here, the last three are this pull request's, and I will rebase it as soon as #26 lands.

Two changes to how the first window comes up, one commit each with its own tests: the sidebar's lists and the folder named at launch are read on the thread pool while the window is being made, where the lists were read on the UI thread before the window existed and the folder only after it. From a cold start into a folder of 100,000 files the rows are listed about 600 ms sooner, and by the medians the window has settled about 330 ms sooner in the grid, 300 ms in the list and 65 ms in the columns. A third commit leaves that reading when the path has since come to lead to another folder. In the grid the window itself comes up later and comes up whole; that and the memory it costs there are under Limits. The suite goes from 236 to 241 tests. No library, depends or optdepends entry is added, and nothing is kept running: one reading, taken or dropped in the first turn of the event loop.

What you'd notice

  • The sidebar's lists no longer hold up the window. The home folders, the views, the bookmarks and the system's device were read in the Places constructor, on the UI thread, before the QML was compiled. A pool thread now reads them from the constructor on, and the first look at any list waits for it. The first frame still draws every list whole, and no change signal follows it.
  • The folder is there when the window is. A folder was listed only once the window's model existed, after the QML was compiled and the window built, so the sidebar stood beside an empty pane and a large folder's rows came in well after it. main() now starts reading the folder named at launch before the window is made, and the first model takes that reading. The sidebar and the rows appear together.
  • Never an old folder. The reading is made under an inotify watch that is added before the first name is read, and it is taken only when no event is queued on that watch; otherwise the model reads the folder itself, as it does today. A folder that can't be watched is read by the model as today. One model takes the reading, once. A reading nobody takes by the first turn of the event loop is dropped: one not yet begun opens nothing, and one under way stops at the next name, as does one whose model has left for another folder. And never another folder: the reading notes which folder its path led to, and is left when a link on the way has since been switched to lead elsewhere, which no watch of the folder read reports.

Numbers

Measured on a mini PC, cold start into a folder of 100,000 files, six runs a view, #26's head (e7ccfe5) against this branch's. Settled is the time until the window is up and the process has gone quiet. Both are the CMake build.

View #26 this branch
Grid First window 906 to 966 ms 850 to 1089 ms
Settled 1530 to 1635 ms 1227 to 1316 ms
Memory (PSS) 206.5 to 206.9 MiB 218.2 to 222.1 MiB
CPU time 1.37 to 1.43 s 1.35 to 1.52 s
List First window 921 to 960 ms 838 to 989 ms
Settled 1448 to 1491 ms 1107 to 1360 ms
Memory (PSS) 154.5 to 163.5 MiB 154.5 to 155.3 MiB
CPU time 1.21 to 1.26 s 1.20 to 1.26 s
Columns First window 942 to 1079 ms 878 to 1016 ms
Settled 1472 to 1587 ms 1372 to 1463 ms
Memory (PSS) 165.7 to 186.5 MiB 172.4 to 173.7 MiB
CPU time 1.99 to 2.07 s 1.81 to 2.00 s

Where the time goes, from a build of each over today's main with three marks added (list, cold, twelve runs, counted from exec): the folder is listed at 707 to 814 ms where it was listed at 1317 to 1420 ms, and the first frame is drawn at 774 to 883 ms where it was drawn at 836 to 915 ms.

Screenshots

The grid from a warm start, frames of a screen recording, each with its time from launch. Before, the sidebar stands beside an empty pane until the rows land; after, the window comes up with its rows.

The grid coming up, before and after

Tests

Five new cases, each failing without its commit: the sidebar's lists are read off the UI thread and are whole at the first look; a folder read ahead is taken once, and a change made after the reading began (a file grown, a file added) is seen by the model, never the reading; a folder that can't be watched is read by the model itself; and a reading nobody wants (never taken, or taken by a model that left) opens no folder, counted with IN_OPEN on the folders themselves while the pool is held; and a reading whose path was switched to another folder after it was made is left for that folder's own rows, while one through a link left alone is taken.

Limits

  • The reading ahead uses one inotify instance and one watch for the launch folder, closed when the reading is taken or dropped. With none free, nothing is read ahead and the model lists the folder as today.
  • A link switched to another folder and back again within the reading itself is not seen.
  • Only the folder named at launch is read ahead, in the order a new window lists it (by name, hidden files left out).
  • In the grid the window comes up later than it did, by about 110 ms at the median into the 100,000-file folder (the table's first row; four runs of six, the other two came up sooner): the rows are there before the first frame, so the tiles are made before it instead of after it. In the recording above it is whole about 30 ms sooner than before.
  • In the grid the process holds 11 to 16 MiB more in the 100,000-file folder, and about 3 MiB more in a folder of 4,000. With glibc's mmap threshold fixed at its default, so that freed listing buffers go back to the system, the difference in the large folder is 7 MiB and both builds hold 12 to 25 MiB less; I have not found where the rest goes. The list and the columns hold what they held.

@greptile-apps

greptile-apps Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issues were found.

Summary

This PR overlaps the first folder and sidebar reads with window creation.

  • Folder views fill in row details and previews as they are needed.
  • Large folder listings reuse data and skip work they do not need.
  • The launch folder can be ready when Folio's first window appears.
  • Sidebar lists are read in the background and arrive as complete lists.
  • Folio updates its corner rounding when Hyprland reports a config reload.

The earlier findings were rechecked against the current code. No new actionable issues were found. Tests were inspected but not run.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Launch folder] --> B[Start watched read on pool]
    A --> C[Build window and sidebar]
    C --> D[First model requests folder]
    B --> E{Same folder and no queued changes?}
    D --> E
    E -->|Yes| F[Take earlier rows once]
    E -->|No| G[Read folder again]
    F --> H[Show rows]
    G --> H
Loading

Reviews (8) · Last reviewed commit: "Leave a read-ahead whose path leads to a..." · Reviewed by Greptile

Comment thread src/foldermodel.cpp Resolved
Comment thread src/theme.cpp Resolved
Comment thread src/theme.cpp Resolved
@thisisgm
thisisgm force-pushed the launch branch 3 times, most recently from dbae059 to fab9ca6 Compare November 12, 2026 06:04
Comment thread src/foldermodel.cpp Resolved
Building a listing asked the disk about every file a second time,
because Qt stats a file again before it reads the name. A regular file
is now typed from the name the listing already read; a link, a pipe or
a device keeps the full lookup.
A selection, a folder count or a second view of the same folder each
made the model copy its whole listing. They now read it in place, so a
large folder is held once however many views show it.
The preview's folder summary sorted every name to count them and to
pick two pictures. It now keeps the first two pictures in one pass, so
summarizing a large folder no longer holds every name.
Every entry kept its own copy of its type, kind and extension, and the
listing kept spare room for entries that never came. The entries of a
listing now share those strings, and the spare room is given back.
Opening a folder typed every file before its first row showed. The
listing now leaves a regular file's type, kind and extension unread
until a role asks, which touches no disk; a sort by kind still types
every entry in the worker.
Every refresh of Places, the 15 second poll included, told all four
sidebar lists they had changed. Each list now has its own signal and
is announced only when its contents differ.
While active, Folio ran hyprctl once a second to read the corner
rounding. It now reads it at start, on a palette change, on return to
the window and when Hyprland's event socket reports a reloaded config.
Sorting 100,000 names collated both strings on every compare. A
listing that arrives in a few ordered runs is now merged, and a name
or kind sort makes one sort key an entry first. The comparator is the
same, so every order and tie stays; the C locale compares as before.
A name or kind listing read every entry's size and date before its
first row showed. It now reads the first 320 rows, and a later row
when a view asks, one batch a turn, off the UI thread. A row not read
yet shows no size and no date, never 0 B.
A re-read that found the same names kept each folder's item count,
even when the folder's own date had moved. Such a folder's count is
now dropped and counted again, as it was before the listing learned
to keep its rows through a re-read.
The event socket was opened once. Hyprland hangs up on a reader that
fell behind, and after that a config reload no longer reached the
corners. The watch is now taken up again when Folio is back in use.
A text tile past the rows read at first asked for its page with no
date, then again when the date arrived, and a page has no cache on
disk, so the file was read and drawn twice. Like a thumbnail, a page
is now asked for only once its date is known.
A folder past the rows read at first is counted without its date
being read, so a re-read had nothing to compare and kept its old
count. Its date is now read just before it is counted and kept
with the count, and the re-read sees the folder changed since.
A burst of events was read whole into memory before any of it was
looked at, on the UI thread. One turn of the event loop now reads
64 KiB at most and looks at each piece as it comes, and a line too
long to keep is dropped to its end, its last piece included.
Hung up on by Hyprland, the theme took its watch up again only when
Folio was left and come back to, so a config reload in between left
the corners as they were. While in use it now connects again after
a pause, and reads the rounding for a reload that went by untold.
Hung up on while Folio stayed in use, the theme tried Hyprland's
event socket once more after a pause. A try that found no socket
was the last, and reloads went untold until Folio was left and
come back to. Each failed try now starts the pause again.
A re-read drops the answers on their way and reads the rows asked for itself.
A row it could not read either stayed marked as being read, so no view could
ask for it again. Such a row is now owed: a view may ask again, the folder's
next re-read tries it again, and a view is told when another view read it.
A view asks for a row's kind, MIME type and extension each time it
makes the row, and each ask read them from the name again: 48 us a
row in /usr/bin. The model now keeps up to 4,096 names' types, until
a folder is found changed.
The home folders, the views, the bookmarks and the system's device
were read in the constructor, on the UI thread, before the window's
QML was compiled. A thread now reads them from the constructor on,
and the first look at any list waits for it, so the first frame
still draws every list whole and no change signal follows it.
The window's folder was listed only once its model existed, after
the QML was compiled and the window built, so a large folder's rows
came well after the window. The folder named at launch is now read
from the start, under a watch, and the first model takes that
reading unless the folder changed since, when it reads it again.
A reading nobody takes, or whose model has left for another folder,
stops, and one not yet begun opens nothing.
The first folder is read while the window is made, and taken when its
watch saw no change. A link on the way that came to lead to another folder
changes nothing in the folder read, so its rows stood under a path that led
elsewhere. The folder is now noted, and the reading left unless it is the same.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant