Commit 7c9e09f3 authored by Tiago Barroso's avatar Tiago Barroso
Browse files

first public commit

parents
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
incendium-*.tar
# Temporary files, for example, from tests.
/tmp/
# Incendium
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `incendium` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:incendium, "~> 0.1.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/incendium](https://hexdocs.pm/incendium).
This diff is collapsed.
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
# Example flamegraph
This is an example flamegraph generated by Incendium.
Flamegraphs are highly dynamic thanks to the `d3` library.
Ýou can click on a horizontal rectangle (which represents a function call)
to zoom in on the rectangle and its descendants.
You can use the search form on the top right to search for a specific function name.
The support for "standalone" flamegraphs which you can embed in other documents
isn't good yet.
We plan on making it easy to download a flamegraph which can be embedded in a larger
HTML or markdown file.
The HTML generated by Incendium was heavily customized in order to embed this flamegraph
inside an ExDoc page.
<link rel="stylesheet" href="assets/incendium.css" />
<script src="assets/incendium.js" charset="utf-8"></script>
<div class="container">
<div class="header clearfix">
<nav>
<div class="pull-right">
<form class="form-inline" id="form">
<a class="btn" href="javascript: resetZoom();">Reset zoom</a>
<a class="btn" href="javascript: clear();">Clear</a>
<div class="form-group">
<input type="text" class="form-control" id="term">
</div>
<a class="btn btn-primary" href="javascript: search();">Search</a>
</form>
</div>
</nav>
<h3 class="text-muted">Incendium <small>(built on d3-flame-graph)</small></h3>
</div>
<div id="chart"></div>
<hr/>
<div id="details"></div></div>
<script type="text/javascript" src="assets/flamegraph1.js"></script>
defmodule Incendium do
@moduledoc """
Incendium is a way of ...
"""
alias Incendium.Storage
@doc """
Adds routes for an Incendium controller.
"""
defmacro routes(controller) do
require Phoenix.Router
quote do
Phoenix.Router.get "/", unquote(controller), :latest_flamegraph
end
end
@doc """
Profiles a function call using tracing (as opposed to sampling).
This profiler uses [`:eflame`](https://github.com/proger/eflame) under the hood.
Profiled code will be very slow. Never use this in production.
Stack data will be written to #{Storage.latest_stacks_path()}.
"""
def profile_with_tracing(fun) do
output_file = Storage.latest_stacks_path() |> to_charlist()
:eflame.apply(:normal_with_children, output_file, fun, [])
end
end
defmodule Incendium.Assets do
@moduledoc false
@d3_js_url "https://d3js.org/d3.v4.min.js"
@d3_tip_js_url "https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.9.1/d3-tip.min.js"
@d3_flamegraph_js_url "https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.min.js"
@bootstrap_css_url "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
@d3_flamegraph_css_url "https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.css"
@extra_css File.read!("lib/incendium/assets/extra.css")
def css_path(), do: Path.join([:code.priv_dir(:incendium), "assets", "incendium.css"])
def js_path(), do: Path.join([:code.priv_dir(:incendium), "assets", "incendium.js"])
def build_js() do
{:ok, {_, _, d3_js}} = :httpc.request(@d3_js_url)
{:ok, {_, _, d3_tip_js}} = :httpc.request(@d3_tip_js_url)
{:ok, {_, _, d3_flamegraph_js}} = :httpc.request(@d3_flamegraph_js_url)
all_js = [
d3_js,
d3_tip_js,
d3_flamegraph_js
]
File.write(js_path(), all_js)
end
def build_css() do
{:ok, {_, _, bootstrap_css}} = :httpc.request(@bootstrap_css_url)
{:ok, {_, _, d3_flamegraph_css}} = :httpc.request(@d3_flamegraph_css_url)
all_css = [
bootstrap_css,
d3_flamegraph_css,
@extra_css
]
File.write!(css_path(), all_css)
end
def build_assets() do
build_css()
build_js()
end
end
/* Space out content a bit */
body {
padding-top: 20px;
padding-bottom: 20px;
}
/* Custom page header */
.header {
padding-bottom: 20px;
padding-right: 15px;
padding-left: 15px;
border-bottom: 1px solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
margin-top: 0;
margin-bottom: 0;
line-height: 40px;
}
/* Customize container */
.container {
max-width: 990px;
}
\ No newline at end of file
defmodule Incendium.Controller do
alias Incendium.{
Flamegraph,
Assets,
Storage
}
@doc """
Turn the current module into an Icendium controller.
Options:
* `routes_module` (required) - The module that contains the routes
in your application (it should be something like `YourApp.Router.Helpers`)
* `:otp_app` (required) - The main OTP application
## Examples
defmodule MyApp.IncendiumController do
use Incendium.Controller,
routes_module: MyApp.Router.Helpers,
otp_app: :my_app
end
"""
defmacro __using__(opts) do
routes_module = Keyword.fetch!(opts, :routes_module)
otp_app = Keyword.fetch!(opts, :otp_app)
app_priv = :code.priv_dir(otp_app)
static_js_dest = Path.join([app_priv, "static", "js", "incendium.js"])
static_css_dest = Path.join([app_priv, "static", "css", "incendium.css"])
File.cp!(Assets.css_path(), static_css_dest)
File.cp!(Assets.js_path(), static_js_dest)
quote do
use Phoenix.Controller
def latest_flamegraph(conn, params) do
Incendium.Controller.latest_flamegraph(conn, params, unquote(routes_module))
end
end
end
@doc false
def latest_flamegraph(conn, _params, routes_module) do
latest_hierarchy =
Storage.latest_stacks_path()
|> Flamegraph.file_to_hierarchy()
|> Jason.encode!()
body =
Incendium.View.render("latest-flamegraph.html",
conn: conn,
routes_module: routes_module,
latest_hierarchy: latest_hierarchy
)
|> Phoenix.HTML.Safe.to_iodata()
Plug.Conn.send_resp(conn, 200, body)
end
end
defmodule Incendium.Decorator do
@moduledoc """
Defines decorators to profile function calls.
"""
use Decorator.Define, [
incendium_profile_with_tracing: 0
]
alias Incendium.Storage
@doc """
*Decorator*. When the decorated function is invoked,
it will bd profiled using tracing.
Stack data will be written to #{Storage.latest_stacks_path()}.
**Note**: profiling will be disabled if `Mix.env() == :prod`.
This means it's safe(ish) to leave these decorators in a production
environment.
It's never a good idea to profile in prod using the `:eflame`
library, as it makes your code run ~10x slower.
## Example
defmodule MyApp.UserController do
# ...
use Incendium.Decorator
@decorate incendium_profile_with_tracing()
def index(conn, params) do
# ...
end
end
"""
def incendium_profile_with_tracing(body, _context) do
# If we're not in prod, it's safe to profile
if Mix.env() in [:dev, :test] do
Storage.latest_stacks_path()
|> Path.dirname()
|> File.mkdir_p!()
quote do
Incendium.profile_with_tracing(fn -> unquote(body) end)
end
# It's never a good idea to profile in prod
# (your functions will run ~10x slower)
else
body
end
end
end
defmodule Incendium.Flamegraph do
@moduledoc false
def file_to_hierarchy(path) do
path
|> file_to_stacks()
|> stacks_to_hierarchy()
|> maybe_wrap_in_root()
end
def file_to_stacks(path) do
File.read!(path)
|> String.split("\n")
|> Enum.map(fn line -> String.split(line, ";") end)
end
def stacks_to_hierarchy(stacks) do
level =
Enum.reduce(stacks, %{}, fn
[], acc ->
acc
[frame | stack], acc ->
case acc do
%{^frame => stacks_for_frame} ->
%{acc | frame => [stack | stacks_for_frame]}
_acc ->
Map.put(acc, frame, [stack])
end
end)
handle_level(level)
end
defp handle_level(level) do
for {frame, stacks} <- level do
levels = stacks_to_hierarchy(stacks)
%{name: frame, value: length(stacks), children: levels}
end
end
defp maybe_wrap_in_root(hierarchy) when is_map(hierarchy), do: hierarchy
defp maybe_wrap_in_root(hierarchy) when is_list(hierarchy) do
value =
hierarchy
|> Enum.map(fn node -> node.value end)
|> Enum.sum()
%{name: "root", value: value, children: hierarchy}
end
end
defmodule Incendium.Storage do
@moduledoc false
# TODO: maybe expand this into a real solution which stores
# profile data in a persistent backend such as ETS tables.
def latest_stacks_path() do
Path.join("incendium", "stacks.out")
end
end
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="<%= @routes_module.static_path(@conn, "/css/incendium.css") %>"/>
<script src="<%= @routes_module.static_path(@conn, "/js/incendium.js") %>" charset="utf-8"></script>
<title>Incendium Flamegraph</title>
</head>
<body>
<div class="container">
<div class="header clearfix">
<nav>
<div class="pull-right">
<form class="form-inline" id="form">
<a class="btn" href="javascript: resetZoom();">Reset zoom</a>
<a class="btn" href="javascript: clear();">Clear</a>
<div class="form-group">
<input type="text" class="form-control" id="term">
</div>
<a class="btn btn-primary" href="javascript: search();">Search</a>
</form>
</div>
</nav>
<h3 class="text-muted">Incendium <small>(built on d3-flame-graph)</small></h3>
</div>
<div id="chart">
</div>
<hr>
<div id="details">
</div>
</div>
<script type="text/javascript">
var data = <%= {:safe, @latest_hierarchy} %>;
var flameGraph = d3.flamegraph()
.width(960)
.cellHeight(18)
.transitionDuration(750)
.minFrameSize(5)
.transitionEase(d3.easeCubic)
.sort(true)
.title("")
.onClick(onClick)
.differential(false)
.selfValue(false);
var details = document.getElementById("details");
flameGraph.setDetailsElement(details);
// Example of how to set fixed chart height
// flameGraph.height(540);
d3.select("#chart")
.datum(data)
.call(flameGraph);
document.getElementById("form").addEventListener("submit", function (event) {
event.preventDefault();
search();
});
function search() {
var term = document.getElementById("term").value;
flameGraph.search(term);
}
function clear() {
document.getElementById('term').value = '';
flameGraph.clear();
}
function resetZoom() {
flameGraph.resetZoom();
}
function onClick(d) {
console.info("Clicked on " + d.data.name);
}
</script>
</body>
</html>
defmodule Incendium.View do
@moduledoc false
# Helpers to render pages in the IncendiumController
@external_resource "#{__DIR__}/templates/latest-flamegraph.html.eex"
use Phoenix.View,
root: "#{__DIR__}/templates",
path: ""
end
defmodule Mix.Incendium.Assets do
use Mix.Task
@impl Mix.Task
def run(args) do
_dir =
case args do
[dir] -> dir
_ -> raise "Task requires a directory name as a single argument"
end
end
end
defmodule Incendium.MixProject do
use Mix.Project
def project do
[
app: :incendium,
version: "0.1.0",
elixir: "~> 1.12",
start_permanent: Mix.env() == :prod,
deps: deps(),
docs: [
extras: ["doc_extra/pages/Example flamegraph.md"],
assets: "doc_extra/assets/"
]
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger, :inets]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:phoenix_html, "~> 2.0"},
{:phoenix, "~> 1.4"},
{:decorator, "~> 1.4"},
{:jason, "~> 1.2"},
{:eflame, "~> 1.0"},
{:ex_doc, "~> 0.23", only: :dev}
]
end
end
%{
"decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"},
"earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"},
"eflame": {:hex, :eflame, "1.0.1", "0664d287e39eef3c413749254b3af5f4f8b00be71c1af67d325331c4890be0fc", [:mix], [], "hexpm", "e0b08854a66f9013129de0b008488f3411ae9b69b902187837f994d7a99cf04e"},
"ex_doc": {:hex, :ex_doc, "0.24.2", "e4c26603830c1a2286dae45f4412a4d1980e1e89dc779fcd0181ed1d5a05c8d9", [:mix], [{:earmark_parser, "~> 1.4.0", [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", "e134e1d9e821b8d9e4244687fb2ace58d479b67b282de5158333b0d57c6fb7da"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"phoenix": {:hex, :phoenix, "1.5.9", "a6368d36cfd59d917b37c44386e01315bc89f7609a10a45a22f47c007edf2597", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7e4bce20a67c012f1fbb0af90e5da49fa7bf0d34e3a067795703b74aef75427d"},
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"},
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
}
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment