logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

activities.ex (16996B)


  1. defmodule Pleroma.LoadTesting.Activities do
  2. @moduledoc """
  3. Module for generating different activities.
  4. """
  5. import Ecto.Query
  6. import Pleroma.LoadTesting.Helper, only: [to_sec: 1]
  7. alias Ecto.UUID
  8. alias Pleroma.Constants
  9. alias Pleroma.LoadTesting.Users
  10. alias Pleroma.Repo
  11. alias Pleroma.Web.CommonAPI
  12. require Constants
  13. @defaults [
  14. iterations: 170,
  15. friends_used: 20,
  16. non_friends_used: 20
  17. ]
  18. @max_concurrency 10
  19. @visibility ~w(public private direct unlisted)
  20. @types [
  21. :simple,
  22. :simple_filtered,
  23. :emoji,
  24. :mentions,
  25. :hell_thread,
  26. :attachment,
  27. :tag,
  28. :like,
  29. :reblog,
  30. :simple_thread
  31. ]
  32. @groups [:friends_local, :friends_remote, :non_friends_local, :non_friends_local]
  33. @remote_groups [:friends_remote, :non_friends_remote]
  34. @friends_groups [:friends_local, :friends_remote]
  35. @non_friends_groups [:non_friends_local, :non_friends_remote]
  36. @spec generate(User.t(), keyword()) :: :ok
  37. def generate(user, opts \\ []) do
  38. {:ok, _} =
  39. Agent.start_link(fn -> %{} end,
  40. name: :benchmark_state
  41. )
  42. opts = Keyword.merge(@defaults, opts)
  43. users = Users.prepare_users(user, opts)
  44. {:ok, _} = Agent.start_link(fn -> users[:non_friends_remote] end, name: :non_friends_remote)
  45. task_data =
  46. for visibility <- @visibility,
  47. type <- @types,
  48. group <- [:user | @groups],
  49. do: {visibility, type, group}
  50. IO.puts("Starting generating #{opts[:iterations]} iterations of activities...")
  51. public_long_thread = fn ->
  52. generate_long_thread("public", users, opts)
  53. end
  54. private_long_thread = fn ->
  55. generate_long_thread("private", users, opts)
  56. end
  57. iterations = opts[:iterations]
  58. {time, _} =
  59. :timer.tc(fn ->
  60. Enum.each(
  61. 1..iterations,
  62. fn
  63. i when i == iterations - 2 ->
  64. spawn(public_long_thread)
  65. spawn(private_long_thread)
  66. generate_activities(users, Enum.shuffle(task_data), opts)
  67. _ ->
  68. generate_activities(users, Enum.shuffle(task_data), opts)
  69. end
  70. )
  71. end)
  72. IO.puts("Generating iterations of activities took #{to_sec(time)} sec.\n")
  73. :ok
  74. end
  75. def generate_power_intervals(opts \\ []) do
  76. count = Keyword.get(opts, :count, 20)
  77. power = Keyword.get(opts, :power, 2)
  78. IO.puts("Generating #{count} intervals for a power #{power} series...")
  79. counts = Enum.map(1..count, fn n -> :math.pow(n, power) end)
  80. sum = Enum.sum(counts)
  81. densities =
  82. Enum.map(counts, fn c ->
  83. c / sum
  84. end)
  85. densities
  86. |> Enum.reduce(0, fn density, acc ->
  87. if acc == 0 do
  88. [{0, density}]
  89. else
  90. [{_, lower} | _] = acc
  91. [{lower, lower + density} | acc]
  92. end
  93. end)
  94. |> Enum.reverse()
  95. end
  96. def generate_tagged_activities(opts \\ []) do
  97. tag_count = Keyword.get(opts, :tag_count, 20)
  98. users = Keyword.get(opts, :users, Repo.all(Pleroma.User))
  99. activity_count = Keyword.get(opts, :count, 200_000)
  100. intervals = generate_power_intervals(count: tag_count)
  101. IO.puts(
  102. "Generating #{activity_count} activities using #{tag_count} different tags of format `tag_n`, starting at tag_0"
  103. )
  104. Enum.each(1..activity_count, fn _ ->
  105. random = :rand.uniform()
  106. i = Enum.find_index(intervals, fn {lower, upper} -> lower <= random && upper > random end)
  107. CommonAPI.post(Enum.random(users), %{status: "a post with the tag #tag_#{i}"})
  108. end)
  109. end
  110. defp generate_long_thread(visibility, users, _opts) do
  111. group =
  112. if visibility == "public",
  113. do: :friends_local,
  114. else: :user
  115. tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50)
  116. {:ok, activity} =
  117. CommonAPI.post(users[:user], %{
  118. status: "Start of #{visibility} long thread",
  119. visibility: visibility
  120. })
  121. Agent.update(:benchmark_state, fn state ->
  122. key =
  123. if visibility == "public",
  124. do: :public_thread,
  125. else: :private_thread
  126. Map.put(state, key, activity)
  127. end)
  128. acc = {activity.id, ["@" <> users[:user].nickname, "reply to long thread"]}
  129. insert_replies_for_long_thread(tasks, visibility, users, acc)
  130. IO.puts("Generating #{visibility} long thread ended\n")
  131. end
  132. defp insert_replies_for_long_thread(tasks, visibility, users, acc) do
  133. Enum.reduce(tasks, acc, fn
  134. :user, {id, data} ->
  135. user = users[:user]
  136. insert_reply(user, List.delete(data, "@" <> user.nickname), id, visibility)
  137. group, {id, data} ->
  138. replier = Enum.random(users[group])
  139. insert_reply(replier, List.delete(data, "@" <> replier.nickname), id, visibility)
  140. end)
  141. end
  142. defp generate_activities(users, task_data, opts) do
  143. Task.async_stream(
  144. task_data,
  145. fn {visibility, type, group} ->
  146. insert_activity(type, visibility, group, users, opts)
  147. end,
  148. max_concurrency: @max_concurrency,
  149. timeout: 30_000
  150. )
  151. |> Stream.run()
  152. end
  153. defp insert_local_activity(visibility, group, users, status) do
  154. {:ok, _} =
  155. group
  156. |> get_actor(users)
  157. |> CommonAPI.post(%{status: status, visibility: visibility})
  158. end
  159. defp insert_remote_activity(visibility, group, users, status) do
  160. actor = get_actor(group, users)
  161. {act_data, obj_data} = prepare_activity_data(actor, visibility, users[:user])
  162. {activity_data, object_data} = other_data(actor, status)
  163. activity_data
  164. |> Map.merge(act_data)
  165. |> Map.put("object", Map.merge(object_data, obj_data))
  166. |> Pleroma.Web.ActivityPub.ActivityPub.insert(false)
  167. end
  168. defp user_mentions(users) do
  169. user_mentions =
  170. Enum.reduce(
  171. @groups,
  172. [],
  173. fn group, acc ->
  174. acc ++ get_random_mentions(users[group], Enum.random(0..2))
  175. end
  176. )
  177. if Enum.random([true, false]),
  178. do: ["@" <> users[:user].nickname | user_mentions],
  179. else: user_mentions
  180. end
  181. defp hell_thread_mentions(users) do
  182. with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do
  183. cached =
  184. @groups
  185. |> Enum.reduce([users[:user]], fn group, acc ->
  186. acc ++ Enum.take(users[group], 5)
  187. end)
  188. |> Enum.map(&"@#{&1.nickname}")
  189. |> Enum.join(", ")
  190. Cachex.put(:user_cache, "hell_thread_mentions", cached)
  191. cached
  192. else
  193. {:ok, cached} -> cached
  194. end
  195. end
  196. defp insert_activity(:simple, visibility, group, users, _opts)
  197. when group in @remote_groups do
  198. insert_remote_activity(visibility, group, users, "Remote status")
  199. end
  200. defp insert_activity(:simple, visibility, group, users, _opts) do
  201. insert_local_activity(visibility, group, users, "Simple status")
  202. end
  203. defp insert_activity(:simple_filtered, visibility, group, users, _opts)
  204. when group in @remote_groups do
  205. insert_remote_activity(visibility, group, users, "Remote status which must be filtered")
  206. end
  207. defp insert_activity(:simple_filtered, visibility, group, users, _opts) do
  208. insert_local_activity(visibility, group, users, "Simple status which must be filtered")
  209. end
  210. defp insert_activity(:emoji, visibility, group, users, _opts)
  211. when group in @remote_groups do
  212. insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:")
  213. end
  214. defp insert_activity(:emoji, visibility, group, users, _opts) do
  215. insert_local_activity(visibility, group, users, "Simple status with emoji :firefox:")
  216. end
  217. defp insert_activity(:mentions, visibility, group, users, _opts)
  218. when group in @remote_groups do
  219. mentions = user_mentions(users)
  220. status = Enum.join(mentions, ", ") <> " remote status with mentions"
  221. insert_remote_activity(visibility, group, users, status)
  222. end
  223. defp insert_activity(:mentions, visibility, group, users, _opts) do
  224. mentions = user_mentions(users)
  225. status = Enum.join(mentions, ", ") <> " simple status with mentions"
  226. insert_remote_activity(visibility, group, users, status)
  227. end
  228. defp insert_activity(:hell_thread, visibility, group, users, _)
  229. when group in @remote_groups do
  230. mentions = hell_thread_mentions(users)
  231. insert_remote_activity(visibility, group, users, mentions <> " remote hell thread status")
  232. end
  233. defp insert_activity(:hell_thread, visibility, group, users, _opts) do
  234. mentions = hell_thread_mentions(users)
  235. insert_local_activity(visibility, group, users, mentions <> " hell thread status")
  236. end
  237. defp insert_activity(:attachment, visibility, group, users, _opts) do
  238. actor = get_actor(group, users)
  239. obj_data = %{
  240. "actor" => actor.ap_id,
  241. "name" => "4467-11.jpg",
  242. "type" => "Document",
  243. "url" => [
  244. %{
  245. "href" =>
  246. "#{Pleroma.Web.Endpoint.url()}/media/b1b873552422a07bf53af01f3c231c841db4dfc42c35efde681abaf0f2a4eab7.jpg",
  247. "mediaType" => "image/jpeg",
  248. "type" => "Link"
  249. }
  250. ]
  251. }
  252. object = Repo.insert!(%Pleroma.Object{data: obj_data})
  253. {:ok, _activity} =
  254. CommonAPI.post(actor, %{
  255. status: "Post with attachment",
  256. visibility: visibility,
  257. media_ids: [object.id]
  258. })
  259. end
  260. defp insert_activity(:tag, visibility, group, users, _opts) do
  261. insert_local_activity(visibility, group, users, "Status with #tag")
  262. end
  263. defp insert_activity(:like, visibility, group, users, opts) do
  264. actor = get_actor(group, users)
  265. with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
  266. {:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do
  267. :ok
  268. else
  269. {:error, _} ->
  270. insert_activity(:like, visibility, group, users, opts)
  271. nil ->
  272. Process.sleep(15)
  273. insert_activity(:like, visibility, group, users, opts)
  274. end
  275. end
  276. defp insert_activity(:reblog, visibility, group, users, opts) do
  277. actor = get_actor(group, users)
  278. with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
  279. {:ok, _activity} <- CommonAPI.repeat(activity_id, actor) do
  280. :ok
  281. else
  282. {:error, _} ->
  283. insert_activity(:reblog, visibility, group, users, opts)
  284. nil ->
  285. Process.sleep(15)
  286. insert_activity(:reblog, visibility, group, users, opts)
  287. end
  288. end
  289. defp insert_activity(:simple_thread, "direct", group, users, _opts) do
  290. actor = get_actor(group, users)
  291. tasks = get_reply_tasks("direct", group)
  292. list =
  293. case group do
  294. :user ->
  295. group = Enum.random(@friends_groups)
  296. Enum.take(users[group], 3)
  297. _ ->
  298. Enum.take(users[group], 3)
  299. end
  300. data = Enum.map(list, &("@" <> &1.nickname))
  301. {:ok, activity} =
  302. CommonAPI.post(actor, %{
  303. status: Enum.join(data, ", ") <> "simple status",
  304. visibility: "direct"
  305. })
  306. acc = {activity.id, ["@" <> users[:user].nickname | data] ++ ["reply to status"]}
  307. insert_direct_replies(tasks, users[:user], list, acc)
  308. end
  309. defp insert_activity(:simple_thread, visibility, group, users, _opts) do
  310. actor = get_actor(group, users)
  311. tasks = get_reply_tasks(visibility, group)
  312. {:ok, activity} =
  313. CommonAPI.post(users[:user], %{status: "Simple status", visibility: visibility})
  314. acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
  315. insert_replies(tasks, visibility, users, acc)
  316. end
  317. defp get_actor(:user, %{user: user}), do: user
  318. defp get_actor(group, users), do: Enum.random(users[group])
  319. defp other_data(actor, content) do
  320. %{host: host} = URI.parse(actor.ap_id)
  321. datetime = DateTime.utc_now() |> to_string()
  322. context_id = "https://#{host}/contexts/#{UUID.generate()}"
  323. activity_id = "https://#{host}/activities/#{UUID.generate()}"
  324. object_id = "https://#{host}/objects/#{UUID.generate()}"
  325. activity_data = %{
  326. "actor" => actor.ap_id,
  327. "context" => context_id,
  328. "id" => activity_id,
  329. "published" => datetime,
  330. "type" => "Create",
  331. "directMessage" => false
  332. }
  333. object_data = %{
  334. "actor" => actor.ap_id,
  335. "attachment" => [],
  336. "attributedTo" => actor.ap_id,
  337. "bcc" => [],
  338. "bto" => [],
  339. "content" => content,
  340. "context" => context_id,
  341. "conversation" => context_id,
  342. "emoji" => %{},
  343. "id" => object_id,
  344. "published" => datetime,
  345. "sensitive" => false,
  346. "summary" => "",
  347. "tag" => [],
  348. "to" => ["https://www.w3.org/ns/activitystreams#Public"],
  349. "type" => "Note"
  350. }
  351. {activity_data, object_data}
  352. end
  353. defp prepare_activity_data(actor, "public", _mention) do
  354. obj_data = %{
  355. "cc" => [actor.follower_address],
  356. "to" => [Constants.as_public()]
  357. }
  358. act_data = %{
  359. "cc" => [actor.follower_address],
  360. "to" => [Constants.as_public()]
  361. }
  362. {act_data, obj_data}
  363. end
  364. defp prepare_activity_data(actor, "private", _mention) do
  365. obj_data = %{
  366. "cc" => [],
  367. "to" => [actor.follower_address]
  368. }
  369. act_data = %{
  370. "cc" => [],
  371. "to" => [actor.follower_address]
  372. }
  373. {act_data, obj_data}
  374. end
  375. defp prepare_activity_data(actor, "unlisted", _mention) do
  376. obj_data = %{
  377. "cc" => [Constants.as_public()],
  378. "to" => [actor.follower_address]
  379. }
  380. act_data = %{
  381. "cc" => [Constants.as_public()],
  382. "to" => [actor.follower_address]
  383. }
  384. {act_data, obj_data}
  385. end
  386. defp prepare_activity_data(_actor, "direct", mention) do
  387. %{host: mentioned_host} = URI.parse(mention.ap_id)
  388. obj_data = %{
  389. "cc" => [],
  390. "content" =>
  391. "<span class=\"h-card\"><a class=\"u-url mention\" href=\"#{mention.ap_id}\" rel=\"ugc\">@<span>#{
  392. mention.nickname
  393. }</span></a></span> direct message",
  394. "tag" => [
  395. %{
  396. "href" => mention.ap_id,
  397. "name" => "@#{mention.nickname}@#{mentioned_host}",
  398. "type" => "Mention"
  399. }
  400. ],
  401. "to" => [mention.ap_id]
  402. }
  403. act_data = %{
  404. "cc" => [],
  405. "directMessage" => true,
  406. "to" => [mention.ap_id]
  407. }
  408. {act_data, obj_data}
  409. end
  410. defp get_reply_tasks("public", :user) do
  411. [:friends_local, :friends_remote, :non_friends_local, :non_friends_remote, :user]
  412. end
  413. defp get_reply_tasks("public", group) when group in @friends_groups do
  414. [:non_friends_local, :non_friends_remote, :user, :friends_local, :friends_remote]
  415. end
  416. defp get_reply_tasks("public", group) when group in @non_friends_groups do
  417. [:user, :friends_local, :friends_remote, :non_friends_local, :non_friends_remote]
  418. end
  419. defp get_reply_tasks(visibility, :user) when visibility in ["unlisted", "private"] do
  420. [:friends_local, :friends_remote, :user, :friends_local, :friends_remote]
  421. end
  422. defp get_reply_tasks(visibility, group)
  423. when visibility in ["unlisted", "private"] and group in @friends_groups do
  424. [:user, :friends_remote, :friends_local, :user]
  425. end
  426. defp get_reply_tasks(visibility, group)
  427. when visibility in ["unlisted", "private"] and
  428. group in @non_friends_groups,
  429. do: []
  430. defp get_reply_tasks("direct", :user), do: [:friends_local, :user, :friends_remote]
  431. defp get_reply_tasks("direct", group) when group in @friends_groups,
  432. do: [:user, group, :user]
  433. defp get_reply_tasks("direct", group) when group in @non_friends_groups do
  434. [:user, :non_friends_remote, :user, :non_friends_local]
  435. end
  436. defp insert_replies(tasks, visibility, users, acc) do
  437. Enum.reduce(tasks, acc, fn
  438. :user, {id, data} ->
  439. insert_reply(users[:user], data, id, visibility)
  440. group, {id, data} ->
  441. replier = Enum.random(users[group])
  442. insert_reply(replier, data, id, visibility)
  443. end)
  444. end
  445. defp insert_direct_replies(tasks, user, list, acc) do
  446. Enum.reduce(tasks, acc, fn
  447. :user, {id, data} ->
  448. {reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct")
  449. {reply_id, data}
  450. _, {id, data} ->
  451. actor = Enum.random(list)
  452. {reply_id, _} =
  453. insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct")
  454. {reply_id, data}
  455. end)
  456. end
  457. defp insert_reply(actor, data, activity_id, visibility) do
  458. {:ok, reply} =
  459. CommonAPI.post(actor, %{
  460. status: Enum.join(data, ", "),
  461. visibility: visibility,
  462. in_reply_to_status_id: activity_id
  463. })
  464. {reply.id, ["@" <> actor.nickname | data]}
  465. end
  466. defp get_random_mentions(_users, count) when count == 0, do: []
  467. defp get_random_mentions(users, count) do
  468. users
  469. |> Enum.shuffle()
  470. |> Enum.take(count)
  471. |> Enum.map(&"@#{&1.nickname}")
  472. end
  473. defp get_random_create_activity_id do
  474. Repo.one(
  475. from(a in Pleroma.Activity,
  476. where: fragment("(?)->>'type' = ?", a.data, ^"Create"),
  477. order_by: fragment("RANDOM()"),
  478. limit: 1,
  479. select: a.id
  480. )
  481. )
  482. end
  483. end