596 lines
17 KiB
Elixir
596 lines
17 KiB
Elixir
defmodule Pleroma.LoadTesting.Activities do
|
|
@moduledoc """
|
|
Module for generating different activities.
|
|
"""
|
|
import Ecto.Query
|
|
import Pleroma.LoadTesting.Helper, only: [to_sec: 1]
|
|
|
|
alias Ecto.UUID
|
|
alias Pleroma.Constants
|
|
alias Pleroma.LoadTesting.Users
|
|
alias Pleroma.Repo
|
|
alias Pleroma.Web.CommonAPI
|
|
|
|
require Constants
|
|
|
|
@defaults [
|
|
iterations: 170,
|
|
friends_used: 20,
|
|
non_friends_used: 20
|
|
]
|
|
|
|
@max_concurrency 10
|
|
|
|
@visibility ~w(public private direct unlisted)
|
|
@types [
|
|
:simple,
|
|
:simple_filtered,
|
|
:emoji,
|
|
:mentions,
|
|
:hell_thread,
|
|
:attachment,
|
|
:tag,
|
|
:like,
|
|
:reblog,
|
|
:simple_thread
|
|
]
|
|
@groups [:friends_local, :friends_remote, :non_friends_local, :non_friends_local]
|
|
@remote_groups [:friends_remote, :non_friends_remote]
|
|
@friends_groups [:friends_local, :friends_remote]
|
|
@non_friends_groups [:non_friends_local, :non_friends_remote]
|
|
|
|
@spec generate(User.t(), keyword()) :: :ok
|
|
def generate(user, opts \\ []) do
|
|
{:ok, _} =
|
|
Agent.start_link(fn -> %{} end,
|
|
name: :benchmark_state
|
|
)
|
|
|
|
opts = Keyword.merge(@defaults, opts)
|
|
|
|
users = Users.prepare_users(user, opts)
|
|
|
|
{:ok, _} = Agent.start_link(fn -> users[:non_friends_remote] end, name: :non_friends_remote)
|
|
|
|
task_data =
|
|
for visibility <- @visibility,
|
|
type <- @types,
|
|
group <- [:user | @groups],
|
|
do: {visibility, type, group}
|
|
|
|
IO.puts("Starting generating #{opts[:iterations]} iterations of activities...")
|
|
|
|
public_long_thread = fn ->
|
|
generate_long_thread("public", users, opts)
|
|
end
|
|
|
|
private_long_thread = fn ->
|
|
generate_long_thread("private", users, opts)
|
|
end
|
|
|
|
iterations = opts[:iterations]
|
|
|
|
{time, _} =
|
|
:timer.tc(fn ->
|
|
Enum.each(
|
|
1..iterations,
|
|
fn
|
|
i when i == iterations - 2 ->
|
|
spawn(public_long_thread)
|
|
spawn(private_long_thread)
|
|
generate_activities(users, Enum.shuffle(task_data), opts)
|
|
|
|
_ ->
|
|
generate_activities(users, Enum.shuffle(task_data), opts)
|
|
end
|
|
)
|
|
end)
|
|
|
|
IO.puts("Generating iterations of activities took #{to_sec(time)} sec.\n")
|
|
:ok
|
|
end
|
|
|
|
def generate_power_intervals(opts \\ []) do
|
|
count = Keyword.get(opts, :count, 20)
|
|
power = Keyword.get(opts, :power, 2)
|
|
IO.puts("Generating #{count} intervals for a power #{power} series...")
|
|
counts = Enum.map(1..count, fn n -> :math.pow(n, power) end)
|
|
sum = Enum.sum(counts)
|
|
|
|
densities =
|
|
Enum.map(counts, fn c ->
|
|
c / sum
|
|
end)
|
|
|
|
densities
|
|
|> Enum.reduce(0, fn density, acc ->
|
|
if acc == 0 do
|
|
[{0, density}]
|
|
else
|
|
[{_, lower} | _] = acc
|
|
[{lower, lower + density} | acc]
|
|
end
|
|
end)
|
|
|> Enum.reverse()
|
|
end
|
|
|
|
def generate_tagged_activities(opts \\ []) do
|
|
tag_count = Keyword.get(opts, :tag_count, 20)
|
|
users = Keyword.get(opts, :users, Repo.all(Pleroma.User))
|
|
activity_count = Keyword.get(opts, :count, 200_000)
|
|
|
|
intervals = generate_power_intervals(count: tag_count)
|
|
|
|
IO.puts(
|
|
"Generating #{activity_count} activities using #{tag_count} different tags of format `tag_n`, starting at tag_0"
|
|
)
|
|
|
|
Enum.each(1..activity_count, fn _ ->
|
|
random = :rand.uniform()
|
|
i = Enum.find_index(intervals, fn {lower, upper} -> lower <= random && upper > random end)
|
|
CommonAPI.post(Enum.random(users), %{status: "a post with the tag #tag_#{i}"})
|
|
end)
|
|
end
|
|
|
|
defp generate_long_thread(visibility, users, _opts) do
|
|
group =
|
|
if visibility == "public",
|
|
do: :friends_local,
|
|
else: :user
|
|
|
|
tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50)
|
|
|
|
{:ok, activity} =
|
|
CommonAPI.post(users[:user], %{
|
|
status: "Start of #{visibility} long thread",
|
|
visibility: visibility
|
|
})
|
|
|
|
Agent.update(:benchmark_state, fn state ->
|
|
key =
|
|
if visibility == "public",
|
|
do: :public_thread,
|
|
else: :private_thread
|
|
|
|
Map.put(state, key, activity)
|
|
end)
|
|
|
|
acc = {activity.id, ["@" <> users[:user].nickname, "reply to long thread"]}
|
|
insert_replies_for_long_thread(tasks, visibility, users, acc)
|
|
IO.puts("Generating #{visibility} long thread ended\n")
|
|
end
|
|
|
|
defp insert_replies_for_long_thread(tasks, visibility, users, acc) do
|
|
Enum.reduce(tasks, acc, fn
|
|
:user, {id, data} ->
|
|
user = users[:user]
|
|
insert_reply(user, List.delete(data, "@" <> user.nickname), id, visibility)
|
|
|
|
group, {id, data} ->
|
|
replier = Enum.random(users[group])
|
|
insert_reply(replier, List.delete(data, "@" <> replier.nickname), id, visibility)
|
|
end)
|
|
end
|
|
|
|
defp generate_activities(users, task_data, opts) do
|
|
Task.async_stream(
|
|
task_data,
|
|
fn {visibility, type, group} ->
|
|
insert_activity(type, visibility, group, users, opts)
|
|
end,
|
|
max_concurrency: @max_concurrency,
|
|
timeout: 30_000
|
|
)
|
|
|> Stream.run()
|
|
end
|
|
|
|
defp insert_local_activity(visibility, group, users, status) do
|
|
{:ok, _} =
|
|
group
|
|
|> get_actor(users)
|
|
|> CommonAPI.post(%{status: status, visibility: visibility})
|
|
end
|
|
|
|
defp insert_remote_activity(visibility, group, users, status) do
|
|
actor = get_actor(group, users)
|
|
{act_data, obj_data} = prepare_activity_data(actor, visibility, users[:user])
|
|
{activity_data, object_data} = other_data(actor, status)
|
|
|
|
activity_data
|
|
|> Map.merge(act_data)
|
|
|> Map.put("object", Map.merge(object_data, obj_data))
|
|
|> Pleroma.Web.ActivityPub.ActivityPub.insert(false)
|
|
end
|
|
|
|
defp user_mentions(users) do
|
|
user_mentions =
|
|
Enum.reduce(
|
|
@groups,
|
|
[],
|
|
fn group, acc ->
|
|
acc ++ get_random_mentions(users[group], Enum.random(0..2))
|
|
end
|
|
)
|
|
|
|
if Enum.random([true, false]),
|
|
do: ["@" <> users[:user].nickname | user_mentions],
|
|
else: user_mentions
|
|
end
|
|
|
|
defp hell_thread_mentions(users) do
|
|
with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do
|
|
cached =
|
|
@groups
|
|
|> Enum.reduce([users[:user]], fn group, acc ->
|
|
acc ++ Enum.take(users[group], 5)
|
|
end)
|
|
|> Enum.map(&"@#{&1.nickname}")
|
|
|> Enum.join(", ")
|
|
|
|
Cachex.put(:user_cache, "hell_thread_mentions", cached)
|
|
cached
|
|
else
|
|
{:ok, cached} -> cached
|
|
end
|
|
end
|
|
|
|
defp insert_activity(:simple, visibility, group, users, _opts)
|
|
when group in @remote_groups do
|
|
insert_remote_activity(visibility, group, users, "Remote status")
|
|
end
|
|
|
|
defp insert_activity(:simple, visibility, group, users, _opts) do
|
|
insert_local_activity(visibility, group, users, "Simple status")
|
|
end
|
|
|
|
defp insert_activity(:simple_filtered, visibility, group, users, _opts)
|
|
when group in @remote_groups do
|
|
insert_remote_activity(visibility, group, users, "Remote status which must be filtered")
|
|
end
|
|
|
|
defp insert_activity(:simple_filtered, visibility, group, users, _opts) do
|
|
insert_local_activity(visibility, group, users, "Simple status which must be filtered")
|
|
end
|
|
|
|
defp insert_activity(:emoji, visibility, group, users, _opts)
|
|
when group in @remote_groups do
|
|
insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:")
|
|
end
|
|
|
|
defp insert_activity(:emoji, visibility, group, users, _opts) do
|
|
insert_local_activity(visibility, group, users, "Simple status with emoji :firefox:")
|
|
end
|
|
|
|
defp insert_activity(:mentions, visibility, group, users, _opts)
|
|
when group in @remote_groups do
|
|
mentions = user_mentions(users)
|
|
|
|
status = Enum.join(mentions, ", ") <> " remote status with mentions"
|
|
|
|
insert_remote_activity(visibility, group, users, status)
|
|
end
|
|
|
|
defp insert_activity(:mentions, visibility, group, users, _opts) do
|
|
mentions = user_mentions(users)
|
|
|
|
status = Enum.join(mentions, ", ") <> " simple status with mentions"
|
|
insert_remote_activity(visibility, group, users, status)
|
|
end
|
|
|
|
defp insert_activity(:hell_thread, visibility, group, users, _)
|
|
when group in @remote_groups do
|
|
mentions = hell_thread_mentions(users)
|
|
insert_remote_activity(visibility, group, users, mentions <> " remote hell thread status")
|
|
end
|
|
|
|
defp insert_activity(:hell_thread, visibility, group, users, _opts) do
|
|
mentions = hell_thread_mentions(users)
|
|
|
|
insert_local_activity(visibility, group, users, mentions <> " hell thread status")
|
|
end
|
|
|
|
defp insert_activity(:attachment, visibility, group, users, _opts) do
|
|
actor = get_actor(group, users)
|
|
|
|
obj_data = %{
|
|
"actor" => actor.ap_id,
|
|
"name" => "4467-11.jpg",
|
|
"type" => "Document",
|
|
"url" => [
|
|
%{
|
|
"href" =>
|
|
"#{Pleroma.Web.Endpoint.url()}/media/b1b873552422a07bf53af01f3c231c841db4dfc42c35efde681abaf0f2a4eab7.jpg",
|
|
"mediaType" => "image/jpeg",
|
|
"type" => "Link"
|
|
}
|
|
]
|
|
}
|
|
|
|
object = Repo.insert!(%Pleroma.Object{data: obj_data})
|
|
|
|
{:ok, _activity} =
|
|
CommonAPI.post(actor, %{
|
|
status: "Post with attachment",
|
|
visibility: visibility,
|
|
media_ids: [object.id]
|
|
})
|
|
end
|
|
|
|
defp insert_activity(:tag, visibility, group, users, _opts) do
|
|
insert_local_activity(visibility, group, users, "Status with #tag")
|
|
end
|
|
|
|
defp insert_activity(:like, visibility, group, users, opts) do
|
|
actor = get_actor(group, users)
|
|
|
|
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
|
|
{:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do
|
|
:ok
|
|
else
|
|
{:error, _} ->
|
|
insert_activity(:like, visibility, group, users, opts)
|
|
|
|
nil ->
|
|
Process.sleep(15)
|
|
insert_activity(:like, visibility, group, users, opts)
|
|
end
|
|
end
|
|
|
|
defp insert_activity(:reblog, visibility, group, users, opts) do
|
|
actor = get_actor(group, users)
|
|
|
|
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
|
|
{:ok, _activity} <- CommonAPI.repeat(activity_id, actor) do
|
|
:ok
|
|
else
|
|
{:error, _} ->
|
|
insert_activity(:reblog, visibility, group, users, opts)
|
|
|
|
nil ->
|
|
Process.sleep(15)
|
|
insert_activity(:reblog, visibility, group, users, opts)
|
|
end
|
|
end
|
|
|
|
defp insert_activity(:simple_thread, "direct", group, users, _opts) do
|
|
actor = get_actor(group, users)
|
|
tasks = get_reply_tasks("direct", group)
|
|
|
|
list =
|
|
case group do
|
|
:user ->
|
|
group = Enum.random(@friends_groups)
|
|
Enum.take(users[group], 3)
|
|
|
|
_ ->
|
|
Enum.take(users[group], 3)
|
|
end
|
|
|
|
data = Enum.map(list, &("@" <> &1.nickname))
|
|
|
|
{:ok, activity} =
|
|
CommonAPI.post(actor, %{
|
|
status: Enum.join(data, ", ") <> "simple status",
|
|
visibility: "direct"
|
|
})
|
|
|
|
acc = {activity.id, ["@" <> users[:user].nickname | data] ++ ["reply to status"]}
|
|
insert_direct_replies(tasks, users[:user], list, acc)
|
|
end
|
|
|
|
defp insert_activity(:simple_thread, visibility, group, users, _opts) do
|
|
actor = get_actor(group, users)
|
|
tasks = get_reply_tasks(visibility, group)
|
|
|
|
{:ok, activity} =
|
|
CommonAPI.post(users[:user], %{status: "Simple status", visibility: visibility})
|
|
|
|
acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
|
|
insert_replies(tasks, visibility, users, acc)
|
|
end
|
|
|
|
defp get_actor(:user, %{user: user}), do: user
|
|
defp get_actor(group, users), do: Enum.random(users[group])
|
|
|
|
defp other_data(actor, content) do
|
|
%{host: host} = URI.parse(actor.ap_id)
|
|
datetime = DateTime.utc_now() |> to_string()
|
|
context_id = "https://#{host}/contexts/#{UUID.generate()}"
|
|
activity_id = "https://#{host}/activities/#{UUID.generate()}"
|
|
object_id = "https://#{host}/objects/#{UUID.generate()}"
|
|
|
|
activity_data = %{
|
|
"actor" => actor.ap_id,
|
|
"context" => context_id,
|
|
"id" => activity_id,
|
|
"published" => datetime,
|
|
"type" => "Create",
|
|
"directMessage" => false
|
|
}
|
|
|
|
object_data = %{
|
|
"actor" => actor.ap_id,
|
|
"attachment" => [],
|
|
"attributedTo" => actor.ap_id,
|
|
"bcc" => [],
|
|
"bto" => [],
|
|
"content" => content,
|
|
"context" => context_id,
|
|
"conversation" => context_id,
|
|
"emoji" => %{},
|
|
"id" => object_id,
|
|
"published" => datetime,
|
|
"sensitive" => false,
|
|
"summary" => "",
|
|
"tag" => [],
|
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
|
"type" => "Note"
|
|
}
|
|
|
|
{activity_data, object_data}
|
|
end
|
|
|
|
defp prepare_activity_data(actor, "public", _mention) do
|
|
obj_data = %{
|
|
"cc" => [actor.follower_address],
|
|
"to" => [Constants.as_public()]
|
|
}
|
|
|
|
act_data = %{
|
|
"cc" => [actor.follower_address],
|
|
"to" => [Constants.as_public()]
|
|
}
|
|
|
|
{act_data, obj_data}
|
|
end
|
|
|
|
defp prepare_activity_data(actor, "private", _mention) do
|
|
obj_data = %{
|
|
"cc" => [],
|
|
"to" => [actor.follower_address]
|
|
}
|
|
|
|
act_data = %{
|
|
"cc" => [],
|
|
"to" => [actor.follower_address]
|
|
}
|
|
|
|
{act_data, obj_data}
|
|
end
|
|
|
|
defp prepare_activity_data(actor, "unlisted", _mention) do
|
|
obj_data = %{
|
|
"cc" => [Constants.as_public()],
|
|
"to" => [actor.follower_address]
|
|
}
|
|
|
|
act_data = %{
|
|
"cc" => [Constants.as_public()],
|
|
"to" => [actor.follower_address]
|
|
}
|
|
|
|
{act_data, obj_data}
|
|
end
|
|
|
|
defp prepare_activity_data(_actor, "direct", mention) do
|
|
%{host: mentioned_host} = URI.parse(mention.ap_id)
|
|
|
|
obj_data = %{
|
|
"cc" => [],
|
|
"content" =>
|
|
"<span class=\"h-card\"><a class=\"u-url mention\" href=\"#{mention.ap_id}\" rel=\"ugc\">@<span>#{
|
|
mention.nickname
|
|
}</span></a></span> direct message",
|
|
"tag" => [
|
|
%{
|
|
"href" => mention.ap_id,
|
|
"name" => "@#{mention.nickname}@#{mentioned_host}",
|
|
"type" => "Mention"
|
|
}
|
|
],
|
|
"to" => [mention.ap_id]
|
|
}
|
|
|
|
act_data = %{
|
|
"cc" => [],
|
|
"directMessage" => true,
|
|
"to" => [mention.ap_id]
|
|
}
|
|
|
|
{act_data, obj_data}
|
|
end
|
|
|
|
defp get_reply_tasks("public", :user) do
|
|
[:friends_local, :friends_remote, :non_friends_local, :non_friends_remote, :user]
|
|
end
|
|
|
|
defp get_reply_tasks("public", group) when group in @friends_groups do
|
|
[:non_friends_local, :non_friends_remote, :user, :friends_local, :friends_remote]
|
|
end
|
|
|
|
defp get_reply_tasks("public", group) when group in @non_friends_groups do
|
|
[:user, :friends_local, :friends_remote, :non_friends_local, :non_friends_remote]
|
|
end
|
|
|
|
defp get_reply_tasks(visibility, :user) when visibility in ["unlisted", "private"] do
|
|
[:friends_local, :friends_remote, :user, :friends_local, :friends_remote]
|
|
end
|
|
|
|
defp get_reply_tasks(visibility, group)
|
|
when visibility in ["unlisted", "private"] and group in @friends_groups do
|
|
[:user, :friends_remote, :friends_local, :user]
|
|
end
|
|
|
|
defp get_reply_tasks(visibility, group)
|
|
when visibility in ["unlisted", "private"] and
|
|
group in @non_friends_groups,
|
|
do: []
|
|
|
|
defp get_reply_tasks("direct", :user), do: [:friends_local, :user, :friends_remote]
|
|
|
|
defp get_reply_tasks("direct", group) when group in @friends_groups,
|
|
do: [:user, group, :user]
|
|
|
|
defp get_reply_tasks("direct", group) when group in @non_friends_groups do
|
|
[:user, :non_friends_remote, :user, :non_friends_local]
|
|
end
|
|
|
|
defp insert_replies(tasks, visibility, users, acc) do
|
|
Enum.reduce(tasks, acc, fn
|
|
:user, {id, data} ->
|
|
insert_reply(users[:user], data, id, visibility)
|
|
|
|
group, {id, data} ->
|
|
replier = Enum.random(users[group])
|
|
insert_reply(replier, data, id, visibility)
|
|
end)
|
|
end
|
|
|
|
defp insert_direct_replies(tasks, user, list, acc) do
|
|
Enum.reduce(tasks, acc, fn
|
|
:user, {id, data} ->
|
|
{reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct")
|
|
{reply_id, data}
|
|
|
|
_, {id, data} ->
|
|
actor = Enum.random(list)
|
|
|
|
{reply_id, _} =
|
|
insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct")
|
|
|
|
{reply_id, data}
|
|
end)
|
|
end
|
|
|
|
defp insert_reply(actor, data, activity_id, visibility) do
|
|
{:ok, reply} =
|
|
CommonAPI.post(actor, %{
|
|
status: Enum.join(data, ", "),
|
|
visibility: visibility,
|
|
in_reply_to_status_id: activity_id
|
|
})
|
|
|
|
{reply.id, ["@" <> actor.nickname | data]}
|
|
end
|
|
|
|
defp get_random_mentions(_users, count) when count == 0, do: []
|
|
|
|
defp get_random_mentions(users, count) do
|
|
users
|
|
|> Enum.shuffle()
|
|
|> Enum.take(count)
|
|
|> Enum.map(&"@#{&1.nickname}")
|
|
end
|
|
|
|
defp get_random_create_activity_id do
|
|
Repo.one(
|
|
from(a in Pleroma.Activity,
|
|
where: fragment("(?)->>'type' = ?", a.data, ^"Create"),
|
|
order_by: fragment("RANDOM()"),
|
|
limit: 1,
|
|
select: a.id
|
|
)
|
|
)
|
|
end
|
|
end
|