Commit fb828f52 authored by Tiago Barroso's avatar Tiago Barroso
Browse files

make it easier to embed flamegraphs in other webpages

parent 7c9e09f3
{
"cSpell.words": [
"clearfix"
]
}
\ No newline at end of file
# Incendium
**TODO: Add description**
Easy profiling for your Phoenix controller actions (and other functions) using [flamegraphs](http://www.brendangregg.com/flamegraphs.html).
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
The package can be installed
by adding `incendium` to your list of dependencies in `mix.exs`:
```elixir
......@@ -15,7 +16,99 @@ def deps do
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).
Documentation can be found at [https://hexdocs.pm/incendium](https://hexdocs.pm/incendium).
<!-- ex_doc -->
## Rationale
Profiling Elixir code is easy using the default Erlang tools, such as `fprof`.
These tools produce a lot of potentially useful data, but visualizing and interpreting all that data is not easy.
The erlang tool [`eflame`](https://github.com/proger/eflame) contains some utilities to generate flamegraphs from the results of profiling your code
But... `eflame` expects you to manually run a bash script, which according to my reading seems to call a perl (!) script that generates an interactive SVG.
And although the generated SVGs support some minimal interaction, it's possible to do better.
When developping a web application, one can take advantage of the web browser as a highly dynamic tool for visualization using SVG, HTML and Javascript.
Fortunately there is a very good javascrtip library to generate flamegraphs: [d3-flame-graph](https://github.com/spiermar/d3-flame-graph).
By reading `:eflame` stacktrace samples and converting them into a format that `d3-flame-graph` can understand, we can render the flamegraph in a webpage.
That way, instead of the manual steps above you can just visit an URL in your web application.
## Usage
To use `incendium` in your web application, you need to follow these steps:
### 1. Add Incendium as a dependency
```elixir
def deps do
[
{:incendium, "~> 0.1.0"}
]
end
```
You can make it a `:dev` only dependency if you wish, but Incendium will only decorate your functions if you're in `:dev` mode.
Incendium decorators *won't* decorate your functions in `:prod` (profilers such as `eflame` should never be used in `:prod` because they add a very significant overhead; your code will be ~10-12 times slower)
### 2. Create an Incendium Controller for your application
```elixir
# lib/my_app_web/controllers/incencdium_controller.ex
defmodule MyApp.IncendiumController do
use Incendium.Controller,
routes_module: MyAppWeb.Router.Helpers,
otp_app: :my_app
end
```
There is no need to define an accompanying view.
Currently the controller is not extensible (and there aren't many natural extension points anyway).
Upon compilation, the controller will automcatically add the files `incendium.js` and `incendium.css` to your `priv/static` directory so that those static files will be served using the normal Phoenix mechanisms.
On unusual Phoenix apps which have static files in other places, this might not work as expected.
Currently there isn't a way to override the place where the static files should be added.
### 3. Add the controller to your Router
```elixir
# lib/my_app_web/controllers/router.ex
require Incendium
scope "/incendium", MyAppWeb do
Incendium.routes(IncendiumController)
end
```
### 4. Decorate the functions you want to profile
Incendium decorators depend on the [decorator](https://hex.pm/packages/decorator) package.
```elixir
defmodule MyAppWeb.MyContext.ExampleController do
use MyAppWeb.Mandarin, :controller
# Activate the incendium decorators
use Incendium.Decorator
# Each invocation of the `index/2` function will be traced and profiled.
@decorate incendium_profile_with_tracing()
def index(conn, params) do
resources = MyContext.list_resources(params)
render(conn, "index.html", resources: resources)
end
end
```
Currently incendium only supports tracing profilers
(which are very slow and not practical in production).
In the future we may support better options such as sampling profilers.
### 5. Visit the `/incendium` route to see the generated flamegraph
Each time you run a profiled function, a new stacktrace will be generated.
Stacktraces are not currently saves, you can only access the latest one.
In the future we might add a persistence layer that stores a number of stacktraces instead of keeping just the last one.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -7,34 +7,24 @@ Flamegraphs are highly dynamic thanks to the `d3` library.
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>
<script type="text/javascript" src="assets/incendium_flamegraph_hkctthqlqhcubcsgrazymmvaldzllxbq.js"></script>
The *Download* link allows you to download the flamegraph as a javascript file.
To draw the flamegraph in a webpage you just have to add an inline `<script>` that references the javascript file.
You don't even need to call any javascript.
The script automatically inserts the flamgeraph just before the script tag.
For the flamegraph to work properly, you must include the `incendium.js` and `incendium.css` in the webpage (the generated javascript code dependes on the javascript in `incendium.js`). While incendium could be changed in order to generate completely standalone javascript, if you want to include several flamegrpahs in the same webpage, including the entire contents of `incendium.js` would be very wasteful.
For example, the flamegraph above was generated by the following HTML:
```html
<link rel="stylesheet" href="assets/incendium.css" />
<script src="assets/incendium.js" charset="utf-8"></script>
<script type="text/javascript" src="assets/incendium_flamegraph_hkctthqlqhcubcsgrazymmvaldzllxbq.js"></script>
```
\ No newline at end of file
defmodule Incendium do
@moduledoc """
Incendium is a way of ...
"""
readme_part =
"README.md"
|> File.read!()
|> String.split("<!-- ex_doc -->")
|> Enum.at(1)
@moduledoc readme_part
alias Incendium.Storage
......
......@@ -5,14 +5,29 @@ defmodule Incendium.Assets do
@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")
extra_css_path = "lib/incendium/assets/extra.css"
extra_js_path = "lib/incendium/assets/extra.js"
# Recompile this module if we rebuild the assets
@external_resource "priv/assets/incendium.css"
@external_resource "priv/assets/incendium.js"
@external_resource extra_css_path
@external_resource extra_js_path
@extra_css File.read!(extra_css_path)
@extra_js File.read!(extra_js_path)
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 extra_css() do
@extra_css
end
def build_js() do
{:ok, {_, _, d3_js}} = :httpc.request(@d3_js_url)
{:ok, {_, _, d3_tip_js}} = :httpc.request(@d3_tip_js_url)
......@@ -21,23 +36,17 @@ defmodule Incendium.Assets do
all_js = [
d3_js,
d3_tip_js,
d3_flamegraph_js
d3_flamegraph_js,
@extra_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)
File.write!(css_path(), d3_flamegraph_css)
end
def build_assets() do
......
/* 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
font-family:Open Sans,Arial;
color:#454545;
font-size:16px;
margin:2em auto;
max-width:800px;
padding:1em;
line-height:1.4;
text-align:justify
}
html.contrast body {
color:#050505
}
html.contrast blockquote {
color:#11151a
}
html.contrast blockquote:before {
color:#262626
}
html.contrast a {
color:#03f
}
html.contrast a:visited {
color:#7d013e
}
html.contrast span.wr {
color:#800
}
html.contrast span.mfw {
color:#4d0000
}
@media screen and (prefers-color-scheme:light) {
html.inverted {
background-color:#000
}
html.inverted body {
color:#d9d9d9
}
html.inverted #contrast,
html.inverted #invmode {
color:#fff;
background-color:#000
}
html.inverted blockquote {
color:#d3c9be
}
html.inverted blockquote:before {
color:#b8b8b8
}
html.inverted a {
color:#00a2e7
}
html.inverted a:visited {
color:#ca1a70
}
html.inverted span.wr {
color:#d24637
}
html.inverted span.mfw {
color:#b00000
}
html.inverted.contrast {
background-color:#000
}
html.inverted.contrast body {
color:#fff
}
html.inverted.contrast #contrast,
html.inverted.contrast #invmode {
color:#fff;
background-color:#000
}
html.inverted.contrast blockquote {
color:#f8f6f5
}
html.inverted.contrast blockquote:before {
color:#e5e5e5
}
html.inverted.contrast a {
color:#44c7ff
}
html.inverted.contrast a:visited {
color:#e9579e
}
html.inverted.contrast span.wr {
color:#db695d
}
html.inverted.contrast span.mfw {
color:#ff0d0d
}
}
@media (prefers-color-scheme:dark) {
html:not(.inverted) {
background-color:#000
}
html:not(.inverted) body {
color:#d9d9d9
}
html:not(.inverted) #contrast,
html:not(.inverted) #invmode {
color:#fff;
background-color:#000
}
html:not(.inverted) blockquote {
color:#d3c9be
}
html:not(.inverted) blockquote:before {
color:#b8b8b8
}
html:not(.inverted) a {
color:#00a2e7
}
html:not(.inverted) a:visited {
color:#ca1a70
}
html:not(.inverted) span.wr {
color:#d24637
}
html:not(.inverted) span.mfw {
color:#b00000
}
html:not(.inverted).contrast {
background-color:#000
}
html:not(.inverted).contrast body {
color:#fff
}
html:not(.inverted).contrast #contrast,
html:not(.inverted).contrast #invmode {
color:#fff;
background-color:#000
}
html:not(.inverted).contrast blockquote {
color:#f8f6f5
}
html:not(.inverted).contrast blockquote:before {
color:#e5e5e5
}
html:not(.inverted).contrast a {
color:#44c7ff
}
html:not(.inverted).contrast a:visited {
color:#e9579e
}
html:not(.inverted).contrast span.wr {
color:#db695d
}
html:not(.inverted).contrast span.mfw {
color:#ff0d0d
}
html.inverted html {
background-color:#fefefe
}
}
a {
color:#07a
}
a:visited {
color:#941352
}
.noselect {
-webkit-touch-callout:none;
-webkit-user-select:none;
-khtml-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none
}
span.citneed {
vertical-align:top;
font-size:.7em;
padding-left:.3em
}
small {
font-size:.4em
}
p.st {
margin-top:-1em
}
div.fancyPositioning div.picture-left {
float:left;
width:40%;
overflow:hidden;
margin-right:1em
}
div.fancyPositioning div.picture-left img {
width:100%
}
div.fancyPositioning div.picture-left figure {
margin:10px
}
div.fancyPositioning div.picture-left figure figcaption {
font-size:.7em
}
div.fancyPositioning div.tleft {
float:left;
width:55%
}
div.fancyPositioning div.tleft p:first-child {
margin-top:0
}
div.fancyPositioning:after {
display:block;
content:"";
clear:both
}
ul li img {
height:1em
}
blockquote {
color:#456;
margin-left:0;
margin-top:2em;
margin-bottom:2em
}
blockquote span {
float:left;
margin-left:1rem;
padding-top:1rem
}
blockquote author {
display:block;
clear:both;
font-size:.6em;
margin-left:2.4rem;
font-style:oblique
}
blockquote author:before {
content:"- ";
margin-right:1em
}
blockquote:before {
font-family:Times New Roman,Times,Arial;
color:#666;
content:open-quote;
font-size:2.2em;
font-weight:600;
float:left;
margin-top:0;
margin-right:.2rem;
width:1.2rem
}
blockquote:after {
content:"";
display:block;
clear:both
}
@media screen and (max-width:500px) {
body {
text-align:left
}
div.fancyPositioning div.picture-left,
div.fancyPositioning div.tleft {
float:none;
width:inherit
}
blockquote span {
width:80%
}
blockquote author {
padding-top:1em;
width:80%;
margin-left:15%
}
blockquote author:before {
content:"";
margin-right:inherit
}
}
span.visited {
color:#941352
}
span.visited-maroon {
color:#85144b
}
span.wr {
color:#c0392b;
font-weight:600
}
button.cont-inv,
span.wr {
text-decoration:underline
}
button.cont-inv {
cursor:pointer;
border-radius:2px;
position:fixed;
right:10px;
font-size:.8em;
border:0;
padding:2px 5px
}
#contrast {
color:#000;
top:10px
}
#contrast,
#invmode {
-webkit-touch-callout:none;
-webkit-user-select:none;
-khtml-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none
}
#invmode {
color:#fff;
background-color:#000;
position:fixed;
top:34px;
text-decoration:underline
}
@media screen and (max-width:1080px) {
#contrast,
#invmode {
position:absolute
}
}
span.sb {
color:#00e
}
span.sb,
span.sv {
cursor:not-allowed
}
span.sv {
color:#551a8b
}
span.foufoufou {
color:#444;
font-weight:700
}
span.foufoufou:before {
content:"";
display:inline-block;
width:1em;
height:1em;
margin-left:.2em;
margin-right:.2em;
background-color:#444
}
span.foufivfoufivfoufiv {
color:#454545;
font-weight:700
}
span.foufivfoufivfoufiv:before {