Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Why Akavache is Fast: How not to use sqlite3

Why Akavache is Fast: How not to use sqlite3

Talk given at Xamarin Evolve 2016 - https://www.youtube.com/watch?v=j7WnQhwBwqA

Ana Betts

May 09, 2016
Tweet

More Decks by Ana Betts

Other Decks in Programming

Transcript

  1. What can we do with Byte Dictionaries? • Arbitrary objects

    via JSON.NET • Fetching and loading Images and URLs from the Internet • Storing and automatically encrypting User Credentials
  2. Akavache uses SQLite3 to store data sqlite> .schema CREATE TABLE

    "CacheElement"( "Key" varchar primary key not null , "TypeName" varchar , "Value" blob , "Expiration" bigint , "CreatedAt" bigint );
  3. Akavache uses SQLite3 to store its data • Users anecdotally

    report that once they move to Akavache, they get a huge app speedup vs SQLite3 • Yet, Akavache is built on SQLite3
  4. SQLite3 is fast when you: • Use transactions properly •

    Access SQLite3 on a single thread • Prepare commands ahead-of-time
  5. SQLite3 is not fast when you: • Just issue commands

    (this means every command is a transaction) • Call into SQLite3 from a bunch of background threads at the same time
  6. Accidentally using SQLite wrong is easy 1. Load Messages 2.

    Every message instantiates a MessageCellViewModel 3. MessageCellViewModel downloads avatar 4. Avatar gets saved in cache
  7. Make SQLite3 faster, the easy way // NB: Setting journal_mode

    returns a row, nfi Connection.ExecuteScalar<int>("PRAGMA journal_mode=WAL"); Connection.Execute("PRAGMA temp_store=MEMORY"); Connection.Execute("PRAGMA synchronous=OFF");
  8. Make sure we prepare statements • Key-Value stores have pretty

    predictable query needs • Create them all on startup • Make sure to destroy them or you'll have problems!
  9. Make sure we prepare statements var result = (SQLite3.Result)raw.sqlite3_prepare_v2( conn.Handle,

    "INSERT OR REPLACE INTO CacheElement VALUES (?,?,?,?,?)", out insertOp);
  10. Make sure we prepare statements foreach (var v in insertList)

    { try { this.Checked(raw.sqlite3_bind_text(insertOp, 1, v.Key)); if (String.IsNullOrWhiteSpace(v.TypeName)) { this.Checked(raw.sqlite3_bind_null(insertOp, 2)); } else { this.Checked(raw.sqlite3_bind_text(insertOp, 2, v.TypeName)); } this.Checked(raw.sqlite3_bind_blob(insertOp, 3, v.Value)); this.Checked(raw.sqlite3_bind_int64(insertOp, 4, v.Expiration.Ticks)); this.Checked(raw.sqlite3_bind_int64(insertOp, 5, v.CreatedAt.Ticks)); this.Checked(raw.sqlite3_step(insertOp)); } finally { this.Checked(raw.sqlite3_reset(insertOp)); } }
  11. First, Akavache added Bulk Operations • Bulk operations let us

    insert / lookup multiple keys at once • We can insert all of the keys in a single transaction • If one fails, we have to fail them all in a single transaction
  12. Use a Queue! • Every Akavache cache gets a single

    dedicated worker thread • That thread is the only thread to touch SQLite3 and processes items off the Queue • The queue is a list of "Operation to run + TaskCompletionSource to notify" • Bingo Bango, no concurrency problems
  13. The Power of TaskCompletionSource • Tasks don't have to have

    an associated thread running • Tasks != Threads! • They're really just a box for a future value
  14. The Power of TaskCompletionSource var tcs = new TaskCompletionSource<bool>(); return

    tcs.Task; // Later in another part of town... tcs.TrySetValue(true); // Or tcs.TrySetException(new Exception("Didn't Work!"));
  15. Use a (Blocking) Queue! 1. Try to fetch a single

    item and block if there is none 2. If we succeed, try to non-blocking fetch 31 more items at most 3. Create a transaction 4. Run all 32 commands 5. Drop the transaction, complete the associated Tasks
  16. Operation Coalescing Gone Wrong Write “foo” Failed Read “foo” Delete

    “foo” This Read should have worked, but incorrect coalescing made it fail!
  17. What Akavache actually does 1. Write “Bar” 2. Read “Foo”

    3. Read “Bar” 4. Read “Foo” 5. Read “Baz” 6. Delete “Bamf” { Bar: [Write Bar, Read Bar], Foo: [Read Foo, Read Foo], Baz: [Read Baz], Bamf: [Invalidate Bamf] } Group by the key, but maintain original order
  18. What Akavache actually does { Bar: [Write Bar, Read Bar],

    Foo: [Read Foo, Read Foo], Baz: [Read Baz], Bamf: [Invalidate Bamf] } { Bar: [Write Bar, Read Bar], Foo: [Read Foo, Read Foo], Baz: [Read Baz], Bamf: [Invalidate Bamf] } Dedupe operations that do the same thing in a row
  19. What Akavache actually does { Bar: [Write Bar, Read Bar],

    Foo: [Read Foo], Baz: [Read Baz], Bamf: [Invalidate Bamf] } 1. Read [Foo, Baz] 2. Write [Bar] 3. Invalidate [Bamf] Remove the First item in every list, and try to smush them together
  20. What Akavache actually does { Bar: [Read Bar], Foo: [],

    Baz: [], Bamf: [] } 1. Read [Bar] Keep doing this, until we run out of items
  21. Final Result 1. Write "Bar" 2. Read "Foo" 3. Read

    "Bar" 4. Read "Foo" 5. Read "Baz" 6. Invalidate "Bamf" 1. Read [Foo, Baz] 2. Write [Bar] 3. Invalidate [Bamf] 4. Read [Bar] Six commands down to four, not bad!