defmodule GitGud.Packages.DiskUsage do
@moduledoc """
Where the registry's disk went.
Every number here is one aggregate query rather than a walk: an
instance with a few hundred thousand files should be able to render
the admin page without thinking about it.
`SUM` over a bigint column is `numeric` in Postgres, which Ecto loads
as a `Decimal` — so every sum here is cast back to an integer in the
query rather than leaving each caller to discover that the hard way.
Sizes come from `package_files.size` where a format has files, and
from `package_versions.size` where it doesn't — OCI's bytes live in
zot, but they are still bytes this instance is answerable for, so
they are counted and labelled rather than left out of the total.
"""
import Ecto.Query, warn: false
alias GitGud.Organizations.Organization
alias GitGud.Packages.Package
alias GitGud.Packages.PackageFile
alias GitGud.Packages.PackageVersion
alias GitGud.Repo
alias GitGud.Repositories.Repository
@typedoc "Bytes and counts for one grouping."
@type row :: %{
key: String.t() | integer(),
label: String.t(),
bytes: non_neg_integer(),
versions: non_neg_integer(),
packages: non_neg_integer()
}
@doc """
Instance-wide totals, one row per format, largest first.
The shape every view starts from: what is on this disk, and which
ecosystem put it there.
"""
@spec by_format(keyword()) :: [row()]
def by_format(opts \\ []) do
base()
|> scope(opts)
|> group_by([p], p.format)
|> select([p, v, f], %{
key: p.format,
bytes: type(coalesce(sum(coalesce(f.size, v.size)), 0), :integer),
versions: count(v.id, :distinct),
packages: count(p.id, :distinct)
})
|> Repo.all()
|> Enum.map(&Map.put(&1, :label, &1.key))
|> Enum.sort_by(& &1.bytes, :desc)
end
@doc "One row per repository, largest first. Scoped like `by_format/1`."
@spec by_repository(keyword()) :: [row()]
def by_repository(opts \\ []) do
base()
|> scope(opts)
|> join(:inner, [p], r in Repository, on: r.id == p.repository_id)
|> group_by([p, v, f, r], [r.id, r.name])
|> select([p, v, f, r], %{
key: r.id,
label: r.name,
bytes: type(coalesce(sum(coalesce(f.size, v.size)), 0), :integer),
versions: count(v.id, :distinct),
packages: count(p.id, :distinct)
})
|> Repo.all()
|> Enum.sort_by(& &1.bytes, :desc)
end
@doc """
The biggest packages, largest first — what to look at when the
number at the top of the page is a surprise.
"""
@spec top_packages(keyword()) :: [map()]
def top_packages(opts \\ []) do
limit = Keyword.get(opts, :limit, 20)
base()
|> scope(opts)
|> join(:inner, [p], r in Repository, on: r.id == p.repository_id)
|> group_by([p, v, f, r], [p.id, p.name, p.format, r.id, r.name])
|> select([p, v, f, r], %{
package_id: p.id,
name: p.name,
format: p.format,
repository_id: r.id,
repository: r.name,
bytes: type(coalesce(sum(coalesce(f.size, v.size)), 0), :integer),
versions: count(v.id, :distinct)
})
|> order_by([p, v, f], desc: coalesce(sum(coalesce(f.size, v.size)), 0))
|> limit(^limit)
|> Repo.all()
end
@doc "One number: everything in scope."
@spec total(keyword()) :: %{
bytes: non_neg_integer(),
packages: non_neg_integer(),
versions: non_neg_integer()
}
def total(opts \\ []) do
base()
|> scope(opts)
|> select([p, v, f], %{
bytes: type(coalesce(sum(coalesce(f.size, v.size)), 0), :integer),
versions: count(v.id, :distinct),
packages: count(p.id, :distinct)
})
|> Repo.one()
end
# Files left-joined so a version with no file rows (OCI) still counts
# once, at the version's own size.
defp base do
from(p in Package,
left_join: v in PackageVersion,
on: v.package_id == p.id,
left_join: f in PackageFile,
on: f.package_version_id == v.id
)
end
defp scope(query, opts) do
Enum.reduce(opts, query, fn
{:repository, %Repository{id: id}}, q -> where(q, [p], p.repository_id == ^id)
{:repository_id, id}, q -> where(q, [p], p.repository_id == ^id)
{:organization, %Organization{id: id}}, q -> scope_org(q, id)
{:organization_id, id}, q -> scope_org(q, id)
{:format, format}, q -> where(q, [p], p.format == ^format)
_, q -> q
end)
end
defp scope_org(query, org_id) do
repos = from(r in Repository, where: r.organization_id == ^org_id, select: r.id)
where(query, [p], p.repository_id in subquery(repos))
end
@doc """
Human bytes. Binary units, because that is what a disk is measured
in and what every other size in this app uses.
"""
@spec humanize(integer() | nil) :: String.t()
def humanize(nil), do: "0 B"
def humanize(bytes) when bytes < 1024, do: "#{bytes} B"
def humanize(bytes) when bytes < 1_048_576, do: "#{Float.round(bytes / 1024, 1)} KiB"
def humanize(bytes) when bytes < 1_073_741_824, do: "#{Float.round(bytes / 1_048_576, 1)} MiB"
def humanize(bytes), do: "#{Float.round(bytes / 1_073_741_824, 2)} GiB"
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
5.1 KiB · text
History
b868907