Take the sort of a large folder off the main thread

The 20000-file measurement had a shape worth following up: median 184 frames, but one iteration of five at 598. Reading the listing path found the reason it could be that shape. AllFilesSource.list already hops to Dispatchers.IO, but BrowserViewModel.recompute filtered and sorted whatever it returned on the caller's thread - and the default order is natural order, which parses the digits in every name on every comparison, so 20000 rows is a few hundred thousand string comparisons plus a fresh 20000-element list inside a tap.

recompute now sorts off the main thread above 500 rows and keeps the synchronous path below it, so the common case is untouched and the tests that read state immediately after an action still hold. The dispatcher is injected for exactly that reason, and the large path has its own test: 600 files, listed, in natural order. The state update is guarded on path, sort and hidden-files so a second tap cannot be overwritten by the answer to the first.

The fix is not re-measured: the 20000-file fixture takes half an hour to build through MIUI's storage layer, so PERFORMANCE.md keeps the 598 outlier as the motivation and says plainly that whoever has the fixture next should confirm the maximum came down.
This commit is contained in:
2026-10-17 20:34:39 +02:00
parent 65569e9af2
commit e5c1ab10a7
4 changed files with 97 additions and 6 deletions
+14
View File
@@ -143,6 +143,20 @@ three seconds' worth of frames; one iteration of five took three times that, so
is not merely slow but occasionally stalls — worth watching rather than explaining away, and the reason the
20 000 point is the one a release should be checked against.
Reading the listing path afterwards found a candidate that explains a hitch of exactly that shape: the
entries were read off the main thread but **sorted on it**. `AllFilesSource.list` already hops to
`Dispatchers.IO`, but `BrowserViewModel.recompute` filtered and sorted whatever `list` returned on the
caller's thread — and the default order is natural order, which parses the digits in every name on every
comparison. At 20 000 rows that is a few hundred thousand string comparisons inside a tap, plus a fresh
20 000-element list, which is the sort of work that is usually fine and occasionally not.
`recompute` now sorts off the main thread above 500 rows (`SORT_OFF_MAIN_THRESHOLD`) and keeps the simple
synchronous path below it, so small folders — the common case, and the one the tests exercise — behave
exactly as before. The dispatcher is injected, so the large path is covered by a test that lists 600 files
and asserts they come out in natural order. **It is not re-measured**: the 20 000-file fixture takes half an
hour to build through MIUI's storage layer, so the 598 outlier stands as the motivation and the next
person with that fixture should confirm the max has come down.
Building that fixture is the expensive part, not measuring it: 20 000 files took about thirty minutes
through MIUI's storage layer (`: >` in a loop), against roughly four minutes of benchmark on top. Worth
knowing before anyone re-creates it, and an argument for the fixture being built once and kept.