1 defmodule Pleroma.LoadTesting.Activities do
3 Module for generating different activities.
6 import Pleroma.LoadTesting.Helper, only: [to_sec: 1]
9 alias Pleroma.Constants
10 alias Pleroma.LoadTesting.Users
12 alias Pleroma.Web.CommonAPI
24 @visibility ~w(public private direct unlisted)
25 @types ~w(simple emoji mentions hell_thread attachment tag like reblog simple_thread remote)
26 @groups ~w(user friends non_friends)
28 @spec generate(User.t(), keyword()) :: :ok
29 def generate(user, opts \\ []) do
31 Agent.start_link(fn -> %{} end,
32 name: :benchmark_state
35 opts = Keyword.merge(@defaults, opts)
39 |> Users.get_users(limit: opts[:friends_used], local: :local, friends?: true)
44 |> Users.get_users(limit: opts[:non_friends_used], local: :local, friends?: false)
48 for visibility <- @visibility,
51 do: {visibility, type, group}
53 IO.puts("Starting generating #{opts[:iterations]} iterations of activities...")
55 friends_thread = Enum.take(friends, 5)
56 non_friends_thread = Enum.take(friends, 5)
58 public_long_thread = fn ->
59 generate_long_thread("public", user, friends_thread, non_friends_thread, opts)
62 private_long_thread = fn ->
63 generate_long_thread("private", user, friends_thread, non_friends_thread, opts)
66 iterations = opts[:iterations]
73 i when i == iterations - 2 ->
74 spawn(public_long_thread)
75 spawn(private_long_thread)
76 generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts)
79 generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts)
84 IO.puts("Generating iterations of activities took #{to_sec(time)} sec.\n")
88 def generate_power_intervals(opts \\ []) do
89 count = Keyword.get(opts, :count, 20)
90 power = Keyword.get(opts, :power, 2)
91 IO.puts("Generating #{count} intervals for a power #{power} series...")
92 counts = Enum.map(1..count, fn n -> :math.pow(n, power) end)
93 sum = Enum.sum(counts)
96 Enum.map(counts, fn c ->
101 |> Enum.reduce(0, fn density, acc ->
105 [{_, lower} | _] = acc
106 [{lower, lower + density} | acc]
112 def generate_tagged_activities(opts \\ []) do
113 tag_count = Keyword.get(opts, :tag_count, 20)
114 users = Keyword.get(opts, :users, Repo.all(Pleroma.User))
115 activity_count = Keyword.get(opts, :count, 200_000)
117 intervals = generate_power_intervals(count: tag_count)
120 "Generating #{activity_count} activities using #{tag_count} different tags of format `tag_n`, starting at tag_0"
123 Enum.each(1..activity_count, fn _ ->
124 random = :rand.uniform()
125 i = Enum.find_index(intervals, fn {lower, upper} -> lower <= random && upper > random end)
126 CommonAPI.post(Enum.random(users), %{status: "a post with the tag #tag_#{i}"})
130 defp generate_long_thread(visibility, user, friends, non_friends, _opts) do
132 if visibility == "public",
136 tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50)
139 CommonAPI.post(user, %{
140 status: "Start of #{visibility} long thread",
141 visibility: visibility
144 Agent.update(:benchmark_state, fn state ->
146 if visibility == "public",
148 else: :private_thread
150 Map.put(state, key, activity)
153 acc = {activity.id, ["@" <> user.nickname, "reply to long thread"]}
154 insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc)
155 IO.puts("Generating #{visibility} long thread ended\n")
158 defp insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc) do
159 Enum.reduce(tasks, acc, fn
160 "friend", {id, data} ->
161 friend = Enum.random(friends)
162 insert_reply(friend, List.delete(data, "@" <> friend.nickname), id, visibility)
164 "non_friend", {id, data} ->
165 non_friend = Enum.random(non_friends)
166 insert_reply(non_friend, List.delete(data, "@" <> non_friend.nickname), id, visibility)
168 "user", {id, data} ->
169 insert_reply(user, List.delete(data, "@" <> user.nickname), id, visibility)
173 defp generate_activities(user, friends, non_friends, task_data, opts) do
176 fn {visibility, type, group} ->
177 insert_activity(type, visibility, group, user, friends, non_friends, opts)
179 max_concurrency: @max_concurrency,
185 defp insert_activity("simple", visibility, group, user, friends, non_friends, _opts) do
188 |> get_actor(user, friends, non_friends)
189 |> CommonAPI.post(%{status: "Simple status", visibility: visibility})
192 defp insert_activity("emoji", visibility, group, user, friends, non_friends, _opts) do
195 |> get_actor(user, friends, non_friends)
197 status: "Simple status with emoji :firefox:",
198 visibility: visibility
202 defp insert_activity("mentions", visibility, group, user, friends, non_friends, _opts) do
204 get_random_mentions(friends, Enum.random(0..3)) ++
205 get_random_mentions(non_friends, Enum.random(0..3))
208 if Enum.random([true, false]),
209 do: ["@" <> user.nickname | user_mentions],
214 |> get_actor(user, friends, non_friends)
216 status: Enum.join(user_mentions, ", ") <> " simple status with mentions",
217 visibility: visibility
221 defp insert_activity("hell_thread", visibility, group, user, friends, non_friends, _opts) do
223 with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do
225 ([user | Enum.take(friends, 10)] ++ Enum.take(non_friends, 10))
226 |> Enum.map(&"@#{&1.nickname}")
229 Cachex.put(:user_cache, "hell_thread_mentions", cached)
232 {:ok, cached} -> cached
237 |> get_actor(user, friends, non_friends)
239 status: mentions <> " hell thread status",
240 visibility: visibility
244 defp insert_activity("attachment", visibility, group, user, friends, non_friends, _opts) do
245 actor = get_actor(group, user, friends, non_friends)
248 "actor" => actor.ap_id,
249 "name" => "4467-11.jpg",
250 "type" => "Document",
254 "#{Pleroma.Web.base_url()}/media/b1b873552422a07bf53af01f3c231c841db4dfc42c35efde681abaf0f2a4eab7.jpg",
255 "mediaType" => "image/jpeg",
261 object = Repo.insert!(%Pleroma.Object{data: obj_data})
264 CommonAPI.post(actor, %{
265 status: "Post with attachment",
266 visibility: visibility,
267 media_ids: [object.id]
271 defp insert_activity("tag", visibility, group, user, friends, non_friends, _opts) do
274 |> get_actor(user, friends, non_friends)
275 |> CommonAPI.post(%{status: "Status with #tag", visibility: visibility})
278 defp insert_activity("like", visibility, group, user, friends, non_friends, opts) do
279 actor = get_actor(group, user, friends, non_friends)
281 with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
282 {:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do
286 insert_activity("like", visibility, group, user, friends, non_friends, opts)
290 insert_activity("like", visibility, group, user, friends, non_friends, opts)
294 defp insert_activity("reblog", visibility, group, user, friends, non_friends, opts) do
295 actor = get_actor(group, user, friends, non_friends)
297 with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
298 {:ok, _activity, _object} <- CommonAPI.repeat(activity_id, actor) do
302 insert_activity("reblog", visibility, group, user, friends, non_friends, opts)
306 insert_activity("reblog", visibility, group, user, friends, non_friends, opts)
310 defp insert_activity("simple_thread", visibility, group, user, friends, non_friends, _opts)
311 when visibility in ["public", "unlisted", "private"] do
312 actor = get_actor(group, user, friends, non_friends)
313 tasks = get_reply_tasks(visibility, group)
315 {:ok, activity} = CommonAPI.post(user, %{status: "Simple status", visibility: visibility})
317 acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
318 insert_replies(tasks, visibility, user, friends, non_friends, acc)
321 defp insert_activity("simple_thread", "direct", group, user, friends, non_friends, _opts) do
322 actor = get_actor(group, user, friends, non_friends)
323 tasks = get_reply_tasks("direct", group)
328 Enum.take(non_friends, 3)
331 Enum.take(friends, 3)
334 data = Enum.map(list, &("@" <> &1.nickname))
337 CommonAPI.post(actor, %{
338 status: Enum.join(data, ", ") <> "simple status",
342 acc = {activity.id, ["@" <> user.nickname | data] ++ ["reply to status"]}
343 insert_direct_replies(tasks, user, list, acc)
346 defp insert_activity("remote", _, "user", _, _, _, _), do: :ok
348 defp insert_activity("remote", visibility, group, user, _friends, _non_friends, opts) do
350 Users.get_users(user, limit: opts[:friends_used], local: :external, friends?: true)
353 Users.get_users(user, limit: opts[:non_friends_used], local: :external, friends?: false)
355 actor = get_actor(group, user, remote_friends, remote_non_friends)
357 {act_data, obj_data} = prepare_activity_data(actor, visibility, user)
358 {activity_data, object_data} = other_data(actor)
361 |> Map.merge(act_data)
362 |> Map.put("object", Map.merge(object_data, obj_data))
363 |> Pleroma.Web.ActivityPub.ActivityPub.insert(false)
366 defp get_actor("user", user, _friends, _non_friends), do: user
367 defp get_actor("friends", _user, friends, _non_friends), do: Enum.random(friends)
368 defp get_actor("non_friends", _user, _friends, non_friends), do: Enum.random(non_friends)
370 defp other_data(actor) do
371 %{host: host} = URI.parse(actor.ap_id)
372 datetime = DateTime.utc_now()
373 context_id = "http://#{host}:4000/contexts/#{UUID.generate()}"
374 activity_id = "http://#{host}:4000/activities/#{UUID.generate()}"
375 object_id = "http://#{host}:4000/objects/#{UUID.generate()}"
378 "actor" => actor.ap_id,
379 "context" => context_id,
381 "published" => datetime,
383 "directMessage" => false
387 "actor" => actor.ap_id,
389 "attributedTo" => actor.ap_id,
392 "content" => "Remote post",
393 "context" => context_id,
394 "conversation" => context_id,
397 "published" => datetime,
398 "sensitive" => false,
401 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
405 {activity_data, object_data}
408 defp prepare_activity_data(actor, "public", _mention) do
410 "cc" => [actor.follower_address],
411 "to" => [Constants.as_public()]
415 "cc" => [actor.follower_address],
416 "to" => [Constants.as_public()]
422 defp prepare_activity_data(actor, "private", _mention) do
425 "to" => [actor.follower_address]
430 "to" => [actor.follower_address]
436 defp prepare_activity_data(actor, "unlisted", _mention) do
438 "cc" => [Constants.as_public()],
439 "to" => [actor.follower_address]
443 "cc" => [Constants.as_public()],
444 "to" => [actor.follower_address]
450 defp prepare_activity_data(_actor, "direct", mention) do
451 %{host: mentioned_host} = URI.parse(mention.ap_id)
456 "<span class=\"h-card\"><a class=\"u-url mention\" href=\"#{mention.ap_id}\" rel=\"ugc\">@<span>#{
458 }</span></a></span> direct message",
461 "href" => mention.ap_id,
462 "name" => "@#{mention.nickname}@#{mentioned_host}",
466 "to" => [mention.ap_id]
471 "directMessage" => true,
472 "to" => [mention.ap_id]
478 defp get_reply_tasks("public", "user"), do: ~w(friend non_friend user)
479 defp get_reply_tasks("public", "friends"), do: ~w(non_friend user friend)
480 defp get_reply_tasks("public", "non_friends"), do: ~w(user friend non_friend)
482 defp get_reply_tasks(visibility, "user") when visibility in ["unlisted", "private"],
483 do: ~w(friend user friend)
485 defp get_reply_tasks(visibility, "friends") when visibility in ["unlisted", "private"],
486 do: ~w(user friend user)
488 defp get_reply_tasks(visibility, "non_friends") when visibility in ["unlisted", "private"],
491 defp get_reply_tasks("direct", "user"), do: ~w(friend user friend)
492 defp get_reply_tasks("direct", "friends"), do: ~w(user friend user)
493 defp get_reply_tasks("direct", "non_friends"), do: ~w(user non_friend user)
495 defp insert_replies(tasks, visibility, user, friends, non_friends, acc) do
496 Enum.reduce(tasks, acc, fn
497 "friend", {id, data} ->
498 friend = Enum.random(friends)
499 insert_reply(friend, data, id, visibility)
501 "non_friend", {id, data} ->
502 non_friend = Enum.random(non_friends)
503 insert_reply(non_friend, data, id, visibility)
505 "user", {id, data} ->
506 insert_reply(user, data, id, visibility)
510 defp insert_direct_replies(tasks, user, list, acc) do
511 Enum.reduce(tasks, acc, fn
512 group, {id, data} when group in ["friend", "non_friend"] ->
513 actor = Enum.random(list)
516 insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct")
520 "user", {id, data} ->
521 {reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct")
526 defp insert_reply(actor, data, activity_id, visibility) do
528 CommonAPI.post(actor, %{
529 status: Enum.join(data, ", "),
530 visibility: visibility,
531 in_reply_to_status_id: activity_id
534 {reply.id, ["@" <> actor.nickname | data]}
537 defp get_random_mentions(_users, count) when count == 0, do: []
539 defp get_random_mentions(users, count) do
543 |> Enum.map(&"@#{&1.nickname}")
546 defp get_random_create_activity_id do
548 from(a in Pleroma.Activity,
549 where: fragment("(?)->>'type' = ?", a.data, ^"Create"),
550 order_by: fragment("RANDOM()"),