Background
The backend emits a coarse storage_size bucket metric once at boot (§11.2.a) so we can see, in aggregate, roughly how much data CoMapeo installs hold. Today this is computed by sampleStorageSize in backend/index.js, which recursively walks the private storage directory in the nodejs-mobile backend — readdir + fs.stat on every file — and sums the bytes before bucketing.
That walk is O(number of files): on an install with a large media/blob store it is real disk I/O and battery proportional to the dataset, run on every cold start. As an interim fix (#111) the walk is now skipped entirely when Sentry is disabled (metrics.isEnabled() gate), so opted-out users no longer pay for it. This issue tracks replacing the walk with an OS-provided figure so even diagnostics-on users don't pay for it.
Proposed change
Use the platform's built-in per-app storage accounting instead of walking the tree.
Android — StorageStatsManager (API 26+).
StorageStatsManager.queryStatsForUid(StorageManager.UUID_DEFAULT, Process.myUid()) returns a StorageStats with getDataBytes() / getCacheBytes() / getAppBytes(). The OS already tracks this per-UID, so it is effectively a lookup rather than a walk. Querying our own app's UID needs no permission (PACKAGE_USAGE_STATS is only required to query other apps).
The backend is JS (nodejs-mobile) and can't call Android APIs directly, but we already compute DeviceTags (deviceClass/osMajor) in Kotlin and pass them to the backend via argv (--deviceClass=…). Storage can ride the same channel: compute the bucket in Kotlin and pass --storageBucket=…, or emit the metric straight from the FGS Sentry bridge. Either way the JS walk disappears.
iOS has no equivalent per-sandbox OS counter (URLResourceValues is still per-file), so iOS keeps a gated walk or skips the metric.
Pros
- Removes an O(files) disk walk from every cold start on Android; the OS figure is ~constant-cost.
- No new permission required for our own UID.
- Fits the existing Kotlin→backend argv plumbing used for device tags.
- Arguably more accurate as a "user footprint" signal:
getDataBytes() is the whole app's on-disk data (filesDir + databases + caches + …), a superset of the privateStorageDir subtree we currently sum.
Cons / considerations
- minSdk is 24;
StorageStatsManager is API 26+. API 24–25 needs a fallback (walk, or skip the metric) — a small and shrinking device slice.
- Metric semantics shift: whole-app data bytes vs. the
privateStorageDir subtree. If we want continuity with already-collected data, account for the step change (e.g. note it in the dashboard, or restrict to the data subdir if the API allows — it does not, it's UID-level).
queryStatsForUid can itself be non-trivial on some devices/large apps (the OS may compute on demand), though still far cheaper than our walk and the sanctioned API.
- Cross-platform split: Android gets the native path; iOS keeps a walk or drops the metric. Slightly more code paths than the current single JS implementation.
- Moves one more piece of telemetry from the backend into the native layer — fine given device tags already live there, but worth being deliberate about where the storage metric is owned.
Rejected alternatives
StatFs — only reports whole-filesystem free/total, not this app's usage.
du -s <dir> shell-out — still an O(files) walk (in C), and awkward to spawn from nodejs-mobile; marginal win.
Acceptance
Interim gating landed in #111.
Background
The backend emits a coarse
storage_sizebucket metric once at boot (§11.2.a) so we can see, in aggregate, roughly how much data CoMapeo installs hold. Today this is computed bysampleStorageSizeinbackend/index.js, which recursively walks the private storage directory in the nodejs-mobile backend —readdir+fs.staton every file — and sums the bytes before bucketing.That walk is O(number of files): on an install with a large media/blob store it is real disk I/O and battery proportional to the dataset, run on every cold start. As an interim fix (#111) the walk is now skipped entirely when Sentry is disabled (
metrics.isEnabled()gate), so opted-out users no longer pay for it. This issue tracks replacing the walk with an OS-provided figure so even diagnostics-on users don't pay for it.Proposed change
Use the platform's built-in per-app storage accounting instead of walking the tree.
Android —
StorageStatsManager(API 26+).StorageStatsManager.queryStatsForUid(StorageManager.UUID_DEFAULT, Process.myUid())returns aStorageStatswithgetDataBytes()/getCacheBytes()/getAppBytes(). The OS already tracks this per-UID, so it is effectively a lookup rather than a walk. Querying our own app's UID needs no permission (PACKAGE_USAGE_STATSis only required to query other apps).The backend is JS (nodejs-mobile) and can't call Android APIs directly, but we already compute
DeviceTags(deviceClass/osMajor) in Kotlin and pass them to the backend via argv (--deviceClass=…). Storage can ride the same channel: compute the bucket in Kotlin and pass--storageBucket=…, or emit the metric straight from the FGS Sentry bridge. Either way the JS walk disappears.iOS has no equivalent per-sandbox OS counter (
URLResourceValuesis still per-file), so iOS keeps a gated walk or skips the metric.Pros
getDataBytes()is the whole app's on-disk data (filesDir + databases + caches + …), a superset of theprivateStorageDirsubtree we currently sum.Cons / considerations
StorageStatsManageris API 26+. API 24–25 needs a fallback (walk, or skip the metric) — a small and shrinking device slice.privateStorageDirsubtree. If we want continuity with already-collected data, account for the step change (e.g. note it in the dashboard, or restrict to the data subdir if the API allows — it does not, it's UID-level).queryStatsForUidcan itself be non-trivial on some devices/large apps (the OS may compute on demand), though still far cheaper than our walk and the sanctioned API.Rejected alternatives
StatFs— only reports whole-filesystem free/total, not this app's usage.du -s <dir>shell-out — still an O(files) walk (in C), and awkward to spawn from nodejs-mobile; marginal win.Acceptance
StorageStatsManager.queryStatsForUid(API 26+), with a 24–25 fallback decided (walk or skip).Interim gating landed in #111.