Skip to content

perf(sentry): compute storage-size metric via StorageStatsManager instead of a recursive walk #178

Description

@gmaclennan

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

  • Android computes storage bytes via StorageStatsManager.queryStatsForUid (API 26+), with a 24–25 fallback decided (walk or skip).
  • Value reaches the metric without a JS recursive walk on Android.
  • iOS path decided (gated walk or drop).
  • Bucket-semantics change documented for whoever reads the dashboard.

Interim gating landed in #111.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: androidAndroid-specificperformancePerformance improvement (changelog)sentrySentry telemetry integrationtech-debtRefactor / hardening / cleanup

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions