Skip to content

Commit

Permalink
V1.2.0 (#2)
Browse files Browse the repository at this point in the history
* moving to a dependancy

* moving to a dependancy

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Moved ezcodeprofiler.ez to be a dependency that includes a management module

* Add dep clean

* Add dep clean

* wip

* wip

* wip

* wip

* wip

* wip

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* wip

* wip

* wip

* wip

* wip

* Update README.md

* Update README.md

* Update README.md

* wip

* Update README.md

* wip

* Update README.md

* Update README.md
  • Loading branch information
nhpip authored Jul 9, 2022
1 parent 2fdb7f2 commit 9c13f28
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 160 deletions.
71 changes: 60 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@

Provides a simple to use profiling mechanism to inspect the behavior of an application on a target VM. Under the hood it utilizes Erlang's profiler tools, namely `eprof`, the default, `fprof` or `cprof`. This runs as a stand-alone `escript` for both for ease of use and to minimize impact on the target VM.
Provides a simple to use profiling mechanism to inspect the behavior of an application on a target VM. It utilizes Erlang's profiler tools, namely `eprof`, the default, `fprof` or `cprof`. Supports profiling of processes or the ability to *decorate* your code with, zero cost, analysis functions. For ease of use and to minimize impact on the application to be profiled `ezprofiler` runs as a stand-alone `escript` rather than an application in the same VM. That said, for code-profiling there is an option to manage `ezprofiler` via source code within your application for deployments where access to the VM may be limited (see below).

## Overview
The `ezprofiler` utilty presents the user with two types of profiling, both controlled via a simple shell-type interface.
There is a separate dependency, `ezprofiler_deps`, that needs to be included in the `deps` function of your `mix.exs` for code profiling to work correctly:

```
defp deps do
[
{:ezprofiler_deps, git: "https://github.com/nhpip/ezprofiler_deps.git"}
]
end
```
It is also possible to include `ezprofiler` as a dependency too:
```
defp deps do
[
{:ezprofiler, git: "https://github.com/nhpip/ezprofiler.git", app: false},
{:ezprofiler_deps, git: "https://github.com/nhpip/ezprofiler_deps.git"}
]
```

## Profiling
`ezprofiler` presents the user with two types of profiling, processes based and code based profiling, both controlled via a simple shell-type interface or, optionally, via code in the case of code profiling.

### Process Profiling
Attach the profiler to specific processes, registered names or pg/pg2 groups. When profiling starts those processes will be traced. The selection of pg vs pg2 is based on the OTP release, see `@pg_otp_version` in `lib/ezprofiler/term_helper.ex` if you wish to change that behavior.

### Code Profiling
The process option can be omitted. Instead the code can be decorated with profiling functions. In this case, to simplify the analysis, only a single (the first) process to invoke that code block will be profiled. This is useful in, for example, web-based applications where 100's of processes maybe spawned and invoke the same code at the same time. The profiling functions incur zero run-time cost, and are safe to use in prodution, until the profiler is started.
The process option can be omitted. Instead the code can be decorated with profiling functions. In this case, to simplify the analysis, only a single (the first) process to invoke that code block will be profiled. This is useful in, for example, web-based applications where 1000's of processes maybe spawned and invoke the same code at the same time. The profiling functions incur near zero run-time cost (zero-cost when profiling is disabled), and are safe to use in prodution. Once profiling is started an insignificant run-time cost maybe incurred (the cost of a call to a `GenServer`).

## Process Profiling
This is when you know the process that you want to profile, the process is specified as a pid, registered name or PG2 group. The process or processes are specified with the command line option `--processes`. This coupled with the `--sos` (set on spawn) option can profile a process and carry on the profiling on all processes that are spawned by the target process. Included is the option to specify `:ranch` in the `--processes` option. This makes tracing of the popular `Ranch` socket acceptor pool, and specified with the `--sos` and `--sol` options will follow the spawned processes that are created on an inbound TCP accept requests.
This is when you know the process that you want to profile, the process is specified as a pid, registered name or PG2 group. The process or processes are specified with the command line option `--processes`. This coupled with the `--sos` (set on spawn) option can profile a process and carry on the profiling on all processes that are spawned by the target process. Included is the option to specify `:ranch` in the `--processes` option. This makes tracing of the popular `Ranch` socket acceptor pool, and when specified with the `--sos` and `--sol` options will follow the spawned processes that are created on an inbound TCP accept requests.

The list of processes can be a simple process id, a registered name, pg/pg2 group or a list. THey must be enclosed in double quotes, for example:

Expand All @@ -31,7 +49,7 @@ Options:
'a' to get profiling results when 'profiling'
'r' to abandon (reset) profiling and go back to 'waiting' state with initial value for 'u' set
'c' to enable code profiling (once)
'c' "label"to enable code profiling (once) for label (an atom), e.g. "c special_atom"
'c' "label"to enable code profiling (once) for label (an atom or string), e.g. "c special_atom"
'u' "M:F" to update the module and function to trace (only with eprof)
'v' to view last saved results file
'g' for debugging, returns the state on the target VM
Expand Down Expand Up @@ -111,7 +129,7 @@ If specified with the `--directory` option the results can be saved and the last
**NOTE:** If `fprof` is selected as the profiler the results will not be output to screen unless `'v'` is selected.

## Code Profiling
This permits the profiling of code dynamically. The user can decorate functions or blocks of code, and when ready the user can, from the escript, start profiling that function or block of code. The decorating is quite simple, the file `priv/ezcodeprofiler.ex` should be included in your application. This module (`EZProfiler.CodeProfiler`) contains stub functions that you can place throughout your code that have zero run-time cost. When the profiler connects to your application this code is hot-swapped out for a module with the same name, containing the same fucntion names. These functions contain actual working code. The run-time cost is still minimal as only a single process will be monitored at a time, the only cost to other processes is a single `gen:call` to an Elixir `Agent` if a profiling function is called. Once `ezprofiler` terminates the original "stub" module is restored once again ensuring zero run-time cost.
This permits the profiling of code dynamically. The user can decorate functions or blocks of code, and when ready the user can, from the escript, start profiling that function or block of code. The decorating is quite simple, the dependency `ezprofiler_deps` should be added to your application `mix.exs` file (see below). This contains the module (`EZProfiler.CodeProfiler`) that has stub functions that you can place throughout your code that have zero run-time cost. When the profiler connects to your application this code is hot-swapped out for a module with the same name, containing the same function names. These functions contain actual working code. The run-time cost is still minimal as only a single process will be monitored at a time, the only cost to other processes is a single `gen:call` to an Elixir `Agent` if a profiling function is called. Once `ezprofiler` terminates the original "stub" module is restored, once again ensuring zero run-time cost.

The application will attempt to only show functions, and functions that those functions call, that are part of your workflow. There will however be a couple of internal `ezprofiler` functions included too. These functions are at the end of the profiling run and will not impact your application. If you only wish to see the function been profiled, without `ezprofiler` and other called functions set the `--cpfo` option. Alternatively select the `--mf` option, especially if `cprof` is used as the profiler.

Expand Down Expand Up @@ -156,6 +174,7 @@ def foo(data) do
end
```
### Code profiling via the ezprofiler shell
Invoke `ezprofiler` as below (no need for a process) hitting `c` will start profiling in this case. To abandon hit `r`.

Code profiling still supports the `--mf` option (or `u` on the menu) to filter the results.
Expand All @@ -168,7 +187,7 @@ Options:
'a' to get profiling results when 'profiling'
'r' to abandon (reset) profiling and go back to 'waiting' state with initial value for 'u' set
'c' to enable code profiling (once)
'c' "label"to enable code profiling (once) for label (an atom), e.g. "c mylabel"
'c' "label"to enable code profiling (once) for label (an atom or string), e.g. "c mylabel"
'u' "M:F" to update the module and function to trace (only with eprof)
'v' to view last saved results file
'g' for debugging, returns the state on the target VM
Expand Down Expand Up @@ -210,7 +229,7 @@ Code profiling enabled with a label of :my_label
waiting..(5)>
Got a start profiling from source code with label of :my_label
```
Alternatively the label can be replaced with a lambda that should return a label (atom) if tracing is to be started, or the atom `:nok` if it isn't:
Alternatively the label can be replaced with a lambda that should return a label (atom or string) if tracing is to be started, or the atom `:nok` if it isn't:
```
EZProfiler.CodeProfiler.start_code_profiling(fn -> if should_i_profile?(foo), do: :my_label, else: :nok end)
Expand All @@ -220,8 +239,38 @@ case do_send_email(email, private_key) do
```
See below for additional examples.

## Compiling
Execute `mix escript.build`
### Code profiling via source code
In certain deployments access to a shell, either the Elixir/Erlang shell or a bash shell may be restricted. Instead there is limited functionality to code-profile via your application source code. Please be aware that this still requires the `ezprofiler` escript, which is still started in the background. This may change in future releases, but adding a profiler as a separate application does add risk to any other applications on the VM.

Please see the `EZProfiler.Manager` module documentation for more information. There are 6 functions available for code profiling:
```
start_ezprofiler/0 # Starts the profiler with default configuration
start_ezprofiler/1 # Starts the profiler with custom configuration
stop_ezprofler/0 # Stops the profiler
enable_profiling/0 # Start profiling, same as `c` from the CLI
enable_profiling/1 # Start profiling with a label, same as `c label` from the CLI
wait_for_results/0 # Blocks, and waits for results (up to 60 seconds)
wait_for_results/1 # Blocks, and waits for results for the time, in seconds
wait_for_results_non_block/2 # As `wait_for_results` but is non-blocking. Instead a message is sent to `self()` or the specified pid
get_profiling_results/1 # Retrieves the results
```

## Compiling and Mix
Execute `mix compile` or include `ezprofiler` in `deps` function of application `mix.exs` file along with `ezprofiler_deps`.

```
defp deps do
[
{:ezprofiler, git: "https://github.com/nhpip/ezprofiler.git", app: false},
{:ezprofiler_deps, git: "https://github.com/nhpip/ezprofiler_deps.git"}
]
end
```

## Usage
```
Expand Down
56 changes: 49 additions & 7 deletions lib/ezprofiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,21 @@ defmodule EZProfiler do
sos: :boolean,
sort: :string,
cpfo: :boolean,
inline: :string,
help: :boolean
]
)
|> setup_distributed_erlang() |> start_profiling()
rescue
e in ArgumentError -> error(e.message)
e in FunctionClauseError -> IO.puts("error: " <> e.message)
e in FunctionClauseError -> IO.puts("error: #{inspect(e)}")
e -> IO.puts("error #{inspect(e)}")
end
end

@doc false
def error(error) do
IO.puts("")
IO.puts(~s(#{inspect error}\n))
help()
end
Expand Down Expand Up @@ -79,7 +81,9 @@ defmodule EZProfiler do
cookie -> :erlang.set_cookie(node(), String.to_atom(cookie))
end

{target_node, args}
if :net_adm.ping(target_node) == :pong,
do: {target_node, args},
else: error({target_node, :unavailable})
end

##
Expand Down Expand Up @@ -156,6 +160,8 @@ defmodule EZProfiler do
## Since this "main" process is blocked waiting on user input spawn a process that proxies inbound messages from the target
wait_for_profiler_events(profiler_pid, monitor_pid)

if (filename = Keyword.get(opts, :inline, nil)), do: File.touch(filename)

## Get user input/commands
wait_for_user_events(state)
end
Expand Down Expand Up @@ -257,8 +263,12 @@ defmodule EZProfiler do
wait_for_user_events(%{state | command_count: count+1})

<<"c", label::binary>> ->
label = String.trim(label) |> String.trim(":") |> String.to_atom()
ProfilerOnTarget.allow_code_profiling(target_node, label)
with {:ok, label} <- get_label(label)
do
ProfilerOnTarget.allow_code_profiling(target_node, label)
else
_ -> IO.puts("Bad label")
end
wait_for_user_events(%{state | command_count: count+1})

"v" ->
Expand Down Expand Up @@ -289,6 +299,10 @@ defmodule EZProfiler do
display_message(:new_line1)
wait_for_user_events(state)

{:get_results_file, pid} ->
get_results_file(pid, state)
wait_for_user_events(state)

{:state_change, pid, :waiting} ->
display_message(:new_line1)
send(pid, :state_change_ack)
Expand Down Expand Up @@ -337,6 +351,22 @@ defmodule EZProfiler do
end
end

defp get_label(label) do
label = String.trim(label) |> String.replace("\"", "")
if String.at(label, 0) == ":",
do: do_get_label(label),
else: do_get_label("\"#{label}\"")
end

defp do_get_label(label) do
try do
{new_label, _} = Code.eval_string(label)
{:ok, new_label}
rescue
_ -> :error
end
end

defp make_prompt(%{prompt: :no_prompt} = _state), do: ""

defp make_prompt(%{prompt: prompt, command_count: count} = _state), do: prompt <> "(#{count})> "
Expand All @@ -358,15 +388,28 @@ defmodule EZProfiler do

defp view_results_file(%{results_file: filename} = _state) when is_binary(filename) do
try do
File.stream!(filename) |> Enum.each(fn line -> String.trim(line,"\n") |> IO.puts() end)
File.stream!(filename) |> Enum.each(&(String.trim(&1, "\n")) |> IO.puts())
IO.puts("")
rescue
_ ->
display_message(:no_file)
end
end

defp view_results_file(_state), do: display_message(:no_file)
defp view_results_file(_state), do:
display_message(:no_file)

defp get_results_file(pid, %{results_file: filename} = _state) when is_binary(filename) do
try do
send(pid, {:profiling_results, filename, File.read!(filename)})
rescue
_ ->
send(pid, {:no_profiling_results, :processing_exception})
end
end

defp get_results_file(pid, _state), do:
send(pid, {:profiling_results, :no_results_file})

##
## Gets the module's binary code and load it on the target.
Expand All @@ -378,7 +421,6 @@ defmodule EZProfiler do
end

defp display_message(message_details) do

case message_details do
{:new_line, _} ->
IO.puts("")
Expand Down
2 changes: 2 additions & 0 deletions lib/ezprofiler/code_monitor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ defmodule EZProfiler.CodeMonitor do
def do_init_code_monitor(profiler_node, profiler_mod, profiler_bin) do
Node.monitor(profiler_node, true)

:persistent_term.erase({EZProfiler.CodeProfiler, :stub_loaded})

## Gets the current EZCodeProfiler module data and save it
{correct_mod, correct_bin, correct_file} = get_code_profiler_module_bin() ## Grabs the stub one shipped with collection server

Expand Down
Loading

0 comments on commit 9c13f28

Please sign in to comment.