Skip to content

fix: trim the persisted queue in one pass - #225

Open
marian-bitstack wants to merge 1 commit into
segmentio:mainfrom
marian-bitstack:fix/queue-trim-one-pass
Open

marian-bitstack wants to merge 1 commit into
segmentio:mainfrom
marian-bitstack:fix/queue-trim-one-pass

Conversation

@marian-bitstack

Copy link
Copy Markdown

Fixes #199.

Problem

When uploads keep failing (offline, captive portal, a VPN or DNS filter blocking the API host), the event queue grows past the 512 KB file cap added in #188. From then on, every queue change runs on the UI isolate:

  1. _writeFile encodes the whole queue to check its size.
  2. _trimQueueToTargetMap encodes it again, then removes the oldest event and re-encodes the entire remaining queue after each removal until it fits under 475 KB. That is quadratic in the number of events dropped.
  3. The trimmed result is encoded once more.

Only the copy written to disk is trimmed, so the in-memory queue stays above the cap and this repeats on every write until a flush succeeds. In a production app, a profile on an iPhone 17 Pro Max under Network Link Conditioner showed _writeFile / setPersisted / _trimQueueToTargetMap at ~35 % / ~34 % / ~29 % of main-isolate samples, with visible freezes of 0.5–1.3 s.

Fix

  • One-pass trim (io.dart). encodeQueue walks the queue newest first, encodes each event at most once, tracks the byte size, and stops as soon as the 512 KB limit is passed. If the queue fits, it is written unchanged; otherwise the newest events that fit in 475 KB are kept, exactly as before. The output is assembled from the already-encoded events, so there is no extra full encode. A write now costs at most ~512 KB of encoding regardless of how long the in-memory queue is. The now-unreachable "still too big, write an empty queue" fallback is removed; an event larger than the limit on its own still results in an empty queue.
  • In-memory queue left as is. Only the persisted copy is trimmed, as before. The write cost no longer depends on the in-memory length: on a desktop VM, one write with 1,000 / 5,000 / 20,000 queued events spends 0 / 2 / 11 ms in QueueState.toJson and 2–3 ms in encodeQueue. Capping the in-memory queue would drop events that could still be uploaded if the network recovers before the app is killed, so it is left for a separate discussion.

Write coalescing is left out: PersistedState already coalesces updates while a write is in flight, and changing write scheduling would grow this PR.

Tests

  • test/utils/store/io_test.dart: an under-limit queue is encoded unchanged; an over-limit queue is trimmed to ≤ 475 KB, keeps a contiguous run of the newest events, and one more event would not have fit; with 20,000 events, each event's toJson is called at most once and fewer than 600 are encoded at all (the previous implementation encoded each surviving event dozens of times); a single oversized event yields an empty queue.

flutter analyze (no new issues) and flutter test pass in packages/core.

When uploads keep failing, every queue change re-encoded the whole queue
and, past 512 KB, _trimQueueToTargetMap re-encoded it again after each
removed event, which is quadratic and runs on the UI isolate.

encodeQueue encodes events newest first, each at most once, and stops
once the limit is passed, so a write costs at most ~512 KB of encoding
regardless of queue length. Same 512 KB / 475 KB thresholds as before.

Fixes segmentio#199

This branch has not been deployed

No deployments
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.

segment_analytics 1.1.11: UI jank / apparent freezes on track

1 participant