Skip to content

feat(db): a compact seforim.db, 2.5x smaller and faster to read on Android - #147

Draft
kdroidFilter wants to merge 2 commits into
feat/kmp-searchfrom
feat/compact-db
Draft

kdroidFilter wants to merge 2 commits into
feat/kmp-searchfrom
feat/compact-db

Conversation

@kdroidFilter

Copy link
Copy Markdown
Owner

The shipped seforim.db is no longer the generator's: :packaging:compactDb rewrites it in about 70 s, 8,022 MB → 3,253 MB (2.47x), and it reads faster on Android. Details and measurements in COMPACT_DB.md.

What changes

  • Line texts compressed (LineTextCodec, dao, common Kotlin). Each Hebrew letter, nikud or taam takes one byte instead of two, then LZ77 (LZ4 block format) compresses against a 60 KiB dictionary of the corpus: 3.1x. Decoding is copies only, with no JNI, no zlib and no platform code. content is BLOB AS String behind SQLDelight's adapter, so the queries don't change and plain-text databases (the generator's, older ones) still read.
  • Fixed dictionary: stored in line_codec_dict and in the packaging's resources, so deltas between compact releases stay valid. The logical hash covers it, and LuceneUpdater decodes the texts it reindexes.
  • Indexes: idx_line_book_index covers charCount, so a book's scrollbar no longer reads all its texts. idx_line_toc is partial. The unused idx_line_heref, idx_link_source_line, idx_link_type and idx_toc_text are dropped.
  • Pages: 16 KiB, written in order and full. No ANALYZE.
  • SQLite memory per platform (SqliteTuning): Android gets a 16 MB cache and no mmap; the desktop is unchanged.
  • PatchDbSchema.CURRENT_VERSION = 2: a client with a v1 database takes the full bundle.
  • Release workflow: compacts the database before packaging it and producing the patch fan.

Results (cold / warm, ms)

Android 10 (emulator) simulated entry-level phone
open 150 books 407–480 / 221–262 → 213–223 / 151–171 5,025 / 4,678 → 473 / 58
jump to 300 random pages 260–268 / 142–144 → 199–204 / 124–131 814 / 102 → 476 / 130
scroll 20 books 296–301 / 250–255 → 271–283 / 218–229 824 / 140 → 340 / 78
3,000 random lines 580–626 / 240–252 → 512–517 / 249–258 1,559 / 78 → 1,463 / 123
commentaries 124–178 / 58–60 → 85–87 / 54 200 / 9 → 152 / 9

What got slower:

  • Simulated phone: warm random lines (78 → 123 ms).
  • Desktop: commentaries cold (+6 ms) and scrolling warm (+14 ms).

Verified

  • All 7,019,636 lines are identical to the generator's (CompactDbVerification, plus an independent decoder on a sample).
  • A delta between two compact releases (20 changed lines) carries 20 upserts (150 KB), and its apply reproduces the new release's hash.
  • dao compiles for iOS (iosArm64, iosSimulatorArm64). Not run on iOS.

Before shipping

  • An app version without LineTextCodec can't read a compact database, so the release needs a minimum app version.
  • The release workflow step has not run on CI yet.

The shipped database is no longer the generator's: :packaging:compactDb rewrites it (8,022 MB to 3,382 MB on the
current corpus), every line reading back byte for byte.

- dao: line.content is BLOB AS String through LineTextCodec: raw deflate with a dictionary of the corpus' own text
  (3.1x on lines of 750 bytes, where deflate alone gets 2.5x), behind a two-byte header; plain UTF-8 otherwise (what
  the generator and older databases store). java.util.zip on the JVM and Android, the system's zlib on iOS.
- The dictionary lives in line_codec_dict and in the packaging's resources, the same for every release, so deltas
  carry texts that read with the client's; the logical hash covers it, LuceneUpdater decodes the texts it reindexes.
- Indexes: idx_line_book_index covers charCount (a book's scrollbar no longer reads its texts), idx_line_toc is
  partial, and the unused ones go (idx_line_heref, idx_link_source_line, idx_link_type, idx_toc_text).
- Pages written once and full, 4 KiB; SQLite's cache and map sized per platform (Android: 16 MB, no map).
- PatchDbSchema.CURRENT_VERSION 2: clients with a version 1 database take the full bundle.
- The release workflow compacts the database before packaging it and producing the patch fan.
- DbReadWorkload, run on the desktop (DbReadBenchmark) and on Android (AndroidDbReadBenchmark): 2 to 12x fewer bytes
  read, opening a book 1.5 to 2x faster cold, a line about 5 us more to decode on ART (COMPACT_DB.md).

Signed-off-by: Elie Gambache <elyahou.hadass@gmail.com>
…t database reads faster than the old one

Deflate cost about 5 us a line on ART (JNI, the dictionary's install, the inflate, the UTF-8): scrolling read slower
than plain text. The texts are now one byte per Hebrew letter, nikud or taam, compressed by LZ77 against 60 KiB of
the corpus (LZ4's block format): 3.1x, as much as deflate, decoded by copies only, in common Kotlin (no zlib, no
platform code; dao compiles for iOS on Linux).

- 16 KiB pages (the pageSize option was lost before: PRAGMA page_size now holds through a VACUUM): a page of the
  reader is one read on flash. 8,022 MB to 3,253 MB (2.47x), compacted in 70 s instead of 250.
- Android 10 (emulator), cold: opening books 2x faster, jumps 1.3x, scrolling 1.1x, random lines 1.2x,
  commentaries 1.5x; warm as fast or faster. A simulated entry-level phone (128 MB page cache, eMMC latency): opening
  books 10x faster, scrolling 2.4x.
- CompactDbVerification checks every line against the generator's (7,019,636, no difference); a delta between two
  compact releases carries only the changed lines and verifies.
- The benchmark adds a sequential scroll; the old phase is a jump to random books.

Signed-off-by: Elie Gambache <elyahou.hadass@gmail.com>
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