Skip to content

Bound upload bursts and align batching config with the other platform SDKs - #24

Merged
Fiona2016 merged 3 commits into
mainfrom
feat/upload-throttle-params
Aug 18, 2026
Merged

Bound upload bursts and align batching config with the other platform SDKs#24
Fiona2016 merged 3 commits into
mainfrom
feat/upload-throttle-params

Conversation

@Fiona2016

@Fiona2016 Fiona2016 commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Why

Event uploads share the uplink with the app's own requests. On a narrow
connection that competition is measurable, and two properties of the drain loop
decided how much it hurt — neither reachable from configuration:

  • The batch roll window was a fixed constant (MAX_BATCH_AGE_MS), so events
    could not be packed into fewer, better-compressed requests.
  • The drain loop had no bound at all: a successful upload called
    scheduleNext(0) and immediately claimed the next batch, so a backlog built up
    while offline was emptied in one uninterrupted burst.

Separately, HarmonyOS was the only platform expressing upload pacing as a
millisecond setter. Android, iOS, and Flutter all use the same three enums with
identical case names and values, so tuning advice had to be hand-translated for
this SDK alone.

What

Adopt the shared vocabulary and drop setBatchUploadFrequencyMs:

Setting Cases Default
setUploadFrequency FREQUENT 500 ms · AVERAGE 2 s · RARE 5 s AVERAGE
setBatchSize SMALL 3 s · MEDIUM 10 s · LARGE 35 s MEDIUM
setBatchProcessingLevel LOW 1 · MEDIUM 20 · HIGH 100 batches/cycle MEDIUM

Values and defaults match Android's DEFAULT_CORE_CONFIG exactly, so the same
setting produces the same behaviour on every platform. A test pins the enum
values against the other SDKs — silent drift there would invalidate
cross-platform tuning advice without failing anything else.

BREAKING CHANGE

setBatchUploadFrequencyMs is removed. Replace with
setUploadFrequency(UploadFrequency.RARE) to keep the previous 5 s behaviour, or
drop the call to take the new AVERAGE default. Customer-facing docs reference
it in 6 places and are updated with the release.

Notes for review

The per-cycle bound is enforced at every scheduling path, not just the timer.
triggerNow() re-enters the drain directly on each force-flushed error or crash
write and on backgrounding. A scheduler that paced only its own timer would drop
the bound under exactly the load that needs it — an error storm. A cycle that
spends its budget records cycleYieldUntilMs, and both triggerNow() and the
coalesced-retrigger path in drainOnce() floor their delay at it. The first
force-flush of a cycle with budget left is still immediate.

Deferred-upload pipeline fix. It built its FilePersistenceStrategy without
a batch window, so it used the module fallback rather than the configured one.
The cross-process claim guard is derived from that window: with the main process
on BatchSize.LARGE, the deferred process would have considered a batch
claimable while the main process was still appending to it. Both pipelines now
take the configured value, and the module fallback is defined as
BatchSize.MEDIUM so an omitted argument cannot silently change behaviour.

Range clamping is gone — an enum cannot carry an out-of-range value, so the
checks the millisecond setters needed had nothing left to guard.

flushForWork is deliberately left unbounded — the system already scheduled
that drain off the critical path under the app's chosen network/charging
constraints, so bursting there is free.

Verification

  • node scripts/unit-node/run.mjs — 143 passed, 0 failed (checked across
    repeated runs; the cycle tests are timer-based).
  • ./scripts/ci-check.sh — ALL CHECKS PASSED (HAR build, demo HAP build, ArkTS
    unit-test type check).
  • UploadThrottle.test.ets covers: enum values against the other platforms,
    defaults against Android's, the roll window driving the drain decision, the
    per-cycle bound, a control where the budget exceeds the backlog, and a
    force-flush pump that must not buy extra batches within a spent cycle.
  • Not verified on a physical device: the pacing behaviour is unit-tested against
    fakes, not measured against real uplink contention.

Event uploads share the uplink with the app's own requests. Two properties of
the drain loop decided how much they interfered with it, and neither was
reachable from configuration:

- The batch roll window was a fixed 5 s constant, so events could not be packed
  into fewer, better-compressed requests.
- The drain loop had no bound at all: one successful upload immediately claimed
  the next batch, so a backlog accumulated while offline went out as a single
  uninterrupted burst.

Add setBatchWindowMs and setMaxBatchesPerCycle to ConfigurationBuilder. These
mirror Android's BatchSize and BatchProcessingLevel, as continuous values rather
than enums, following setBatchUploadFrequencyMs which predates them. The
per-cycle bound defaults to 10, matching BatchProcessingLevel.MEDIUM.

Enforce the bound at every scheduling path, not only the timer. triggerNow()
re-enters the drain directly on each force-flushed error or crash write, so a
scheduler pacing only its own timer would drop the bound under exactly the load
that needs it. A cycle that spends its budget records cycleYieldUntilMs, and both
triggerNow() and the coalesced-retrigger path floor their delay at it. The first
force-flush of a cycle with budget left is still immediate, which is what the
call exists for.

batchWindowMs also gates the cross-process claim guard for batches created by
the deferred-upload process, so both processes must be built from the same
Configuration. Noted on the field and in the README.
The Android, iOS, and Flutter SDKs all express upload pacing through the same
three enums — UploadFrequency, BatchSize, BatchProcessingLevel — with identical
case names and identical values. HarmonyOS was the only platform that did not,
exposing a millisecond setter instead, so tuning advice written for one platform
had to be translated by hand for this one.

Adopt the shared vocabulary: add the three enums with the values the other SDKs
use, and drop setBatchUploadFrequencyMs. Defaults now match the Android
DEFAULT_CORE_CONFIG exactly (AVERAGE / MEDIUM / MEDIUM), so the same setting
produces the same behaviour on every platform.

This removes the range clamping the millisecond setters needed: an enum cannot
carry an out-of-range value, so the checks had nothing left to guard. A test
pins the enum values against the other SDKs, since silent drift there would
invalidate cross-platform tuning advice without failing anything else.

Also fixes the deferred-upload pipeline, which built its FilePersistenceStrategy
without a batch window and so used the module fallback rather than the
configured one. The cross-process claim guard is derived from that window: with
the main process on a longer window, the deferred process would consider a batch
claimable while the main process was still appending to it. Both pipelines now
take the configured value, and the module fallback is defined as the configured
default so an omitted argument cannot silently change behaviour.

BREAKING CHANGE: setBatchUploadFrequencyMs is removed. Replace with
setUploadFrequency(UploadFrequency.RARE) for the previous 5 s default, or drop
the call to take the new AVERAGE default.
@Fiona2016 Fiona2016 changed the title Bound upload bursts and make the batch window configurable Bound upload bursts and align batching config with the other platform SDKs Aug 18, 2026
- UploadScheduler: floor every scheduling path at the retry backoff via a
  single scheduleAt choke point. A force-flush (an error/crash write, a
  manual flush, or backgrounding) can no longer cancel an in-progress
  backoff and retry a failing intake at the app's error rate.
- Backgrounding and manual flushes waive the cycle budget: the process is
  about to freeze (a frozen process never fires the timer the budget would
  impose), or the app explicitly asked to ship now. The backoff is never
  waived.
- flushForWork and flushAndWait default to the configured
  BatchProcessingLevel instead of a hard-coded 20, so the configured level
  bounds the deferred-upload and flush drains too.
- FilePersistenceStrategy: the foreign-batch claim guard floors at the
  largest supported batch window, so independently configured processes
  can no longer claim a batch another process is still appending to.
- Keep the new public enums and their setters in the obfuscation keep
  lists so release builds resolve them.
- Document the removed setter, the migration path, and the two default
  changes in the changelog.
@Fiona2016
Fiona2016 merged commit cf2cd74 into main Aug 18, 2026
6 checks passed
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.

1 participant