Guest User

Untitled

a guest
Mar 6th, 2026
31
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.78 KB | None | 0 0
  1. # Benchmark: 3 versions of IEx.Autocomplete internals
  2. # Run with: bin/elixir bench_autocomplete.exs
  3.  
  4. defmodule Bench do
  5. def measure(label, fun, iterations \\ 500) do
  6. # Warmup
  7. for _ <- 1..10, do: fun.()
  8.  
  9. # Measure time
  10. :erlang.garbage_collect()
  11. {time_us, _} = :timer.tc(fn ->
  12. for _ <- 1..iterations, do: fun.()
  13. end)
  14. avg_us = time_us / iterations
  15.  
  16. # Measure memory: total bytes allocated across N calls
  17. # Use binary_memory + heap tracking
  18. :erlang.garbage_collect()
  19. {:total_heap_size, heap0} = :erlang.process_info(self(), :total_heap_size)
  20. {:garbage_collection, gc_info0} = :erlang.process_info(self(), :garbage_collection)
  21. gcs0 = Keyword.get(gc_info0, :minor_gcs, 0)
  22. {:reductions, reds0} = :erlang.process_info(self(), :reductions)
  23.  
  24. n = 50
  25. for _ <- 1..n, do: fun.()
  26.  
  27. {:total_heap_size, heap1} = :erlang.process_info(self(), :total_heap_size)
  28. {:garbage_collection, gc_info1} = :erlang.process_info(self(), :garbage_collection)
  29. gcs1 = Keyword.get(gc_info1, :minor_gcs, 0)
  30. {:reductions, reds1} = :erlang.process_info(self(), :reductions)
  31.  
  32. heap_words = div(heap1 - heap0, n)
  33. gcs = gcs1 - gcs0
  34. reds = div(reds1 - reds0, n)
  35.  
  36. # Also measure with GC forced to see retained memory
  37. :erlang.garbage_collect()
  38. {:heap_size, retained0} = :erlang.process_info(self(), :heap_size)
  39. fun.()
  40. {:heap_size, retained1} = :erlang.process_info(self(), :heap_size)
  41.  
  42. {avg_us, heap_words * 8, reds, gcs, (retained1 - retained0) * 8}
  43. end
  44.  
  45. def compare(label, implementations, iterations \\ 500) do
  46. IO.puts(label)
  47. IO.puts(String.duplicate("-", 100))
  48. :io.format(" ~-30s ~10s ~12s ~10s ~8s ~12s~n",
  49. ["version", "time(µs)", "heap(bytes)", "reds", "GCs", "retained(B)"])
  50. :io.format(" ~-30s ~10s ~12s ~10s ~8s ~12s~n",
  51. ["-------", "--------", "-----------", "----", "---", "-----------"])
  52.  
  53. results = for {name, fun} <- implementations do
  54. {avg_us, heap_bytes, reds, gcs, retained} = measure(name, fun, iterations)
  55. :io.format(" ~-30s ~10.1f ~12B ~10B ~8B ~12B~n",
  56. [name, avg_us, heap_bytes, reds, gcs, retained])
  57. {name, avg_us, heap_bytes}
  58. end
  59.  
  60. # Show speedups relative to first
  61. [{base_name, base_time, base_mem} | rest] = results
  62. IO.puts("")
  63. for {name, time, mem} <- rest do
  64. time_x = if time > 0, do: Float.round(base_time / time, 1), else: :inf
  65. mem_x = if mem != 0 and base_mem != 0, do: Float.round(base_mem / mem, 1), else: "-"
  66. IO.puts(" #{name} vs #{base_name}: #{time_x}x faster, #{mem_x}x less heap")
  67. end
  68. IO.puts("")
  69. end
  70. end
  71.  
  72. for mod <- [Enum, String, Map, Kernel, IO, File, Path, Agent, GenServer, Supervisor, Task,
  73. Logger, Macro, Module, Code, Regex, Range, Stream, URI, Access, Calendar,
  74. Date, DateTime, Time, NaiveDateTime, Keyword, List, Tuple, MapSet,
  75. Integer, Float, System, Process, Node, Port, Registry, DynamicSupervisor] do
  76. Code.ensure_loaded(mod)
  77. end
  78.  
  79. loaded_count = length(:erlang.loaded())
  80. IO.puts("=== IEx Autocomplete Benchmark — 3 versions (#{loaded_count} loaded modules) ===\n")
  81.  
  82. # ============================================================
  83. # Version 1: BEFORE PR (:code.all_loaded + Enum.sort + dedup)
  84. # ============================================================
  85. before_get_modules = fn elixir_root? ->
  86. modules =
  87. if elixir_root? do
  88. ["Elixir.Elixir" | Enum.map(:code.all_loaded(), &Atom.to_string(elem(&1, 0)))]
  89. else
  90. Enum.map(:code.all_loaded(), &Atom.to_string(elem(&1, 0)))
  91. end
  92.  
  93. case :code.get_mode() do
  94. :interactive ->
  95. apps = for [app] <- :ets.match(:ac_tab, {{:loaded, :"$1"}, :_}),
  96. {:ok, mods} = :application.get_key(app, :modules),
  97. mod <- mods, do: Atom.to_string(mod)
  98. modules ++ apps
  99. _ -> modules
  100. end
  101. end
  102.  
  103. before_match_modules = fn hint, elixir_root? ->
  104. before_get_modules.(elixir_root?)
  105. |> Enum.sort()
  106. |> Enum.dedup()
  107. |> Enum.drop_while(&(not String.starts_with?(&1, hint)))
  108. |> Enum.take_while(&String.starts_with?(&1, hint))
  109. end
  110.  
  111. before_match_elixir_modules = fn module, hint ->
  112. name = Atom.to_string(module)
  113. depth = length(String.split(name, ".")) + 1
  114. base = name <> "." <> hint
  115.  
  116. for mod <- before_match_modules.(base, module == Elixir),
  117. parts = String.split(mod, "."),
  118. depth <= length(parts),
  119. name = Enum.at(parts, depth - 1),
  120. uniq: true,
  121. do: name
  122. end
  123.  
  124. # ============================================================
  125. # Version 2: CURRENT (:erlang.loaded + :lists.usort + binary_part)
  126. # ============================================================
  127. current_get_modules = fn elixir_root? ->
  128. modules =
  129. if elixir_root? do
  130. ["Elixir.Elixir" | Enum.map(:erlang.loaded(), &Atom.to_string/1)]
  131. else
  132. Enum.map(:erlang.loaded(), &Atom.to_string/1)
  133. end
  134.  
  135. case :code.get_mode() do
  136. :interactive ->
  137. apps = for [app] <- :ets.match(:ac_tab, {{:loaded, :"$1"}, :_}),
  138. {:ok, mods} = :application.get_key(app, :modules),
  139. mod <- mods, do: Atom.to_string(mod)
  140. modules ++ apps
  141. _ -> modules
  142. end
  143. end
  144.  
  145. current_match_modules = fn hint, elixir_root? ->
  146. current_get_modules.(elixir_root?)
  147. |> :lists.usort()
  148. |> Enum.drop_while(&(not String.starts_with?(&1, hint)))
  149. |> Enum.take_while(&String.starts_with?(&1, hint))
  150. end
  151.  
  152. elixir_submodule_name = fn rest ->
  153. case :binary.match(rest, ".") do
  154. {pos, _} -> binary_part(rest, 0, pos)
  155. :nomatch -> rest
  156. end
  157. end
  158.  
  159. current_match_elixir_modules = fn module, hint ->
  160. prefix = Atom.to_string(module) <> "."
  161. prefix_size = byte_size(prefix)
  162. base = prefix <> hint
  163.  
  164. for mod <- current_match_modules.(base, module == Elixir),
  165. rest = binary_part(mod, prefix_size, byte_size(mod) - prefix_size),
  166. name = elixir_submodule_name.(rest),
  167. uniq: true,
  168. do: name
  169. end
  170.  
  171. # ============================================================
  172. # Version 3: PROPOSED (filter during collection, sort matches only)
  173. # ============================================================
  174. proposed_match_modules = fn hint, elixir_root? ->
  175. modules =
  176. if elixir_root? do
  177. acc = for mod <- :erlang.loaded(),
  178. str = Atom.to_string(mod),
  179. String.starts_with?(str, hint),
  180. do: str
  181. if String.starts_with?("Elixir.Elixir", hint), do: ["Elixir.Elixir" | acc], else: acc
  182. else
  183. for mod <- :erlang.loaded(),
  184. str = Atom.to_string(mod),
  185. String.starts_with?(str, hint),
  186. do: str
  187. end
  188.  
  189. modules =
  190. case :code.get_mode() do
  191. :interactive ->
  192. apps = for [app] <- :ets.match(:ac_tab, {{:loaded, :"$1"}, :_}),
  193. {:ok, mods} = :application.get_key(app, :modules),
  194. mod <- mods,
  195. str = Atom.to_string(mod),
  196. String.starts_with?(str, hint),
  197. do: str
  198. modules ++ apps
  199. _ -> modules
  200. end
  201.  
  202. :lists.usort(modules)
  203. end
  204.  
  205. proposed_match_elixir_modules = fn module, hint ->
  206. prefix = Atom.to_string(module) <> "."
  207. prefix_size = byte_size(prefix)
  208. base = prefix <> hint
  209.  
  210. for mod <- proposed_match_modules.(base, module == Elixir),
  211. rest = binary_part(mod, prefix_size, byte_size(mod) - prefix_size),
  212. name = elixir_submodule_name.(rest),
  213. uniq: true,
  214. do: name
  215. end
  216.  
  217. # ============================================================
  218. # Verify correctness
  219. # ============================================================
  220. IO.puts("Verifying correctness...")
  221. for {hint, root?} <- [{"Elixir.S", true}, {"Elixir.Enum", true}, {"er", false}, {"Elixir.", true}] do
  222. r1 = before_match_modules.(hint, root?)
  223. r2 = current_match_modules.(hint, root?)
  224. r3 = proposed_match_modules.(hint, root?)
  225. if r1 == r2 and r2 == r3 do
  226. IO.puts(" ✓ match_modules(#{inspect(hint)}) — #{length(r1)} results")
  227. else
  228. IO.puts(" ✗ match_modules(#{inspect(hint)}) MISMATCH: #{length(r1)} / #{length(r2)} / #{length(r3)}")
  229. end
  230. end
  231.  
  232. for {mod, hint} <- [{Elixir, "S"}, {Elixir, "Enum"}, {Elixir, ""}] do
  233. r1 = before_match_elixir_modules.(mod, hint)
  234. r2 = current_match_elixir_modules.(mod, hint)
  235. r3 = proposed_match_elixir_modules.(mod, hint)
  236. if Enum.sort(r1) == Enum.sort(r2) and Enum.sort(r2) == Enum.sort(r3) do
  237. IO.puts(" ✓ match_elixir_modules(#{inspect(mod)}, #{inspect(hint)}) — #{length(r1)} results")
  238. else
  239. IO.puts(" ✗ match_elixir_modules(#{inspect(mod)}, #{inspect(hint)}) MISMATCH: #{length(r1)} / #{length(r2)} / #{length(r3)}")
  240. IO.puts(" before: #{inspect(Enum.sort(r1), limit: 5)}")
  241. IO.puts(" current: #{inspect(Enum.sort(r2), limit: 5)}")
  242. IO.puts(" proposed: #{inspect(Enum.sort(r3), limit: 5)}")
  243. end
  244. end
  245. IO.puts("")
  246.  
  247. # ============================================================
  248. # Benchmarks
  249. # ============================================================
  250.  
  251. Bench.compare("match_modules(\"Elixir.S\", true) — broad Elixir prefix", [
  252. {"v1: before PR", fn -> before_match_modules.("Elixir.S", true) end},
  253. {"v2: current", fn -> current_match_modules.("Elixir.S", true) end},
  254. {"v3: proposed", fn -> proposed_match_modules.("Elixir.S", true) end},
  255. ])
  256.  
  257. Bench.compare("match_modules(\"Elixir.Enum\", true) — specific module", [
  258. {"v1: before PR", fn -> before_match_modules.("Elixir.Enum", true) end},
  259. {"v2: current", fn -> current_match_modules.("Elixir.Enum", true) end},
  260. {"v3: proposed", fn -> proposed_match_modules.("Elixir.Enum", true) end},
  261. ])
  262.  
  263. Bench.compare("match_modules(\"Elixir.\", true) — all Elixir modules (worst case)", [
  264. {"v1: before PR", fn -> before_match_modules.("Elixir.", true) end},
  265. {"v2: current", fn -> current_match_modules.("Elixir.", true) end},
  266. {"v3: proposed", fn -> proposed_match_modules.("Elixir.", true) end},
  267. ])
  268.  
  269. Bench.compare("match_modules(\"er\", false) — erlang prefix", [
  270. {"v1: before PR", fn -> before_match_modules.("er", false) end},
  271. {"v2: current", fn -> current_match_modules.("er", false) end},
  272. {"v3: proposed", fn -> proposed_match_modules.("er", false) end},
  273. ])
  274.  
  275. Bench.compare("match_elixir_modules(Elixir, \"S\") — full submodule extraction", [
  276. {"v1: before PR", fn -> before_match_elixir_modules.(Elixir, "S") end},
  277. {"v2: current", fn -> current_match_elixir_modules.(Elixir, "S") end},
  278. {"v3: proposed", fn -> proposed_match_elixir_modules.(Elixir, "S") end},
  279. ])
  280.  
  281. Bench.compare("match_elixir_modules(Elixir, \"\") — all modules (tab after .)", [
  282. {"v1: before PR", fn -> before_match_elixir_modules.(Elixir, "") end},
  283. {"v2: current", fn -> current_match_elixir_modules.(Elixir, "") end},
  284. {"v3: proposed", fn -> proposed_match_elixir_modules.(Elixir, "") end},
  285. ])
  286.  
  287. IO.puts("=== Done ===")
  288.  
Advertisement
Add Comment
Please, Sign In to add comment