Ecto_sqlite3 - an Ecto3 SQLite3 adapter

OK, so the memory usage on the 512 MB system was expected behaviour due to the cache setting. However, from my testing, there is a potential memory leak which I haven’t tracked down how to fix quite yet. But I’m going to put it into production anyway which a reduced cache_size as a mitigation.

Performance

Tested against:

Generated random queries on full-text search that I knew would hit based on bi-gram + wildcard “ba*”, etc.


CPU Information: Intel(R) Core(TM) i5-3320M CPU @ 2.60GHz
Number of Available Cores: 4
Available memory: 7.48 GB
Elixir 1.11.4
Erlang 23.2.1

Name                   ips        average  deviation         median         99th %
exsqlite             86.12       11.61 ms    ±85.74%        9.51 ms       49.56 ms
raw_sqlite           83.64       11.96 ms    ±89.03%        9.28 ms       51.83 ms
ecto_sqlite3         43.93       22.76 ms    ±74.73%       19.87 ms       72.09 ms

Comparison: 
exsqlite             86.12
raw_sqlite           83.64 - 1.03x slower +0.34 ms
ecto_sqlite3         43.93 - 1.96x slower +11.15 ms

The difference between raw_sqlite and exsqlite is due to the using a prepared statement for exqlite instead of a raw statement.

Memory

When tested from IEX, all the sqlite nifs used heap memory space (eheap_alloc). The size of the heap memory used is based on the size of the result of the query and was allocated against iex.

When tested with benchee, for ecto, I couldn’t find the memory being allocated in observer at all. eheap_alloc stayed 13MB the entire time, but resident memory shot up to 500-600MB. Running :erlang.garbage_collect did nothing. The resident memory remained consistent over multiple benchee remained consistent at around 590MB.

But I couldn’t find it. To test further, I killed all Repo for sqlite and the processes “Elixir.DBConnection.Connection:init/1” were all re-spawned. Ran garbage collection again, and nothing was collected. Re-ran benchee and the resident memory doubled to 1GB.

Conclusion

The behaviour I was seeing about consistent memory growth on low-memory systems was not a bug - just 10 workers each consuming 64 MB of cache each. I suggest you put that into the documentation, otherwise people on their raspberry pi’s might have a bad time.

However, memory may leak in other ways (unless I went bug-eyed and lost my mind during the testing process). The exqlite process sets the default cache_size: -64000. In some cases, this memory does not seem to be allocated to any erlang process and then can’t be garbage collected if the process is killed. Shrink_memory, if we could call it, might be able to reclaim it. Or potentially some other sqlite setting where we can ensure that the sqlite cache is on the erlang process heap and not somehow allocated somewhere else. This can be mitigated by reducing the cache_size also.

4 Likes