Repository: parroty/exprof Branch: master Commit: d72e4247e39e Files: 15 Total size: 11.2 KB Directory structure: gitextract_a41okyq2/ ├── .github/ │ └── workflows/ │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib/ │ ├── exprof/ │ │ ├── analyzer.ex │ │ ├── macro.ex │ │ ├── reader.ex │ │ └── records.ex │ ├── exprof.ex │ └── sample/ │ └── sample_runner.ex ├── mix.exs ├── package.exs └── test/ ├── exprof_test.exs └── test_helper.exs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/tests.yml ================================================ name: tests on: push jobs: tests: name: Run Tests runs-on: ubuntu-latest strategy: matrix: otp: ['19.3'] elixir: ['1.5', '1.7'] env: MIX_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v2 - uses: actions/setup-elixir@v1 with: otp-version: ${{ matrix.otp }} elixir-version: ${{ matrix.elixir }} - uses: actions/cache@v1 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} restore-keys: | ${{ runner.os }}-mix- - run: mix deps.get - run: mix test --seed 0 # disable randomization ================================================ FILE: .gitignore ================================================ /ebin /deps erl_crash.dump *.ez tmp_exprof /_build /doc ================================================ FILE: CHANGELOG.md ================================================ 0.2.4 ------ #### Changes * Makes sure it is OK for the profiled code to send/receive messages (#13). * Fix for warnings with elixir v1.11. * Add :tools to extra_applications in mix.exs (#15). 0.2.3 ------ #### Changes * Fixes issue on windows where you get File.Error permission denied. - Changed to use randomized file name (#11). 0.2.2 ------ #### Changes * Return block result after completion (#9). * Link profiled process to avoid hang (#10). ================================================ FILE: LICENSE ================================================ Copyright (c) 2013-2015 parroty Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ExProf ============ [![Build Status](https://github.com/parroty/excoveralls/workflows/tests/badge.svg)](https://github.com/parroty/exprof/actions) [![hex.pm version](https://img.shields.io/hexpm/v/exprof.svg)](https://hex.pm/packages/exprof) [![hex.pm downloads](https://img.shields.io/hexpm/dt/exprof.svg)](https://hex.pm/packages/exprof) A simple code profiler for Elixir using eprof. It provides a simple macro as a wrapper for Erlang's :eprof profiler. ### Install Add `:exprof` to `deps` section of `mix.exs`. ```elixir def deps do [ {:exprof, "~> 0.2.0"} ] end ``` ### Usage import "ExProf.Macro", then use "profile" macro to start profiling. It prints out results, and returns them as list of records, along with the result of the profiled block. ```elixir defmodule SampleRunner do import ExProf.Macro @doc "analyze with profile macro" def do_analyze do profile do :timer.sleep 2000 IO.puts "message\n" end end @doc "get analysis records and sum them up" def run do {records, _block_result} = do_analyze total_percent = Enum.reduce(records, 0.0, &(&1.percent + &2)) IO.inspect "total = #{total_percent}" end end ``` ### Run An example to use in iex console. ```elixir $ iex -S mix .. iex(1)> SampleRunner.run message FUNCTION CALLS % TIME [uS / CALLS] -------- ----- --- ---- [----------] 'Elixir.IO':puts/2 1 0.86 1 [ 1.00] io:o_request/3 1 1.72 2 [ 2.00] io:put_chars/2 1 1.72 2 [ 2.00] erlang:group_leader/0 1 1.72 2 [ 2.00] io:request/2 1 2.59 3 [ 3.00] io:execute_request/2 1 2.59 3 [ 3.00] 'Elixir.SampleRunner':'-run/0-fun-0-'/0 1 2.59 3 [ 3.00] 'Elixir.IO':map_dev/1 1 3.45 4 [ 4.00] erlang:demonitor/2 1 4.31 5 [ 5.00] io:io_request/2 1 6.03 7 [ 7.00] io:wait_io_mon_reply/2 1 6.90 8 [ 8.00] 'Elixir.IO':puts/1 1 8.62 10 [ 10.00] unicode:characters_to_binary/2 1 11.21 13 [ 13.00] timer:sleep/1 1 14.66 17 [ 17.00] erlang:monitor/2 1 31.03 36 [ 36.00] "total = 100.0" ``` ### Add a Mix Task An example to use as mix tasks. ```elixir defmodule Mix.Tasks.Exprof do @shortdoc "Profile using ExProf" use Mix.Task import ExProf.Macro def run(_mix_args) do profile do: do_work(2) end defp do_work(n) do :timer.sleep(n * 1000) end end ``` ================================================ FILE: lib/exprof/analyzer.ex ================================================ defmodule ExProf.Analyzer do @moduledoc """ Proivdes helper methods for operating Prof records. """ import ExPrintf @doc """ Returns records only have major percent values, and filter out rest of them. The default is to pick "80%" and it can be changed by specifying rate argument. """ def get_top_percent_items(records, rate \\ 80.0) do sorted_records = Enum.sort(records, &(&1.percent > &2.percent)) do_get_top_percent_items(sorted_records, rate, []) end defp do_get_top_percent_items(records, rate, acc) when length(records) == 0 or rate <= 0.0 do Enum.reverse(acc) end defp do_get_top_percent_items([head|tail], rate, acc) do do_get_top_percent_items(tail, rate - head.percent, [head|acc]) end @doc """ Print records to screen """ def print(records) do print_header() Enum.each(records, &(do_print(&1))) end defp print_header do IO.puts "FUNCTION CALLS % TIME [uS / CALLS]" IO.puts "-------- ----- --- ---- [----------]" end defp do_print(record) do printf("%-50s %-6d %6.2f %5d [%10.2f]\n", [String.slice(record.function, 0, 50), record.calls, record.percent, record.time, record.us_per_call]) end end ================================================ FILE: lib/exprof/macro.ex ================================================ defmodule ExProf.Macro do @moduledoc """ Provides a macro to profile a block of code. """ @doc """ A macro to specify the code block to profile. It spawns a new process to execute the code block for isolating the profile result. (ex.) profile do :timer.sleep 2000 end """ defmacro profile(do: code) do quote do ref = make_ref() pid = spawn_link(ExProf.Macro, :execute_profile, [fn -> unquote(code) end, ref]) ExProf.start(pid) send pid, {ref, self()} result = receive do {^ref, result} -> result end ExProf.stop records = ExProf.analyze {records, result} end end @doc """ An internal method for initiating profiling. """ def execute_profile(func, ref) do receive do {^ref, sender} -> send sender, {ref, func.()} forward_other_messages(sender) end end defp forward_other_messages(sender) do receive do message -> send sender, message forward_other_messages(sender) end end end ================================================ FILE: lib/exprof/reader.ex ================================================ defmodule ExProf.Reader do @moduledoc """ A reader for eprof outputs. """ @doc """ Read the eprof output file and returns the parsed result of Prof records. """ def read(file_name) do File.read!(file_name) |> String.split("\n") |> parse([]) end defp parse([], acc), do: Enum.reverse(acc) defp parse([head|tail], acc) do case parse_record(head) do nil -> parse(tail, acc) record -> parse(tail, [record|acc]) end end defp parse_record(head) do case Regex.run(~r/(.+?\/[0-9])\s+(.+?)\s+(.+?)\s+(.+?)\s+\[\s+(.+?)\]/, head) do [_all, function, calls, percent, time, us_per_call] -> %Prof{function: function, calls: String.to_integer(calls), percent: String.to_float(percent), time: String.to_integer(time), us_per_call: String.to_float(us_per_call)} nil -> nil end end end ================================================ FILE: lib/exprof/records.ex ================================================ defmodule Prof do defstruct function: nil, calls: nil, percent: nil, time: nil, us_per_call: nil end ================================================ FILE: lib/exprof.ex ================================================ defmodule ExProf do @moduledoc """ Wrapper for eprof library. It needs to be called in start -> stop -> analyze order. """ # temporary file for storing eprof output @tmp_prof_name 'tmp_exprof' @doc """ Start the profiling for the specified pid. """ def start(pid \\ self()) do :eprof.start :eprof.start_profiling([pid]) end @doc """ Stop the profiling previously started with start method call. """ def stop do :eprof.stop_profiling end @doc """ Analyze and output the profiling as the list of Prof records. It also outputs to the STDOUT. """ def analyze do random_number = :rand.uniform(100) file_name = to_string(@tmp_prof_name ++ [to_string(random_number)]) :eprof.log(file_name) :eprof.analyze(:total, [{:sort, :time}]) records = ExProf.Reader.read(file_name) File.rm!(file_name) records end end ================================================ FILE: lib/sample/sample_runner.ex ================================================ defmodule SampleRunner do import ExProf.Macro @doc "analyze with profile macro" def do_analyze do profile do :timer.sleep 2000 IO.puts "message\n" end end @doc "get analysis records and sum them up" def run do {records, result} = do_analyze() total_percent = Enum.reduce(records, 0.0, &(&1.percent + &2)) IO.inspect "total = #{total_percent}" result end end ================================================ FILE: mix.exs ================================================ defmodule ExProf.Mixfile do use Mix.Project def project do [ app: :exprof, version: "0.2.4", elixir: "~> 1.0", deps: deps(), description: description(), package: package() ] end # Configuration for the OTP application def application do [ extra_applications: [:tools] ] end # Returns the list of dependencies in the format: # { :foobar, "~> 0.1", git: "https://github.com/elixir-lang/foobar.git" } defp deps do [ {:exprintf, "~> 0.2"}, {:ex_doc, ">= 0.0.0", only: :dev} ] end defp description do """ A simple code profiler for Elixir using eprof. """ end defp package do [ maintainers: ["parroty"], licenses: ["MIT"], links: %{"GitHub" => "https://github.com/parroty/exprof"} ] end end ================================================ FILE: package.exs ================================================ Expm.Package.new(name: "exprof", description: "A simple code profiler for Elixir using eprof", version: "0.0.1", keywords: ["Elixir","eprof","profiler"], maintainers: [[name: "parroty", email: "parroty00@gmail.com"]], repositories: [[github: "parroty/exprof"]]) ================================================ FILE: test/exprof_test.exs ================================================ defmodule ExprofTest do use ExUnit.Case, async: false import ExUnit.CaptureIO test "sample runner" do assert capture_io(fn -> assert :ok = SampleRunner.run end) =~ ~r/FUNCTION\s+CALLS/m end @tag timeout: 1000 test "abort on exit" do Process.flag(:trap_exit, true) pid = spawn_link(fn -> import ExProf.Macro profile do Process.exit(self(), :kill) end end) assert_receive {:EXIT, ^pid, :killed}, 500 end end ================================================ FILE: test/test_helper.exs ================================================ ExUnit.start