Skip to content

Commit e59f666

Browse files
committed
Keep asciicast stats (total views, popularity) in a separate table
1 parent fa7c7f2 commit e59f666

8 files changed

Lines changed: 297 additions & 84 deletions

File tree

lib/asciinema/recordings.ex

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule Asciinema.Recordings do
99

1010
alias Asciinema.Recordings.{
1111
Asciicast,
12+
AsciicastStats,
1213
Markers,
1314
Paths,
1415
Text
@@ -24,21 +25,21 @@ defmodule Asciinema.Recordings do
2425
def get_asciicast(id) do
2526
Asciicast
2627
|> Repo.get(id)
27-
|> Repo.preload(:user)
28+
|> Repo.preload([:user, :stats])
2829
end
2930

3031
def get_public_asciicast(id) do
3132
Asciicast
3233
|> Repo.get_by(id: id, visibility: :public)
33-
|> Repo.preload(:user)
34+
|> Repo.preload([:user, :stats])
3435
end
3536

3637
def fetch_asciicast(id), do: OK.required(get_asciicast(id), :not_found)
3738

3839
def find_asciicast_by_secret_token(token) do
3940
from(a in Asciicast, where: a.secret_token == ^token)
4041
|> Repo.one()
41-
|> Repo.preload(:user)
42+
|> Repo.preload([:user, :stats])
4243
end
4344

4445
def lookup_asciicast(id, allow_non_public_id \\ false) when is_binary(id) do
@@ -65,8 +66,12 @@ defmodule Asciinema.Recordings do
6566
def query(filters \\ [], order \\ nil)
6667

6768
def query(filters, order) do
69+
filters = List.wrap(filters)
70+
needs_stats_join = order == :popularity or Enum.member?(filters, :popular)
71+
6872
from(Asciicast)
6973
|> where([a], is_nil(a.archived_at))
74+
|> maybe_join_stats(needs_stats_join)
7075
|> apply_filters(filters)
7176
|> sort(order)
7277
end
@@ -100,7 +105,11 @@ defmodule Asciinema.Recordings do
100105
where(q, [a], a.featured == true and a.visibility == :public)
101106

102107
:popular ->
103-
where(q, [a], a.popularity_score > 0.0 and a.visibility == :public)
108+
where(
109+
q,
110+
[a, stats: s],
111+
a.visibility == :public and s.popularity_score > 0.0
112+
)
104113

105114
:public ->
106115
where(q, [a], a.visibility == :public)
@@ -118,13 +127,22 @@ defmodule Asciinema.Recordings do
118127
order_by(q, desc: :id)
119128

120129
:popularity ->
121-
order_by(q, desc: :popularity_score, desc: :id)
130+
order_by(q, [a, stats: s],
131+
desc: s.popularity_score,
132+
desc: s.asciicast_id
133+
)
122134

123135
:random ->
124136
order_by(q, fragment("RANDOM()"))
125137
end
126138
end
127139

140+
defp maybe_join_stats(q, true) do
141+
join(q, :inner, [a], s in assoc(a, :stats), as: :stats)
142+
end
143+
144+
defp maybe_join_stats(q, false), do: q
145+
128146
def search(%Ecto.Query{} = query, q) do
129147
query
130148
|> from()
@@ -558,22 +576,26 @@ defmodule Asciinema.Recordings do
558576

559577
Repo.transact(
560578
fn ->
561-
# Update asciicasts with daily views in the window.
579+
# Update stats for asciicasts with daily views in the window.
562580
{count, _} =
563-
from(a in Asciicast,
581+
from(s in AsciicastStats,
582+
join: a in Asciicast,
583+
on: a.id == s.asciicast_id,
564584
join: ds in subquery(decay_scores),
565-
on: ds.asciicast_id == a.id,
585+
on: ds.asciicast_id == s.asciicast_id,
566586
where: is_nil(a.archived_at),
567587
update: [set: [popularity_score: ds.decay_score, popularity_dirty: false]]
568588
)
569589
|> Repo.update_all([])
570590

571-
# Reset scores for non-archived asciicasts without views in the window.
591+
# Reset scores for non-archived stats without views in the window.
572592
Repo.update_all(
573-
from(a in Asciicast,
593+
from(s in AsciicastStats,
594+
join: a in Asciicast,
595+
on: a.id == s.asciicast_id,
574596
where:
575-
a.popularity_score > 0.0 and is_nil(a.archived_at) and
576-
a.id not in subquery(ids_with_views)
597+
is_nil(a.archived_at) and s.asciicast_id not in subquery(ids_with_views) and
598+
(s.popularity_score > 0.0 or s.popularity_dirty == true)
577599
),
578600
set: [popularity_score: 0.0, popularity_dirty: false]
579601
)
@@ -586,21 +608,23 @@ defmodule Asciinema.Recordings do
586608

587609
:dirty ->
588610
dirty_ids =
589-
from(a in Asciicast,
590-
where: a.popularity_dirty == true and is_nil(a.archived_at),
591-
select: a.id
611+
from(s in AsciicastStats,
612+
join: a in Asciicast,
613+
on: a.id == s.asciicast_id,
614+
where: s.popularity_dirty == true and is_nil(a.archived_at),
615+
select: s.asciicast_id
592616
)
593617

594618
decay_scores = from(dv in decay_scores, where: dv.asciicast_id in subquery(dirty_ids))
595619

596620
Repo.transact(
597621
fn ->
598-
# Update dirty asciicasts that have daily views in the window.
622+
# Update dirty stats for asciicasts that have daily views in the window.
599623
{count, _} =
600-
from(a in Asciicast,
624+
from(s in AsciicastStats,
601625
join: ds in subquery(decay_scores),
602-
on: ds.asciicast_id == a.id,
603-
where: a.id in subquery(dirty_ids),
626+
on: ds.asciicast_id == s.asciicast_id,
627+
where: s.asciicast_id in subquery(dirty_ids),
604628
update: [
605629
set: [
606630
popularity_score: ds.decay_score,
@@ -610,9 +634,9 @@ defmodule Asciinema.Recordings do
610634
)
611635
|> Repo.update_all([])
612636

613-
# Clear remaining dirty asciicasts with no daily views.
637+
# Clear remaining dirty stats with no daily views.
614638
Repo.update_all(
615-
from(a in Asciicast, where: a.id in subquery(dirty_ids)),
639+
from(s in AsciicastStats, where: s.asciicast_id in subquery(dirty_ids)),
616640
set: [popularity_score: 0.0, popularity_dirty: false]
617641
)
618642

@@ -626,8 +650,19 @@ defmodule Asciinema.Recordings do
626650

627651
def register_view(asciicast, date \\ Date.utc_today()) do
628652
Repo.transact(fn ->
629-
from(a in Asciicast, where: a.id == ^asciicast.id)
630-
|> Repo.update_all(inc: [views_count: 1], set: [popularity_dirty: true])
653+
Repo.insert_all(
654+
AsciicastStats,
655+
[
656+
%{
657+
asciicast_id: asciicast.id,
658+
popularity_score: 0.0,
659+
total_views: 1,
660+
popularity_dirty: true
661+
}
662+
],
663+
on_conflict: [inc: [total_views: 1], set: [popularity_dirty: true]],
664+
conflict_target: [:asciicast_id]
665+
)
631666

632667
Repo.insert_all(
633668
"asciicast_daily_views",

lib/asciinema/recordings/asciicast.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ defmodule Asciinema.Recordings.Asciicast do
3434
field :recorded_at, :utc_datetime
3535
field :idle_time_limit, :float
3636
field :speed, :float
37-
field :views_count, :integer, default: 0
38-
field :popularity_score, :float, default: 0.0
39-
field :popularity_dirty, :boolean, default: false
4037
field :archivable, :boolean, default: true
4138
field :archived_at, :utc_datetime
4239
field :markers, :string
@@ -48,6 +45,7 @@ defmodule Asciinema.Recordings.Asciicast do
4845
belongs_to :user, Asciinema.Accounts.User
4946
belongs_to :cli, Asciinema.Accounts.Cli
5047
belongs_to :stream, Asciinema.Streaming.Stream
48+
has_one :stats, Asciinema.Recordings.AsciicastStats, foreign_key: :asciicast_id
5149
end
5250

5351
defimpl Phoenix.Param do
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
defmodule Asciinema.Recordings.AsciicastStats do
2+
use Ecto.Schema
3+
alias Asciinema.Recordings.Asciicast
4+
5+
@primary_key {:asciicast_id, :id, autogenerate: false}
6+
schema "asciicast_stats" do
7+
field :total_views, :integer
8+
field :popularity_score, :float
9+
field :popularity_dirty, :boolean
10+
11+
belongs_to :asciicast, Asciicast, define_field: false
12+
end
13+
end

lib/asciinema_web/controllers/recording_html.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,10 @@ defmodule AsciinemaWeb.RecordingHTML do
167167
def filename_ext(%{version: 3}), do: "cast"
168168

169169
def views_count(asciicast) do
170-
asciicast.views_count
170+
case asciicast.stats do
171+
nil -> 0
172+
%{total_views: total_views} -> total_views
173+
end
171174
end
172175

173176
def svg_cache_key(asciicast) do
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
defmodule Asciinema.Repo.Migrations.CreateAsciicastStats do
2+
use Ecto.Migration
3+
4+
def up do
5+
create table(:asciicast_stats, primary_key: false) do
6+
add :asciicast_id, references(:asciicasts, on_delete: :delete_all),
7+
primary_key: true,
8+
null: false
9+
10+
add :total_views, :integer, null: false, default: 0
11+
add :popularity_score, :float, null: false, default: 0.0
12+
add :popularity_dirty, :boolean, null: false
13+
end
14+
15+
create index(:asciicasts, ["id DESC"],
16+
where: "visibility = 'public' AND archived_at IS NULL",
17+
name: "asciicasts_public_non_archived_index"
18+
)
19+
20+
create index(:asciicast_stats, ["popularity_score DESC", "asciicast_id DESC"],
21+
where: "popularity_score > 0.0",
22+
name: "asciicast_stats_popular_index"
23+
)
24+
25+
create index(:asciicast_stats, [:asciicast_id], where: "popularity_dirty = true")
26+
27+
execute """
28+
INSERT INTO asciicast_stats (asciicast_id, popularity_score, total_views, popularity_dirty)
29+
SELECT id, popularity_score, views_count, popularity_dirty
30+
FROM asciicasts;
31+
"""
32+
33+
alter table(:asciicasts) do
34+
remove :popularity_score
35+
remove :popularity_dirty
36+
remove :views_count
37+
end
38+
end
39+
40+
def down do
41+
alter table(:asciicasts) do
42+
add :popularity_score, :float, null: false, default: 0.0
43+
add :popularity_dirty, :boolean, null: false, default: false
44+
add :views_count, :integer, null: false, default: 0
45+
end
46+
47+
execute """
48+
UPDATE asciicasts
49+
SET popularity_score = s.popularity_score,
50+
popularity_dirty = s.popularity_dirty,
51+
views_count = s.total_views
52+
FROM asciicast_stats s
53+
WHERE asciicasts.id = s.asciicast_id;
54+
"""
55+
56+
create index(:asciicasts, ["popularity_score DESC", "id DESC"],
57+
where: "visibility = 'public' AND popularity_score > 0.0 AND archived_at IS NULL"
58+
)
59+
60+
create index(:asciicasts, [:id], where: "popularity_dirty = true AND archived_at IS NULL")
61+
62+
create index(:asciicasts, [:views_count], name: "index_asciicasts_on_views_count")
63+
64+
drop index(:asciicasts, ["id DESC"], name: "asciicasts_public_non_archived_index")
65+
66+
drop table(:asciicast_stats)
67+
end
68+
end

0 commit comments

Comments
 (0)