Skip to content

docs(core): document row_group_max_size in-memory buffering for streaming/mobile - #82

Closed
sundapeng wants to merge 1 commit into
apache:mainfrom
sundapeng:docs/writer-row-group-buffering-memory
Closed

docs(core): document row_group_max_size in-memory buffering for streaming/mobile#82
sundapeng wants to merge 1 commit into
apache:mainfrom
sundapeng:docs/writer-row-group-buffering-memory

Conversation

@sundapeng

Copy link
Copy Markdown
Member

Problem

A customer reported apparent "high CPU + memory leak" when writing sensor data to mosaic on Android (ARMv8-A, Android 16), via the FFI/C++ writer. Process RSS climbed steadily through a long write session and never came back down, eventually risking OOM on the device.

Root cause: buffering, not a leak

This is not a memory leak. WriterOptions::row_group_max_size defaults to 256 MB (DEFAULT_ROW_GROUP_MAX_SIZE). A writer buffers every row of the in-progress row group in memory and flushes (compresses + writes) only once that much data has accumulated:

// core/src/writer.rs
self.current_buffered_size += size;
if self.current_buffered_size >= self.row_group_max_size {
    self.flush_row_group()?;
}

For a long-lived streaming writer — continuous ingestion, mobile/embedded sensors, anything that writes small batches over a long session — this default holds an unbounded amount of data in RAM:

  • RSS grows roughly 1:1 with the buffered rows until the 256 MB cap is reached.
  • A session shorter than the cap flushes nothing until close(), so the entire session lives in memory.

On a phone this looks and behaves like a leak and can trigger OOM. The high-CPU observation is the sustained Arrow-FFI import + bucket append, plus one giant zstd burst when the cap (or close()) is finally hit — an ANR risk if the write runs on the UI thread.

Reproduction (Android 16, API 36 emulator, ARMv8-A target)

Two runs of the same workload (3000 batches × 1024 rows = 3,072,000 rows, 10 motion columns + device_id), one with the 256 MB default, one with an 8 MB cap:

256 MB default (customer condition) 8 MB cap
peak RSS 134 → 330 MB (unbounded growth) 156 → 174 MB (plateaus, then drops)
peak native heap 15 → 278 MB 17 → 31 MB (8.9× lower)
row groups 1 (one giant flush at close) 23 (~134k rows each)
throughput 503 batch/s + giant zstd burst at close 1409 batch/s, flushes spread out
verify PASS, 3,072,000 rows PASS, 3,072,000 rows

Both files read back correctly with the upstream mosaic CLI (schema / count / cat). The "leak" disappears entirely with a smaller cap: memory is bounded and the native heap stays flat at ~31 MB. macOS leaks (host harness) independently reported 0 leaked bytes — the write/close path frees correctly; the RSS growth is by-design buffering.

The 16 KB page-alignment requirement of Android 15+/16 was ruled out (all 4 ABI .so are 0x4000-aligned). The behavior is OS-independent — it is the Rust core's buffering policy.

Fix: documentation + recommendation (default unchanged)

The 256 MB default is intentional for batch/server wide-table workloads, where a large row group maximizes compression ratio and amortizes seek cost. Changing the global default would risk regressing that primary use case, so this PR leaves it unchanged and instead documents the tradeoff and recommends a smaller row_group_max_size (e.g. 8 MB) for long-lived streaming/mobile writers:

  • core/src/spec.rs — rustdoc on DEFAULT_ROW_GROUP_MAX_SIZE
  • core/src/writer.rs — rustdoc on WriterOptions + the row_group_max_size field
  • include/mosaic.hpp — inline comment on the C++ WriterOptions::row_group_max_size
  • docs/cpp-api.html, docs/java-api.html, docs/python-api.html — table cells + "Memory & long-lived writers" warning callouts with per-language examples

No behavior change, no default change — docs only.

Alternatives considered

  • Lower the global default (e.g. to 8 MB): rejected — would regress compression ratio / row-group count for batch/server workloads, and is a behavior change that warrants broader consensus. Could be revisited as a separate discussion if mobile/streaming becomes a primary target.
  • Add a streaming preset / builder helper: possible follow-up, but additive API surface is out of scope for a docs-first fix.

🤖 Generated with Claude Code

A customer reported apparent "high CPU + memory leak" when writing sensor
data to mosaic on Android (ARMv8-A, Android 16). Root cause is not a leak:
WriterOptions.row_group_max_size defaults to 256 MB (DEFAULT_ROW_GROUP_MAX_SIZE),
so a writer buffers every row of the in-progress row group in memory and flushes
only once 256 MB has accumulated. For a long-lived streaming writer (continuous
ingestion, mobile/embedded sensors) this holds an unbounded amount of data in
RAM — RSS grows ~1:1 with buffered rows until the cap, and a session shorter
than the cap flushes nothing until close(), so the entire session lives in
memory. On a phone this looks and behaves like a leak and risks OOM.

Reproduced on an Android 16 (API 36) emulator with the FFI/C++ writer:
  - 256 MB default: RSS 134 -> 330 MB, native heap 15 -> 278 MB, 1 row group
  - 8 MB cap:       RSS plateaus ~174 MB, native heap ~31 MB, 23 row groups
  Both files verify PASS and read back correctly with the `mosaic` CLI.

The 256 MB default is intentional for batch/server wide-table workloads
(large row groups maximize compression ratio and amortize seek cost), so this
does not change the default. It documents the tradeoff and recommends a smaller
row_group_max_size (e.g. 8 MB) for long-lived streaming/mobile writers in:
  - core rustdoc (DEFAULT_ROW_GROUP_MAX_SIZE, WriterOptions + field)
  - include/mosaic.hpp (C++ WriterOptions inline comment)
  - docs/{cpp,java,python}-api.html (table cells + warning callouts)

Co-Authored-By: Claude Code <noreply@anthropic.com>
@sundapeng

Copy link
Copy Markdown
Member Author

Closing this. On closer review this isn't a defect in mosaic — the 256 MB row_group_max_size default is intentional (large row groups maximize compression ratio and amortize seek cost for batch/server wide-table workloads), and the in-memory buffering it implies is by design. The customer report traced to using that batch-tuned default for a long-lived streaming/mobile writer without overriding it, i.e. a usage/tuning issue on the app side, not a core bug. Apologies for the noise; no upstream change is warranted here.

@sundapeng sundapeng closed this Sep 2, 2026
@sundapeng
sundapeng deleted the docs/writer-row-group-buffering-memory branch September 2, 2026 18:49
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