diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b6bccf..d366d33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,8 @@ jobs: name: Test strategy: matrix: - otp: ["26.1.1"] - elixir: ["1.16.3"] + otp: ["28.1.1"] + elixir: ["1.19.2"] steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 5810154..ee573a8 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,8 @@ flame_on-*.tar .DS_Store -/plts/ \ No newline at end of file +/plts/ + +.envrc + +.claude/ \ No newline at end of file diff --git a/.tool-versions b/.tool-versions index 5a65dd8..8081b2b 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.15.6-otp-26 -erlang 26.1.1 \ No newline at end of file +elixir 1.19.2-otp-28 +erlang 28.1.1 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 75c5795..5b5165c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Next + +- Upgrade LiveView to v1.0 +- Update Elixir and Erlang versions +- Update gettext usage +- Update arity for Bandit.Pipeline default run function +- Update function heads to address type warnings +- Add comprehensive tests + ## 0.7.0 - Add "Download graph as SVG" feature diff --git a/lib/flame_on/capture/server.ex b/lib/flame_on/capture/server.ex index 8ea6de2..cb6ec1e 100644 --- a/lib/flame_on/capture/server.ex +++ b/lib/flame_on/capture/server.ex @@ -73,7 +73,7 @@ defmodule FlameOn.Capture.Server do {:noreply, %State{state | stack: stack}} end - def handle_info(:do_stop_trace, state) do + def handle_info(:do_stop_trace, %State{} = state) do state = %State{state | stack: Stack.finalize_stack(state.stack)} [root_block] = state.stack send_update(state.config, root_block) @@ -109,5 +109,5 @@ defmodule FlameOn.Capture.Server do :ok = :meck.expect(config.module, config.function, fun) end - def microseconds({mega, secs, micro}), do: mega * 1000 * 1000 * 1000 * 1000 + secs * 1000 * 1000 + micro + def microseconds({mega, secs, micro}), do: mega * 1_000_000_000_000 + secs * 1_000_000 + micro end diff --git a/lib/flame_on/capture/server/stack.ex b/lib/flame_on/capture/server/stack.ex index 3bd15e9..ebee70c 100644 --- a/lib/flame_on/capture/server/stack.ex +++ b/lib/flame_on/capture/server/stack.ex @@ -1,7 +1,18 @@ defmodule FlameOn.Capture.Server.Stack do alias FlameOn.Capture.Block - def finalize_stack([root_block]) do + def finalize_stack([%Block{children: []} = root_block]) do + root_block = %Block{ + root_block + | duration: 0 + } + + root_block = populate_levels(root_block, 0) + + [root_block] + end + + def finalize_stack([%Block{} = root_block]) do children = Enum.reverse(root_block.children) absolute_start = hd(children).absolute_start last_child = List.last(children) diff --git a/lib/flame_on/component/capture_schema.ex b/lib/flame_on/component/capture_schema.ex index fbcba4e..bfb91af 100644 --- a/lib/flame_on/component/capture_schema.ex +++ b/lib/flame_on/component/capture_schema.ex @@ -13,7 +13,7 @@ defmodule FlameOn.Component.CaptureSchema do end @default_cowboy_attrs %{module: "cowboy_handler", function: "execute", arity: 2, timeout: 15000} - @default_bandit_attrs %{module: "Bandit.Pipeline", function: "run", arity: 4, timeout: 15000} + @default_bandit_attrs %{module: "Bandit.Pipeline", function: "run", arity: 5, timeout: 15000} def changeset(node, attrs \\ nil) do attrs = attrs || default_attrs(node) diff --git a/lib/flame_on/gettext.ex b/lib/flame_on/gettext.ex index 7cfed43..3bcec57 100644 --- a/lib/flame_on/gettext.ex +++ b/lib/flame_on/gettext.ex @@ -20,5 +20,5 @@ defmodule FlameOn.Gettext do See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. """ - use Gettext, otp_app: :flame_on + use Gettext.Backend, otp_app: :flame_on end diff --git a/mix.exs b/mix.exs index 6bd46b4..75cae09 100644 --- a/mix.exs +++ b/mix.exs @@ -56,7 +56,7 @@ defmodule FlameOn.MixProject do [ {:ets, "~> 0.9.0"}, {:dialyxir, "~> 1.4.1", only: [:dev, :test], runtime: false}, - {:mix_test_watch, "~> 1.1.0", only: :dev, runtime: false}, + {:mix_test_watch, "~> 1.4", only: :dev, runtime: false}, {:ex_doc, "~> 0.28.0", only: :dev, runtime: false}, {:ecto, "~> 3.7"}, {:meck, "~> 0.9.2"}, diff --git a/mix.lock b/mix.lock index 7876a46..338c1c8 100644 --- a/mix.lock +++ b/mix.lock @@ -7,23 +7,23 @@ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ex_doc": {:hex, :ex_doc, "0.28.0", "7eaf526dd8c80ae8c04d52ac8801594426ae322b52a6156cd038f30bafa8226f", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e55cdadf69a5d1f4cfd8477122ebac5e1fadd433a8c1022dafc5025e48db0131"}, - "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "gettext": {:hex, :gettext, "0.23.1", "821e619a240e6000db2fc16a574ef68b3bd7fe0167ccc264a81563cc93e67a31", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "19d744a36b809d810d610b57c27b934425859d158ebd56561bc41f7eeb8795db"}, + "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, + "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, - "mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"}, + "mix_test_watch": {:hex, :mix_test_watch, "1.4.0", "d88bcc4fbe3198871266e9d2f00cd8ae350938efbb11d3fa1da091586345adbb", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "2b4693e17c8ead2ef56d4f48a0329891e8c2d0d73752c0f09272a2b17dc38d1b"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.2", "b99ca56bbce410e9d5ee4f9155a212e942e224e259c7ebbf8f2c86ac21d4fa3c", [:mix], [], "hexpm", "98d51bd64d5f6a2a9c6bb7586ee8129e27dfaab1140b5a4753f24dac0ba27d2f"}, "phoenix": {:hex, :phoenix, "1.7.19", "36617efe5afbd821099a8b994ff4618a340a5bfb25531a1802c4d4c634017a57", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ba4dc14458278773f905f8ae6c2ec743d52c3a35b6b353733f64f02dfe096cd6"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.1", "96798325fab2fed5a824ca204e877b81f9afd2e480f581e81f7b4b64a5a477f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "0ae544ff99f3c482b0807c5cec2c8289e810ecacabc04959d82c3337f4703391"}, "phoenix_html": {:hex, :phoenix_html, "4.2.0", "83a4d351b66f472ebcce242e4ae48af1b781866f00ef0eb34c15030d4e2069ac", [:mix], [], "hexpm", "9713b3f238d07043583a94296cc4bbdceacd3b3a6c74667f4df13971e7866ec8"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.4", "327491b033e79db2f887b065c5a2993228449091883d74cfa1baa12f8c98d5eb", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a9865316ddf8d78f382d63af278d20436b52d262b60239956817a61279514366"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.17", "1d782b5901cf13b137c6d8c56542ff6cb618359b2adca7e185b21df728fa0c6c", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fa82307dd9305657a8236d6b48e60ef2e8d9f742ee7ed832de4b8bcb7e0e5ed2"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, diff --git a/test/flame_on/capture/server/stack_test.exs b/test/flame_on/capture/server/stack_test.exs new file mode 100644 index 0000000..0c6e809 --- /dev/null +++ b/test/flame_on/capture/server/stack_test.exs @@ -0,0 +1,482 @@ +defmodule FlameOn.Capture.Server.StackTest do + use ExUnit.Case, async: true + + alias FlameOn.Capture.Block + alias FlameOn.Capture.Server.Stack + alias FlameOn.Fixtures + + describe "handle_trace_call/3" do + test "adds a new block to the stack" do + stack = [] + function = {:example, :foo, 0} + timestamp = 1000 + + result = Stack.handle_trace_call(stack, function, timestamp) + + assert [%Block{} = block] = result + assert block.function == function + assert block.absolute_start == timestamp + assert block.level == 0 + assert block.children == [] + assert is_binary(block.id) + end + + test "adds block to existing stack" do + parent = Fixtures.simple_block(function: {:example, :parent, 0}) + stack = [parent] + function = {:example, :child, 0} + timestamp = 2000 + + result = Stack.handle_trace_call(stack, function, timestamp) + + assert [%Block{} = child_block, ^parent] = result + assert child_block.function == function + assert child_block.absolute_start == timestamp + assert child_block.level == 1 + end + + test "generates unique IDs for each block" do + stack = [] + function = {:example, :foo, 0} + + stack1 = Stack.handle_trace_call(stack, function, 1000) + stack2 = Stack.handle_trace_call(stack, function, 1000) + + [block1] = stack1 + [block2] = stack2 + + assert block1.id != block2.id + end + end + + describe "handle_trace_return_to/3" do + test "pops a block from the stack and attaches it to parent" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :parent, 0}, 1000}, + {:call, {:example, :child, 0}, 1100} + ]) + + result = Stack.handle_trace_return_to(stack, {:example, :parent, 0}, 1200) + + assert [%Block{function: {:example, :parent, 0}} = parent] = result + assert [%Block{function: {:example, :child, 0}} = child] = parent.children + assert child.duration == 100 + assert child.absolute_start == 1100 + end + + test "reverses children when popping" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :parent, 0}, 1000}, + {:call, {:example, :child1, 0}, 1100}, + {:return_to, {:example, :parent, 0}, 1200}, + {:call, {:example, :child2, 0}, 1300} + ]) + + result = Stack.handle_trace_return_to(stack, {:example, :parent, 0}, 1400) + + assert [%Block{function: {:example, :parent, 0}} = parent] = result + assert [child2, child1] = parent.children + assert child1.function == {:example, :child1, 0} + assert child2.function == {:example, :child2, 0} + end + + test "handles sleep/sleep return pattern" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :foo, 0}, 1000}, + {:call, :sleep, 1100} + ]) + + result = Stack.handle_trace_return_to(stack, :sleep, 1200) + + assert [%Block{function: {:example, :foo, 0}} = parent] = result + assert [%Block{function: :sleep} = sleep_block] = parent.children + assert sleep_block.duration == 100 + end + + test "handles returning to starter block" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :starter, 0}, 1000}, + {:call, {:example, :child, 0}, 1100} + ]) + + result = Stack.handle_trace_return_to(stack, {:example, :different, 0}, 1200) + + assert [%Block{function: {:example, :starter, 0}}] = result + end + + test "handles tail recursion with pruning" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :recursive, 1}, 1000}, + {:call, {:example, :recursive, 1}, 1100}, + {:call, {:example, :recursive, 1}, 1200} + ]) + + result = Stack.handle_trace_return_to(stack, {:example, :recursive, 1}, 1300) + + assert length(result) == 2 + [second, first] = result + assert first.function == {:example, :recursive, 1} + assert second.function == {:example, :recursive, 1} + assert [%Block{function: {:example, :recursive, 1}}] = second.children + end + end + + describe "finalize_stack/1" do + test "finalizes a single root block with children" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :root, 0}, 1000}, + {:call, {:example, :child1, 0}, 1100}, + {:return_to, {:example, :root, 0}, 1200}, + {:call, {:example, :child2, 0}, 1300}, + {:return_to, {:example, :root, 0}, 1400} + ]) + + [root] = Stack.finalize_stack(stack) + + assert root.function == {:example, :root, 0} + assert root.absolute_start == 1100 + assert root.duration == 300 + assert length(root.children) == 2 + assert root.level == 1 + assert root.max_child_level == 1 + end + + test "reverses children in final stack" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :root, 0}, 1000}, + {:call, {:example, :child1, 0}, 1100}, + {:return_to, {:example, :root, 0}, 1200}, + {:call, {:example, :child2, 0}, 1300}, + {:return_to, {:example, :root, 0}, 1400} + ]) + + [root] = Stack.finalize_stack(stack) + + [first_child, second_child] = root.children + assert first_child.function == {:example, :child1, 0} + assert second_child.function == {:example, :child2, 0} + end + + test "collapses leftover stack nodes to root" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :root, 0}, 1000}, + {:call, {:example, :child, 0}, 1100} + ]) + + [root] = Stack.finalize_stack(stack) + + assert root.function == {:example, :root, 0} + assert [child] = root.children + assert child.function == {:example, :child, 0} + end + + test "calculates correct duration from children" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :root, 0}, 1000}, + {:call, {:example, :child1, 0}, 1100}, + {:return_to, {:example, :root, 0}, 1250}, + {:call, {:example, :child2, 0}, 1300}, + {:return_to, {:example, :root, 0}, 1500} + ]) + + [root] = Stack.finalize_stack(stack) + + assert root.absolute_start == 1100 + assert root.duration == 400 + end + + test "handles deeply nested blocks" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :level1, 0}, 1000}, + {:call, {:example, :level2, 0}, 1100}, + {:call, {:example, :level3, 0}, 1200}, + {:return_to, {:example, :level2, 0}, 1300}, + {:return_to, {:example, :level1, 0}, 1400} + ]) + + [root] = Stack.finalize_stack(stack) + + assert root.level == 1 + assert root.max_child_level == 2 + + [level2] = root.children + assert level2.level == 2 + assert level2.max_child_level == 1 + + [level3] = level2.children + assert level3.level == 3 + assert level3.max_child_level == 0 + end + + test "handles block with single child" do + child = + Fixtures.simple_block( + function: {:example, :child, 0}, + absolute_start: 1100, + duration: 100, + children: [] + ) + + block = + Fixtures.simple_block( + function: {:example, :root, 0}, + absolute_start: 1000, + duration: 200, + children: [child] + ) + + [root] = Stack.finalize_stack([block]) + + assert root.level == 1 + assert root.max_child_level == 1 + assert length(root.children) == 1 + end + end + + describe "populate_levels" do + test "assigns level 1 to root with child" do + child = + Fixtures.simple_block( + function: {:example, :child, 0}, + absolute_start: 1100, + duration: 100, + children: [] + ) + + block = + Fixtures.simple_block( + function: {:example, :root, 0}, + absolute_start: 1000, + duration: 200, + children: [child] + ) + + [result] = Stack.finalize_stack([block]) + + assert result.level == 1 + assert result.max_child_level == 1 + end + + test "assigns incrementing levels to nested blocks" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :parent, 0}, 1000}, + {:call, {:example, :child, 0}, 1100}, + {:return_to, {:example, :parent, 0}, 1200} + ]) + + [root] = Stack.finalize_stack(stack) + + assert root.level == 1 + [child] = root.children + assert child.level == 2 + end + + test "calculates max_child_level correctly" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :root, 0}, 1000}, + {:call, {:example, :child1, 0}, 1100}, + {:call, {:example, :grandchild, 0}, 1150}, + {:return_to, {:example, :child1, 0}, 1175}, + {:return_to, {:example, :root, 0}, 1200}, + {:call, {:example, :child2, 0}, 1300}, + {:return_to, {:example, :root, 0}, 1400} + ]) + + [root] = Stack.finalize_stack(stack) + + assert root.max_child_level == 2 + + [child1, child2] = root.children + assert child1.max_child_level == 1 + assert child2.max_child_level == 0 + end + end + + describe "recursive call pruning" do + test "prunes when returning to non-parent with recursive pattern" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :base, 0}, 1000}, + {:call, {:example, :recursive, 1}, 1100}, + {:call, {:example, :recursive, 1}, 1200}, + {:call, {:example, :recursive, 1}, 1300} + ]) + + result = Stack.handle_trace_return_to(stack, {:example, :base, 0}, 1400) + + assert [%Block{function: {:example, :base, 0}} = base] = result + assert [%Block{function: {:example, :recursive, 1}} = top_recursive] = base.children + assert top_recursive.children == [] + end + + test "prunes multiple levels of recursion" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :base, 0}, 1000}, + {:call, {:example, :recursive, 1}, 1100}, + {:call, {:example, :recursive, 1}, 1200}, + {:call, {:example, :recursive, 1}, 1300}, + {:call, {:example, :recursive, 1}, 1400} + ]) + + result = Stack.handle_trace_return_to(stack, {:example, :base, 0}, 1500) + + assert [%Block{function: {:example, :base, 0}} = base] = result + assert [%Block{function: {:example, :recursive, 1}} = top_recursive] = base.children + assert top_recursive.children == [] + end + + test "does not prune when child has different function" do + child = + Fixtures.simple_block( + function: {:example, :different, 0}, + absolute_start: 1100, + duration: 100, + children: [] + ) + + parent = + Fixtures.simple_block( + function: {:example, :recursive, 1}, + absolute_start: 1000, + children: [child] + ) + + other_block = + Fixtures.simple_block( + function: {:example, :base, 0}, + absolute_start: 900 + ) + + stack = [parent, other_block] + + result = Stack.handle_trace_return_to(stack, {:example, :base, 0}, 1300) + + assert [%Block{function: {:example, :base, 0}} = base] = result + assert [%Block{function: {:example, :recursive, 1}} = recursive] = base.children + assert [%Block{function: {:example, :different, 0}}] = recursive.children + end + + test "does not prune when parent has multiple children" do + child1 = + Fixtures.simple_block( + function: {:example, :recursive, 1}, + absolute_start: 1100, + duration: 50, + children: [] + ) + + child2 = + Fixtures.simple_block( + function: {:example, :other, 0}, + absolute_start: 1200, + duration: 50, + children: [] + ) + + parent = + Fixtures.simple_block( + function: {:example, :recursive, 1}, + absolute_start: 1000, + children: [child1, child2] + ) + + base = + Fixtures.simple_block( + function: {:example, :base, 0}, + absolute_start: 900 + ) + + stack = [parent, base] + + result = Stack.handle_trace_return_to(stack, {:example, :base, 0}, 1300) + + assert [%Block{function: {:example, :base, 0}} = base_result] = result + assert [%Block{function: {:example, :recursive, 1}} = recursive] = base_result.children + assert length(recursive.children) == 2 + end + end + + describe "edge cases" do + test "handles single call/return pair" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :root, 0}, 1000}, + {:call, {:example, :foo, 0}, 1500} + ]) + + result = Stack.handle_trace_return_to(stack, {:example, :root, 0}, 2000) + [root] = Stack.finalize_stack(result) + + assert root.function == {:example, :root, 0} + [child] = root.children + assert child.function == {:example, :foo, 0} + assert child.duration == 500 + end + + test "handles zero duration blocks" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :parent, 0}, 1000}, + {:call, {:example, :child, 0}, 1000}, + {:return_to, {:example, :parent, 0}, 1000} + ]) + + [root] = Stack.finalize_stack(stack) + + [child] = root.children + assert child.duration == 0 + end + + test "handles erlang module functions" do + stack = + Fixtures.build_stack_from_trace_events([ + {:call, {:example, :root, 0}, 1000}, + {:call, {:timer, :sleep, 1}, 1500} + ]) + + result = Stack.handle_trace_return_to(stack, {:example, :root, 0}, 2000) + [root] = Stack.finalize_stack(result) + + assert root.function == {:example, :root, 0} + [child] = root.children + assert child.function == {:timer, :sleep, 1} + end + + test "handles complex call patterns" do + events = [ + {:call, {:example, :root, 0}, 1000}, + {:call, {:example, :child1, 0}, 1100}, + {:call, {:example, :grandchild1, 0}, 1150}, + {:return_to, {:example, :child1, 0}, 1200}, + {:return_to, {:example, :root, 0}, 1250}, + {:call, {:example, :child2, 0}, 1300}, + {:call, {:example, :grandchild2, 0}, 1350}, + {:return_to, {:example, :child2, 0}, 1400}, + {:return_to, {:example, :root, 0}, 1450} + ] + + stack = Fixtures.build_stack_from_trace_events(events) + [root] = Stack.finalize_stack(stack) + + assert root.function == {:example, :root, 0} + assert length(root.children) == 2 + + [child1, child2] = root.children + assert length(child1.children) == 1 + assert length(child2.children) == 1 + end + end +end diff --git a/test/flame_on/capture/server_test.exs b/test/flame_on/capture/server_test.exs new file mode 100644 index 0000000..03363ff --- /dev/null +++ b/test/flame_on/capture/server_test.exs @@ -0,0 +1,369 @@ +defmodule FlameOn.Capture.ServerTest do + use ExUnit.Case, async: false + import ExUnit.CaptureLog + + alias FlameOn.Capture.Config + alias FlameOn.Capture.Server + + setup do + on_exit(fn -> + if Process.whereis(Server) do + Process.exit(Process.whereis(Server), :kill) + Process.sleep(100) + end + + case ETS.Set.wrap_existing(Server) do + {:ok, set} -> ETS.Set.delete(set) + _ -> :ok + end + + :meck.unload() + end) + + :ok + end + + describe "microseconds/1" do + test "converts erlang timestamp to microseconds" do + timestamp = {1000, 500_000, 250_000} + result = Server.microseconds(timestamp) + + assert result == 1_000_500_000_250_000 + end + + test "converts zero timestamp" do + timestamp = {0, 0, 0} + result = Server.microseconds(timestamp) + + assert result == 0 + end + + test "converts small timestamp" do + timestamp = {0, 1, 1000} + result = Server.microseconds(timestamp) + + assert result == 1_001_000 + end + end + + describe "trace_started?/0" do + test "returns false on first call" do + config = build_config() + {:ok, _pid} = Server.start(config) + + refute Server.trace_started?() + end + + test "returns true on subsequent calls" do + config = build_config() + {:ok, _pid} = Server.start(config) + + Server.trace_started?() + assert Server.trace_started?() + assert Server.trace_started?() + end + + test "returns true when table not found" do + assert Server.trace_started?() + end + + test "sets flag to true after first call" do + config = build_config() + {:ok, _pid} = Server.start(config) + + first_result = Server.trace_started?() + second_result = Server.trace_started?() + + refute first_result + assert second_result + end + end + + describe "init/1" do + test "creates ETS table" do + config = build_config() + {:ok, _pid} = Server.start(config) + + assert {:ok, _set} = ETS.Set.wrap_existing(Server) + end + + test "sets up starter block in stack" do + config = build_config(module: FlameOnTest.ExampleModule, function: :foo, arity: 0) + {:ok, _pid} = Server.start(config) + + state = :sys.get_state(Server) + + assert [starter] = state.stack + assert starter.function == {FlameOnTest.ExampleModule, :foo, 0} + assert starter.id == "starter" + assert starter.absolute_start == 0 + end + + test "mocks the target function with :meck" do + config = build_config(module: FlameOnTest.ExampleModule) + {:ok, _pid} = Server.start(config) + + assert :meck.validate(FlameOnTest.ExampleModule) + end + + test "schedules timeout message" do + config = build_config(timeout: 100) + {:ok, pid} = Server.start(config) + + ref = Process.monitor(pid) + + assert_receive {:DOWN, ^ref, :process, ^pid, :normal}, 200 + end + end + + describe "handle_info trace messages" do + test "handles :call trace message" do + config = build_config() + {:ok, pid} = Server.start(config) + + send(pid, {:trace_ts, self(), :call, {:example, :foo, 0}, :arity, {0, 1000, 0}}) + Process.sleep(10) + + state = :sys.get_state(pid) + + assert length(state.stack) == 2 + [head | _] = state.stack + assert head.function == {:example, :foo, 0} + end + + test "handles :return_to trace message" do + config = build_config() + {:ok, pid} = Server.start(config) + + send(pid, {:trace_ts, self(), :call, {:example, :child, 0}, :arity, {0, 1000, 0}}) + Process.sleep(10) + send(pid, {:trace_ts, self(), :return_to, {:example, :root, 0}, {0, 2000, 0}}) + Process.sleep(10) + + state = :sys.get_state(pid) + + assert [starter] = state.stack + assert [child] = starter.children + assert child.function == {:example, :child, 0} + assert child.duration == 1_000_000_000 + end + + test "handles :out trace message for sleep" do + config = build_config() + {:ok, pid} = Server.start(config) + + log = + capture_log(fn -> + send(pid, {:trace_ts, self(), :out, {:example, :foo, 0}, {0, 1000, 0}}) + Process.sleep(10) + end) + + state = :sys.get_state(pid) + + assert length(state.stack) == 2 + [head | _] = state.stack + assert head.function == :sleep + assert log =~ "flame_on trace: out" + end + + test "handles :in trace message for sleep" do + config = build_config() + {:ok, pid} = Server.start(config) + + send(pid, {:trace_ts, self(), :out, {:example, :foo, 0}, {0, 1000, 0}}) + Process.sleep(10) + + log = + capture_log(fn -> + send(pid, {:trace_ts, self(), :in, {:example, :foo, 0}, {0, 2000, 0}}) + Process.sleep(10) + end) + + state = :sys.get_state(pid) + + assert [starter] = state.stack + assert [sleep_block] = starter.children + assert sleep_block.function == :sleep + assert log =~ "flame_on trace: in" + end + + test "logs trace messages at debug level" do + config = build_config() + {:ok, pid} = Server.start(config) + + log = + capture_log(fn -> + send(pid, {:trace_ts, self(), :call, {:example, :foo, 0}, :arity, {0, 1000, 0}}) + Process.sleep(10) + end) + + assert log =~ "flame_on trace: call" + assert log =~ "{:example, :foo, 0}" + end + end + + describe "handle_info :do_stop_trace" do + test "finalizes stack and sends update" do + test_pid = self() + config = build_config(reply_to: {:live_component, test_pid, "test-id"}) + {:ok, pid} = Server.start(config) + + send(pid, {:trace_ts, self(), :call, {:example, :child, 0}, :arity, {0, 1000, 0}}) + Process.sleep(10) + send(pid, {:trace_ts, self(), :return_to, {FlameOnTest.ExampleModule, :foo, 0}, {0, 2000, 0}}) + Process.sleep(10) + + ref = Process.monitor(pid) + send(pid, :do_stop_trace) + + assert_receive {:phoenix, :send_update, {{FlameOn.Component, "test-id"}, %{id: "test-id", flame_on_update: root_block}}}, 500 + + assert root_block.function == {FlameOnTest.ExampleModule, :foo, 0} + assert root_block.level == 1 + assert [child] = root_block.children + assert child.function == {:example, :child, 0} + assert child.duration == 1_000_000_000 + + assert_receive {:DOWN, ^ref, :process, ^pid, :normal} + end + + test "stops the server normally" do + config = build_config() + {:ok, pid} = Server.start(config) + + ref = Process.monitor(pid) + send(pid, :do_stop_trace) + + assert_receive {:DOWN, ^ref, :process, ^pid, :normal} + end + end + + describe "handle_info :timeout" do + test "sends timeout update and stops server" do + test_pid = self() + config = build_config(reply_to: {:live_component, test_pid, "test-id"}, timeout: 50) + {:ok, pid} = Server.start(config) + + ref = Process.monitor(pid) + + assert_receive {:phoenix, :send_update, {{FlameOn.Component, "test-id"}, %{id: "test-id", flame_on_timed_out: true}}}, 200 + assert_receive {:DOWN, ^ref, :process, ^pid, :normal} + end + + test "does not send flame_on_update on timeout" do + test_pid = self() + config = build_config(reply_to: {:live_component, test_pid, "test-id"}, timeout: 50) + {:ok, _pid} = Server.start(config) + + assert_receive {:phoenix, :send_update, {_, %{flame_on_timed_out: true}}}, 200 + refute_receive {:phoenix, :send_update, {_, %{flame_on_update: _}}} + end + end + + describe "handle_cast :stop_trace" do + test "schedules :do_stop_trace message" do + config = build_config() + {:ok, pid} = Server.start(config) + + Server.stop_trace() + + state = :sys.get_state(pid) + assert state.config == config + end + + test "eventually stops the server" do + config = build_config() + {:ok, pid} = Server.start(config) + + ref = Process.monitor(pid) + Server.stop_trace() + + assert_receive {:DOWN, ^ref, :process, ^pid, :normal}, 1200 + end + end + + describe "mock_function/1" do + test "creates a :meck mock for the module" do + config = build_config(module: FlameOnTest.ExampleModule, function: :foo) + + Server.mock_function(config) + + assert :meck.validate(FlameOnTest.ExampleModule) + end + + test "uses unstick and passthrough options" do + config = build_config(module: FlameOnTest.ExampleModule) + + Server.mock_function(config) + + assert :meck.validate(FlameOnTest.ExampleModule) + :meck.unload(FlameOnTest.ExampleModule) + end + end + + describe "integration" do + test "handles complete trace sequence" do + test_pid = self() + config = build_config(reply_to: {:live_component, test_pid, "integration-test"}) + {:ok, pid} = Server.start(config) + + send(pid, {:trace_ts, self(), :call, {:example, :parent, 0}, :arity, {0, 1000, 0}}) + Process.sleep(10) + send(pid, {:trace_ts, self(), :call, {:example, :child1, 0}, :arity, {0, 1100, 0}}) + Process.sleep(10) + send(pid, {:trace_ts, self(), :return_to, {:example, :parent, 0}, {0, 1200, 0}}) + Process.sleep(10) + send(pid, {:trace_ts, self(), :call, {:example, :child2, 0}, :arity, {0, 1300, 0}}) + Process.sleep(10) + send(pid, {:trace_ts, self(), :return_to, {:example, :parent, 0}, {0, 1400, 0}}) + Process.sleep(10) + + send(pid, :do_stop_trace) + + assert_receive {:phoenix, :send_update, {_, %{flame_on_update: root}}}, 500 + + assert [parent] = root.children + assert parent.function == {:example, :parent, 0} + assert [child1, child2] = parent.children + assert child1.function == {:example, :child1, 0} + assert child2.function == {:example, :child2, 0} + assert child1.duration == 100_000_000 + assert child2.duration == 100_000_000 + end + + test "handles sleep pattern" do + test_pid = self() + config = build_config(reply_to: {:live_component, test_pid, "sleep-test"}) + {:ok, pid} = Server.start(config) + + send(pid, {:trace_ts, self(), :call, {:example, :foo, 0}, :arity, {0, 1000, 0}}) + Process.sleep(10) + send(pid, {:trace_ts, self(), :out, {:example, :foo, 0}, {0, 1100, 0}}) + Process.sleep(10) + send(pid, {:trace_ts, self(), :in, {:example, :foo, 0}, {0, 1500, 0}}) + Process.sleep(10) + send(pid, {:trace_ts, self(), :return_to, {:example, :root, 0}, {0, 1600, 0}}) + Process.sleep(10) + + send(pid, :do_stop_trace) + + assert_receive {:phoenix, :send_update, {_, %{flame_on_update: root}}}, 500 + + assert [foo] = root.children + assert [sleep_block] = foo.children + assert sleep_block.function == :sleep + assert sleep_block.duration == 400_000_000 + end + end + + defp build_config(opts \\ []) do + %Config{ + module: Keyword.get(opts, :module, FlameOnTest.ExampleModule), + function: Keyword.get(opts, :function, :foo), + arity: Keyword.get(opts, :arity, 0), + timeout: Keyword.get(opts, :timeout, 5000), + target_node: Keyword.get(opts, :target_node, node()), + reply_to: Keyword.get(opts, :reply_to, {:live_component, self(), "test"}) + } + end +end diff --git a/test/flame_on/capture/trace_test.exs b/test/flame_on/capture/trace_test.exs new file mode 100644 index 0000000..9c9aaf2 --- /dev/null +++ b/test/flame_on/capture/trace_test.exs @@ -0,0 +1,57 @@ +defmodule FlameOn.Capture.TraceTest do + use ExUnit.Case, async: false + + alias FlameOn.Capture.Trace + + describe "start_trace/1" do + test "enables tracing on current process" do + tracer = spawn(fn -> :timer.sleep(1000) end) + + result = Trace.start_trace(tracer) + + assert result == 1 + + info = Process.info(self(), :trace) + assert {:trace, _} = info + end + + test "sets up trace patterns" do + tracer = spawn(fn -> :timer.sleep(1000) end) + + Trace.start_trace(tracer) + + info = :erlang.trace_info({:erlang, :spawn, 2}, :traced) + assert info == {:traced, :local} + end + + test "returns number of traced processes" do + tracer = spawn(fn -> :timer.sleep(1000) end) + + result = Trace.start_trace(tracer) + + assert is_integer(result) + assert result >= 1 + end + end + + describe "stop_trace/0" do + test "disables tracing on current process" do + tracer = spawn(fn -> :timer.sleep(1000) end) + Trace.start_trace(tracer) + + result = Trace.stop_trace() + + assert result == :ok + end + + test "removes all trace flags" do + tracer = spawn(fn -> :timer.sleep(1000) end) + Trace.start_trace(tracer) + + Trace.stop_trace() + + info = Process.info(self(), :trace) + assert info == {:trace, 0} + end + end +end diff --git a/test/flame_on/svg_test.exs b/test/flame_on/svg_test.exs new file mode 100644 index 0000000..d94bd99 --- /dev/null +++ b/test/flame_on/svg_test.exs @@ -0,0 +1,61 @@ +defmodule FlameOn.SVGTest do + use ExUnit.Case, async: true + + alias FlameOn.SVG + + describe "mfa_to_string/1" do + test "formats Elixir module function" do + result = SVG.mfa_to_string({Elixir.MyApp.MyModule, :my_function, 2}) + + assert result == "MyApp.MyModule.my_function/2" + end + + test "formats top-level Elixir module" do + result = SVG.mfa_to_string({MyModule, :foo, 0}) + + assert result == "MyModule.foo/0" + end + + test "formats Erlang module function" do + result = SVG.mfa_to_string({:timer, :sleep, 1}) + + assert result == "timer.sleep/1" + end + + test "formats nested Elixir module" do + result = SVG.mfa_to_string({MyApp.Deeply.Nested.Module, :func, 3}) + + assert result == "MyApp.Deeply.Nested.Module.func/3" + end + + test "handles non-tuple functions" do + result = SVG.mfa_to_string(:sleep) + + assert result == ":sleep" + end + + test "handles arity 0" do + result = SVG.mfa_to_string({MyModule, :no_args, 0}) + + assert result == "MyModule.no_args/0" + end + + test "handles large arity" do + result = SVG.mfa_to_string({MyModule, :many_args, 10}) + + assert result == "MyModule.many_args/10" + end + + test "handles single word module" do + result = SVG.mfa_to_string({String, :upcase, 1}) + + assert result == "String.upcase/1" + end + + test "handles erlang atoms as modules" do + result = SVG.mfa_to_string({:lists, :reverse, 1}) + + assert result == "lists.reverse/1" + end + end +end diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex new file mode 100644 index 0000000..f0f1ada --- /dev/null +++ b/test/support/fixtures.ex @@ -0,0 +1,72 @@ +defmodule FlameOn.Fixtures do + alias FlameOn.Capture.Block + + def simple_block(opts \\ []) do + %Block{ + id: Keyword.get(opts, :id, Ecto.UUID.generate()), + children: Keyword.get(opts, :children, []), + duration: Keyword.get(opts, :duration), + function: Keyword.get(opts, :function, {:example, :foo, 0}), + level: Keyword.get(opts, :level), + absolute_start: Keyword.get(opts, :absolute_start, 0), + max_child_level: Keyword.get(opts, :max_child_level) + } + end + + def simple_trace_events do + [ + {:call, {:example, :foo, 0}, 1000}, + {:return_to, {:example, :foo, 0}, 2000} + ] + end + + def nested_trace_events do + [ + {:call, {:example, :parent, 0}, 1000}, + {:call, {:example, :child, 0}, 1100}, + {:return_to, {:example, :parent, 0}, 1200}, + {:return_to, {:example, :parent, 0}, 1300} + ] + end + + def deeply_nested_trace_events do + [ + {:call, {:example, :level1, 0}, 1000}, + {:call, {:example, :level2, 0}, 1100}, + {:call, {:example, :level3, 0}, 1200}, + {:return_to, {:example, :level2, 0}, 1300}, + {:return_to, {:example, :level1, 0}, 1400}, + {:return_to, {:example, :level1, 0}, 1500} + ] + end + + def recursive_trace_events do + [ + {:call, {:example, :recursive, 1}, 1000}, + {:call, {:example, :recursive, 1}, 1100}, + {:call, {:example, :recursive, 1}, 1200}, + {:return_to, {:example, :recursive, 1}, 1300}, + {:return_to, {:example, :recursive, 1}, 1400}, + {:return_to, {:example, :recursive, 1}, 1500} + ] + end + + def sleep_trace_events do + [ + {:call, {:example, :foo, 0}, 1000}, + {:call, :sleep, 1100}, + {:return_to, :sleep, 1200}, + {:return_to, {:example, :foo, 0}, 1300} + ] + end + + def build_stack_from_trace_events(events) do + Enum.reduce(events, [], fn + {:call, function, timestamp}, stack -> + FlameOn.Capture.Server.Stack.handle_trace_call(stack, function, timestamp) + + {:return_to, function, timestamp}, stack -> + FlameOn.Capture.Server.Stack.handle_trace_return_to(stack, function, timestamp) + end) + end +end