feat: add Excache.Memory and kinds
This commit is contained in:
1 parent
bcd5503f80
commit
876662dba7
1 file changed
+188
@@ -0,0 +1,188 @@
|
|||||||
|
defmodule Excache.Memory do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
# Byte accounting for stored entries, the input every eviction policy needs.
|
||||||
|
#
|
||||||
|
# Structural cost comes from :erts_debug.size/1, which walks the term and
|
||||||
|
# counts machine words (shared subterms once). Two things it cannot see are
|
||||||
|
# added explicitly:
|
||||||
|
#
|
||||||
|
# * reference counted binaries. ERTS keeps binaries up to 64 bytes on the
|
||||||
|
# heap, so :erts_debug.size/1 covers those; anything larger lives
|
||||||
|
# off-heap and only its ProcBin header is counted, so the payload is
|
||||||
|
# added by walking the term.
|
||||||
|
#
|
||||||
|
# * the per-row cost of living in an ETS set. Measured against
|
||||||
|
# :ets.info(table, :memory) deltas on 64-bit OTP it is 41.24 bytes per
|
||||||
|
# row above the flat term size: five words of hash bucket bookkeeping
|
||||||
|
# plus an amortised bucket slot. Together with the five words of the
|
||||||
|
# {key, kind, value, expiresAt} tuple itself (header plus four slots)
|
||||||
|
# that is the ten word constant below; memory_test.exs asserts the
|
||||||
|
# resulting estimate against a real ETS insert.
|
||||||
|
|
||||||
|
alias Excache.SkipList
|
||||||
|
alias Excache.Store
|
||||||
|
alias Excache.Stream, as: CacheStream
|
||||||
|
|
||||||
|
@kinds [:string, :list, :hash, :set, :zset, :stream]
|
||||||
|
@entryOverheadWords 10
|
||||||
|
@heapBinaryLimit 64
|
||||||
|
|
||||||
|
def kinds, do: @kinds
|
||||||
|
|
||||||
|
def wordSize, do: :erlang.system_info(:wordsize)
|
||||||
|
|
||||||
|
def entryOverhead, do: @entryOverheadWords * wordSize()
|
||||||
|
|
||||||
|
def keyBytes(key) when is_binary(key), do: termBytes(key)
|
||||||
|
|
||||||
|
def estimateEntry(key, {kind, value, _expiresAt}) do
|
||||||
|
keyBytes(key) + estimateValue(kind, value) + entryOverhead()
|
||||||
|
end
|
||||||
|
|
||||||
|
def entryBytes({key, kind, value, expiresAt}),
|
||||||
|
do: estimateEntry(key, {kind, value, expiresAt})
|
||||||
|
|
||||||
|
def estimateValue(:string, value) when is_binary(value), do: termBytes(value)
|
||||||
|
def estimateValue(:list, value) when is_list(value), do: termBytes(value)
|
||||||
|
def estimateValue(:set, %MapSet{} = value), do: termBytes(value)
|
||||||
|
def estimateValue(:zset, %SkipList{} = value), do: termBytes(value)
|
||||||
|
def estimateValue(:stream, %CacheStream{} = value), do: termBytes(value)
|
||||||
|
def estimateValue(:hash, value) when is_map(value), do: termBytes(value)
|
||||||
|
|
||||||
|
def elementCount(:string, value) when is_binary(value), do: 1
|
||||||
|
def elementCount(:list, value), do: length(value)
|
||||||
|
def elementCount(:set, value), do: MapSet.size(value)
|
||||||
|
def elementCount(:zset, value), do: SkipList.size(value)
|
||||||
|
def elementCount(:stream, value), do: CacheStream.length(value)
|
||||||
|
def elementCount(:hash, value) when is_map(value), do: map_size(value)
|
||||||
|
|
||||||
|
def payloadBytes(:string, value), do: byte_size(value)
|
||||||
|
|
||||||
|
def payloadBytes(:list, value), do: sumBytes(value)
|
||||||
|
|
||||||
|
def payloadBytes(:set, value), do: value |> MapSet.to_list() |> sumBytes()
|
||||||
|
|
||||||
|
def payloadBytes(:zset, value) do
|
||||||
|
Enum.reduce(SkipList.to_list(value), 0, fn {_score, member}, total ->
|
||||||
|
total + byte_size(member) + wordSize()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def payloadBytes(:stream, value) do
|
||||||
|
Enum.reduce(CacheStream.toList(value), 0, fn {_id, fields}, total ->
|
||||||
|
total + 2 * wordSize() + sumBytes(fields)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def payloadBytes(:hash, value) when is_map(value) do
|
||||||
|
Enum.reduce(value, 0, fn {field, member}, total ->
|
||||||
|
total + byte_size(field) + byte_size(member)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def describe(key, {kind, value, expiresAt}) do
|
||||||
|
total = estimateEntry(key, {kind, value, expiresAt})
|
||||||
|
payload = payloadBytes(kind, value)
|
||||||
|
|
||||||
|
%{
|
||||||
|
key: key,
|
||||||
|
kind: kind,
|
||||||
|
bytes: total,
|
||||||
|
keyBytes: keyBytes(key),
|
||||||
|
valueBytes: estimateValue(kind, value),
|
||||||
|
payloadBytes: payload,
|
||||||
|
overheadBytes: total - payload,
|
||||||
|
elements: elementCount(kind, value),
|
||||||
|
volatile: expiresAt != nil
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def usage(key) do
|
||||||
|
case Store.watch_snapshot(key) do
|
||||||
|
:miss -> nil
|
||||||
|
entry -> estimateEntry(key, entry)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def describeKey(key) do
|
||||||
|
case Store.watch_snapshot(key) do
|
||||||
|
:miss -> nil
|
||||||
|
entry -> describe(key, entry)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def usedMemory, do: usedMemory(Store.dump())
|
||||||
|
|
||||||
|
def usedMemory(entries) when is_list(entries),
|
||||||
|
do: Enum.reduce(entries, 0, fn entry, total -> total + entryBytes(entry) end)
|
||||||
|
|
||||||
|
def usageByType, do: usageByType(Store.dump())
|
||||||
|
|
||||||
|
def usageByType(entries) when is_list(entries) do
|
||||||
|
empty = Map.new(@kinds, &{&1, %{keys: 0, bytes: 0}})
|
||||||
|
|
||||||
|
Enum.reduce(entries, empty, fn {_key, kind, _value, _expiresAt} = entry, acc ->
|
||||||
|
Map.update!(acc, kind, fn %{keys: keys, bytes: bytes} ->
|
||||||
|
%{keys: keys + 1, bytes: bytes + entryBytes(entry)}
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stats, do: stats(Store.dump())
|
||||||
|
|
||||||
|
def stats(entries) when is_list(entries) do
|
||||||
|
keys = length(entries)
|
||||||
|
used = usedMemory(entries)
|
||||||
|
|
||||||
|
overhead =
|
||||||
|
Enum.reduce(entries, keys * entryOverhead(), fn {key, _kind, _value, _expiresAt}, total ->
|
||||||
|
total + keyBytes(key)
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{
|
||||||
|
keys: keys,
|
||||||
|
usedMemory: used,
|
||||||
|
overheadBytes: overhead,
|
||||||
|
datasetBytes: used - overhead,
|
||||||
|
bytesPerKey: if(keys == 0, do: 0, else: div(used, keys)),
|
||||||
|
volatileKeys:
|
||||||
|
Enum.count(entries, fn {_key, _kind, _value, expiresAt} -> expiresAt != nil end),
|
||||||
|
byType: usageByType(entries),
|
||||||
|
largestKey: largestKey(entries)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp largestKey([]), do: nil
|
||||||
|
|
||||||
|
defp largestKey(entries) do
|
||||||
|
entries
|
||||||
|
|> Enum.map(fn {key, _kind, _value, _expiresAt} = entry -> {key, entryBytes(entry)} end)
|
||||||
|
|> Enum.max_by(fn {_key, bytes} -> bytes end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp termBytes(term), do: :erts_debug.size(term) * wordSize() + refcBytes(term)
|
||||||
|
|
||||||
|
defp sumBytes(binaries),
|
||||||
|
do: Enum.reduce(binaries, 0, fn binary, total -> total + byte_size(binary) end)
|
||||||
|
|
||||||
|
defp refcBytes(term) when is_binary(term) do
|
||||||
|
size = byte_size(term)
|
||||||
|
if size > @heapBinaryLimit, do: size, else: 0
|
||||||
|
end
|
||||||
|
|
||||||
|
defp refcBytes(term) when is_list(term),
|
||||||
|
do: Enum.reduce(term, 0, fn element, total -> total + refcBytes(element) end)
|
||||||
|
|
||||||
|
defp refcBytes(%_struct{} = term), do: term |> Map.from_struct() |> refcBytes()
|
||||||
|
|
||||||
|
defp refcBytes(term) when is_map(term) do
|
||||||
|
Enum.reduce(term, 0, fn {mapKey, mapValue}, total ->
|
||||||
|
total + refcBytes(mapKey) + refcBytes(mapValue)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp refcBytes(term) when is_tuple(term), do: term |> Tuple.to_list() |> refcBytes()
|
||||||
|
|
||||||
|
defp refcBytes(_term), do: 0
|
||||||
|
end
|
||||||
Reference in new issue
Block a user