From 55b7c2d36d5c6498964c747d8bb5ef8625ea2ff0 Mon Sep 17 00:00:00 2001 From: Hadrian Burkhardt Date: Sat, 17 Oct 2026 20:45:41 +0200 Subject: [PATCH] Correct the record: the sort was never the stall The last commit said the sort explained the 598-frame outlier at 20000 files. A measurement says otherwise: sorting 20000 entries on the JVM takes 3-23 ms, three orders of magnitude short of a ten second iteration. The sort was moved off the main thread on a story that fitted the code instead of the data, and PERFORMANCE.md now says so in those words rather than quietly keeping the claim. Where the cost actually is, from the ladder itself: 1000 to 5000 files added 27 frames (about 0.45 s) and 5000 to 20000 added 143 (about 2.4 s), which is ~0.1 ms per file with nothing superlinear - readdir plus a stat per entry, on the IO thread but on the critical path to the first row. The outlier is therefore most likely GC or scheduling on top of that, and the honest next step is a perfetto trace of one of those iterations, not another theory. The off-main sort stays, described as a precaution: 3-23 ms is cheap, but it is work the frame has no reason to wait for. --- .../ui/browser/BrowserViewModel.kt | 12 ++++----- docs/PERFORMANCE.md | 27 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/de/softwareapp_hb/filemanager/ui/browser/BrowserViewModel.kt b/app/src/main/java/de/softwareapp_hb/filemanager/ui/browser/BrowserViewModel.kt index fa5432f..dc44a44 100644 --- a/app/src/main/java/de/softwareapp_hb/filemanager/ui/browser/BrowserViewModel.kt +++ b/app/src/main/java/de/softwareapp_hb/filemanager/ui/browser/BrowserViewModel.kt @@ -329,12 +329,12 @@ class BrowserViewModel( * Derives what is shown from [listing]: hidden files filtered out, then sorted. * * A big folder is sorted on [io] instead of on the caller's thread. `File.listFiles` and the entry - * data are already read off the main thread, but the *sort* was not: the default order is natural - * order, which parses the digits in every name on every comparison, so twenty thousand rows means - * a few hundred thousand string comparisons in the middle of a tap. Measured on the phone at that - * size, one iteration out of five took 598 frames against a median of 184 — the kind of hitch this - * moves off the frame. Small folders stay on the caller's thread, which keeps the code (and the - * tests) honest for the case that happens all the time. + * data are already read off the main thread, but the *sort* was not, and with natural order the digits + * in every name are parsed on every comparison. Measured on the JVM, twenty thousand entries sort in + * **3–23 ms** — cheap enough that this is not the reason the 20 000-file benchmark had one iteration at + * 598 frames against a median of 184 (that outlier is still unexplained; see docs/PERFORMANCE.md). It + * is moved anyway: work the frame has no reason to wait for belongs off the frame, and the threshold + * leaves the common, small folder on the simple synchronous path. */ private fun recompute() { val current = _state.value diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 02ef7f5..fa9f753 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -143,19 +143,22 @@ 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. +**What the outlier is not.** The obvious suspect was the sort: `AllFilesSource.list` already hops to +`Dispatchers.IO`, but `BrowserViewModel.recompute` filtered and sorted the result on the caller's thread, +and the default order is natural order, which parses the digits out of every name on every comparison. +Measured on the JVM, though, sorting **20 000 entries takes 3–23 ms** — three orders of magnitude away from +a 598-frame iteration (about ten seconds). So the sort does not explain the outlier, and saying it did would +have been a story that fitted the code rather than the measurement. -`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. +Where the time actually goes is **per entry**: `readdir` plus a `stat` for size and date, which is what the +ladder shows — going from 1 000 to 5 000 files added 27 frames (about 0.45 s), and 5 000 to 20 000 added 143 +(about 2.4 s), i.e. ~0.1 ms per file with nothing superlinear in it. Twenty thousand files therefore costs +roughly two seconds of listing before a pixel is drawn, and the 598-frame iteration is most likely a GC or a +scheduler hiccup on top of that — unidentified, and worth a perfetto trace rather than another theory. + +`recompute` still sorts large folders off the main thread, as a precaution rather than a fix: 3–23 ms is +cheap in absolute terms, but it is work the frame has no reason to wait for, and the threshold keeps small +folders on the simple synchronous path. 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