Packages
electric
1.4.7
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.16
1.4.16-beta-1
1.4.15
1.4.14
1.4.13
1.4.12
1.4.11
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
retired
1.1.4
retired
1.1.3
retired
1.1.2
1.1.1
1.1.0
1.0.24
1.0.23
1.0.22
1.0.21
1.0.20
1.0.19
1.0.18
1.0.17
1.0.15
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-beta.23
1.0.0-beta.22
1.0.0-beta.20
1.0.0-beta.19
1.0.0-beta.18
1.0.0-beta.17
1.0.0-beta.16
1.0.0-beta.15
1.0.0-beta.14
1.0.0-beta.13
1.0.0-beta.12
1.0.0-beta.11
1.0.0-beta.10
1.0.0-beta.9
1.0.0-beta.8
1.0.0-beta.7
1.0.0-beta.6
1.0.0-beta.5
1.0.0-beta.4
1.0.0-beta.3
1.0.0-beta.2
1.0.0-beta.1
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.5.2
0.4.4
Postgres sync engine. Sync little subsets of your Postgres data into local apps and services.
Current section
Files
Jump to
Current section
Files
lib/electric/shape_cache/shape_status/shape_db/statistics.ex
defmodule Electric.ShapeCache.ShapeStatus.ShapeDb.Statistics do
@moduledoc """
Uses SQLite's built-in statistics to report memory usage.
https://www.sqlite.org/draft/c3ref/c_status_malloc_count.html
https://www.sqlite.org/draft/c3ref/c_dbstatus_options.html
The `sqlite_memstat` table is part of the `memstat` loadable extension,
provided by the `ExSqlean` package.
Note that the reported values are per-db, not per-connection. There are
per-connection stats included in the query result but they're not included in
the export.
"""
use GenServer
alias Electric.ShapeCache.ShapeStatus.ShapeDb
@measurement_period 60_000
def name(stack_ref) do
Electric.ProcessRegistry.name(stack_ref, __MODULE__)
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: name(args))
end
def current(stack_id) do
GenServer.call(name(stack_id), :statistics)
end
@impl GenServer
def init(args) do
stack_id = Keyword.fetch!(args, :stack_id)
Process.set_label({:shape_db_statistics, stack_id})
Logger.metadata(stack_id: stack_id)
Electric.Telemetry.Sentry.set_tags_context(stack_id: stack_id)
{:ok, %{stack_id: stack_id, page_size: 0, stats: %{}}, {:continue, :initialize_stats}}
end
@impl GenServer
def handle_continue(:initialize_stats, state) do
%{stack_id: stack_id} = state
{:ok, [page_size]} =
ShapeDb.Connection.checkout_write!(stack_id, :read_stats, fn %{conn: conn} ->
with :ok <- ShapeDb.Connection.enable_extension(conn, "memstat") do
ShapeDb.Connection.fetch_one(conn, "PRAGMA page_size", [])
end
end)
{:noreply, read_stats(%{state | page_size: page_size}), :hibernate}
end
@impl GenServer
def handle_info(:read_stats, state) do
{:noreply, read_stats(state), :hibernate}
end
@impl GenServer
def handle_call(:statistics, _from, state) do
{:reply, {:ok, state.stats}, state}
end
defp read_stats(%{stack_id: stack_id} = state) do
{:ok, stats} =
ShapeDb.Connection.checkout_write!(stack_id, :read_stats, fn %{conn: conn} ->
ShapeDb.Connection.fetch_all(
conn,
"""
SELECT '__dbstat__', sum(pgsize), sum(unused) FROM dbstat WHERE aggregate = TRUE
UNION ALL
SELECT name, hiwtr, value FROM sqlite_memstat
""",
[]
)
end)
Process.send_after(self(), :read_stats, @measurement_period)
%{state | stats: analyze_stats(stats, state.page_size)}
end
defp analyze_stats(stats, page_size) do
stats
|> Enum.reduce(%{}, &add_stat(&1, &2, page_size))
|> then(fn stats ->
%{
memory_used: memory_used,
pagecache_used: pagecache_used,
pagecache_overflow: pagecache_overflow,
disk_size: disk_size,
data_size: data_size
} = stats
# 1. MEMORY_USED (primary metric): This is the main memory counter but
# excludes pre-configured page cache memory.
#
# 2. PAGECACHE_USED × page_size (if using SQLITE_CONFIG_PAGECACHE): If
# you configured a page cache via SQLITE_CONFIG_PAGECACHE, this tracks
# pages used from that pool. Multiply by PRAGMA page_size to get
# bytes.
#
# 3. PAGECACHE_OVERFLOW (heap fallback): When the pre-allocated page
# cache is full, overflow goes to heap. This is already in bytes.
%{
total_memory: memory_used + pagecache_used + pagecache_overflow,
page_cache_overflow: pagecache_overflow,
disk_size: disk_size,
data_size: data_size
}
end)
end
# This parameter is the current amount of memory checked out using
# sqlite3_malloc(), either directly or indirectly. The figure includes calls
# made to sqlite3_malloc() by the application and internal memory usage by
# the SQLite library. Auxiliary page-cache memory controlled by
# SQLITE_CONFIG_PAGECACHE is not included in this parameter. The amount
# returned is the sum of the allocation sizes as reported by the xSize method
# in sqlite3_mem_methods.
defp add_stat(["MEMORY_USED", _high, value], acc, _page_size) do
Map.put(acc, :memory_used, value)
end
# This parameter returns the number of pages used out of the pagecache memory
# allocator that was configured using SQLITE_CONFIG_PAGECACHE. The value
# returned is in pages, not in bytes.
defp add_stat(["PAGECACHE_USED", _high, value], acc, page_size) do
Map.put(acc, :pagecache_used, value * page_size)
end
# This parameter returns the number of bytes of page cache allocation which
# could not be satisfied by the SQLITE_CONFIG_PAGECACHE buffer and where
# forced to overflow to sqlite3_malloc().
defp add_stat(["PAGECACHE_OVERFLOW", _high, value], acc, _page_size) do
Map.put(acc, :pagecache_overflow, value)
end
# The DBSTAT virtual table is a read-only eponymous virtual table that
# returns information about the amount of disk space used to store the
# content of an SQLite database
# - `pgsize` - Total storage space used by the current page or btree
# - `unused` - Unused bytes of on the current page or btree
defp add_stat(["__dbstat__", pgsize, unused], acc, _page_size) do
acc
|> Map.put(:disk_size, pgsize)
|> Map.put(:data_size, pgsize - unused)
end
defp add_stat(_, acc, _page_size) do
acc
end
end