├── .formatter.exs ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENCE.txt ├── README.md ├── config └── config.exs ├── doc_extra ├── assets │ ├── incendium.css │ ├── incendium.js │ └── incendium_flamegraph_hkctthqlqhcubcsgrazymmvaldzllxbq.js └── pages │ └── Example flamegraph.md ├── lib ├── incendium.ex ├── incendium │ ├── application.ex │ ├── assets.ex │ ├── assets │ │ ├── extra.css │ │ ├── extra.js │ │ └── vendor │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.js │ │ │ ├── d3-flame-graph │ │ │ ├── d3-flame-graph.js │ │ │ ├── d3-tip.js │ │ │ ├── d3.js │ │ │ ├── flamegraph.css │ │ │ └── jquery.js │ ├── benchee.ex │ ├── benchee_formatter_common.ex │ ├── benchee_formatter_data.ex │ ├── benchee_html_formatter.ex │ ├── benchee_server.ex │ ├── controller.ex │ ├── decorator.ex │ ├── flamegraph.ex │ ├── storage.ex │ ├── templates │ │ ├── flamegraph.js.eex │ │ ├── partials │ │ │ ├── data_table.html.eex │ │ │ ├── disk_space_usage.html.eex │ │ │ ├── flamegraph.html.eex │ │ │ └── system_info.html.eex │ │ ├── suite.html.eex │ │ └── view │ │ │ ├── flamegraph.js.eex │ │ │ └── latest-flamegraph.html.eex │ └── view.ex └── mix │ ├── incendium.assets.ex │ └── incendium.build_assets.ex ├── mix.exs ├── mix.lock ├── priv └── assets │ ├── benchee-incendium.css │ ├── benchee-incendium.js │ ├── incendium.css │ └── incendium.js ├── scripts └── release.exs └── test ├── incendium_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | incendium-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "clearfix" 4 | ] 5 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | ### 0.2.0 - 2021-07-05 00:00:02 6 | 7 | First public version. 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021 by Tiago Barroso 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Incendium 2 | 3 | 4 | Easy profiling for your Phoenix controller actions (and other functions) using [flamegraphs](http://www.brendangregg.com/flamegraphs.html). 5 | 6 | #### Example flamegraph 7 | 8 | 9 | 10 | 11 | 12 | 13 | ## Installation 14 | 15 | The package can be installed 16 | by adding `incendium` to your list of dependencies in `mix.exs`: 17 | 18 | ```elixir 19 | def deps do 20 | [ 21 | {:incendium, "~> x.y.z"} 22 | ] 23 | end 24 | ``` 25 | 26 | Documentation can be found at [https://hexdocs.pm/incendium](https://hexdocs.pm/incendium). 27 | 28 | 29 | ## Rationale 30 | 31 | Profiling Elixir code is easy using the default Erlang tools, such as `fprof`. 32 | These tools produce a lot of potentially useful data, but visualizing and interpreting all that data is not easy. 33 | The erlang tool [`eflame`](https://github.com/proger/eflame) contains some utilities to generate flamegraphs from the results of profiling your code 34 | 35 | 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. 36 | And although the generated SVGs support some minimal interaction, it's possible to do better. 37 | 38 | 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. 39 | Fortunately there is a very good javascrtip library to generate flamegraphs: [d3-flame-graph](https://github.com/spiermar/d3-flame-graph). 40 | 41 | 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. 42 | That way, instead of the manual steps above you can just visit an URL in your web application. 43 | 44 | ## Batch usage (usage with benchmarks) 45 | 46 | Incendium can be used to run benchmarks with integrated profiling data (in the form of flamegraphs). 47 | It uses [Benchee]() under the hood, and the API is actually quite similar to Benchee's. 48 | 49 | It provides a single function, namely `Incendium.run/2`, which takes the same arguments as `Benchee.run/2`, plus some incendium-specific ones. The main difference is that the suite title is a required keyword argument instead of an optional one. 50 | 51 | An example: 52 | 53 | ```elixir 54 | defmodule Incendium.Benchmarks.Example do 55 | defp map_fun(i) do 56 | [i, i * i] 57 | end 58 | 59 | def run() do 60 | list = Enum.to_list(1..10_000) 61 | 62 | Incendium.run(%{ 63 | "flat_map" => fn -> Enum.flat_map(list, &map_fun/1) end, 64 | "map.flatten" => fn -> list |> Enum.map(&map_fun/1) |> List.flatten() end 65 | }, 66 | title: "Example", 67 | incendium_flamegraph_widths_to_scale: true 68 | ) 69 | end 70 | end 71 | 72 | Incendium.Benchmarks.Example.run() 73 | ``` 74 | 75 | The output of the script above can be found [here](https://hexdocs.pm/incendium/0.3.0/assets/Example.html). 76 | 77 | ## Interactive Usage (intgrated with a Phoenix web application) 78 | 79 | To use `incendium` in your web application, you need to follow these steps: 80 | 81 | ### 1. Add Incendium as a dependency 82 | 83 | ```elixir 84 | def deps do 85 | [ 86 | {:incendium, "~> 0.2.0"} 87 | ] 88 | end 89 | ``` 90 | 91 | You can make it a `:dev` only dependency if you wish, but Incendium will only decorate your functions if you're in `:dev` mode. 92 | 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) 93 | 94 | ### 2. Create an Incendium Controller for your application 95 | 96 | ```elixir 97 | # lib/my_app_web/controllers/incencdium_controller.ex 98 | 99 | defmodule MyApp.IncendiumController do 100 | use Incendium.Controller, 101 | routes_module: MyAppWeb.Router.Helpers, 102 | otp_app: :my_app 103 | end 104 | ``` 105 | 106 | There is no need to define an accompanying view. 107 | Currently the controller is not extensible (and there aren't many natural extension points anyway). 108 | 109 | 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. 110 | On unusual Phoenix apps which have static files in other places, this might not work as expected. 111 | Currently there isn't a way to override the place where the static files should be added. 112 | 113 | ### 3. Add the controller to your Router 114 | 115 | ```elixir 116 | # lib/my_app_web/controllers/router.ex 117 | 118 | require Incendium 119 | 120 | scope "/incendium", MyAppWeb do 121 | Incendium.routes(IncendiumController) 122 | end 123 | 124 | ``` 125 | 126 | ### 4. Decorate the functions you want to profile 127 | 128 | Incendium decorators depend on the [decorator](https://hex.pm/packages/decorator) package. 129 | 130 | ```elixir 131 | defmodule MyAppWeb.MyContext.ExampleController do 132 | use MyAppWeb.Mandarin, :controller 133 | 134 | # Activate the incendium decorators 135 | use Incendium.Decorator 136 | 137 | # Each invocation of the `index/2` function will be traced and profiled. 138 | @decorate incendium_profile_with_tracing() 139 | def index(conn, params) do 140 | resources = MyContext.list_resources(params) 141 | render(conn, "index.html", resources: resources) 142 | end 143 | end 144 | ``` 145 | 146 | Currently incendium only supports tracing profilers 147 | (which are very slow and not practical in production). 148 | In the future we may support better options such as sampling profilers. 149 | 150 | ### 5. Visit the `/incendium` route to see the generated flamegraph 151 | 152 | Each time you run a profiled function, a new stacktrace will be generated. 153 | Stacktraces are not currently saves, you can only access the latest one. 154 | In the future we might add a persistence layer that stores a number of stacktraces instead of keeping just the last one. 155 | 156 | [Here](https://hexdocs.pm/incendium/example-flamegraph.html) you can find an example flamegraph with explanations about how to interact with it . -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :phoenix, :json_library, Jason 4 | -------------------------------------------------------------------------------- /doc_extra/assets/incendium.css: -------------------------------------------------------------------------------- 1 | .d3-flame-graph rect { 2 | stroke: #EEEEEE; 3 | fill-opacity: .8; 4 | } 5 | 6 | .d3-flame-graph rect:hover { 7 | stroke: #474747; 8 | stroke-width: 0.5; 9 | cursor: pointer; 10 | } 11 | 12 | .d3-flame-graph-label { 13 | pointer-events: none; 14 | white-space: nowrap; 15 | text-overflow: ellipsis; 16 | overflow: hidden; 17 | font-size: 12px; 18 | font-family: Verdana; 19 | margin-left: 4px; 20 | margin-right: 4px; 21 | line-height: 1.5; 22 | padding: 0 0 0; 23 | font-weight: 400; 24 | color: black; 25 | text-align: left; 26 | } 27 | 28 | .d3-flame-graph .fade { 29 | opacity: 0.6 !important; 30 | } 31 | 32 | .d3-flame-graph .title { 33 | font-size: 20px; 34 | font-family: Verdana; 35 | } 36 | 37 | .d3-flame-graph-tip { 38 | line-height: 1; 39 | font-family: Verdana; 40 | font-size: 12px; 41 | padding: 12px; 42 | background: rgba(0, 0, 0, 0.8); 43 | color: #fff; 44 | border-radius: 2px; 45 | pointer-events: none; 46 | } 47 | 48 | /* Creates a small triangle extender for the tooltip */ 49 | .d3-flame-graph-tip:after { 50 | box-sizing: border-box; 51 | display: inline; 52 | font-size: 10px; 53 | width: 100%; 54 | line-height: 1; 55 | color: rgba(0, 0, 0, 0.8); 56 | position: absolute; 57 | pointer-events: none; 58 | } 59 | 60 | /* Northward tooltips */ 61 | .d3-flame-graph-tip.n:after { 62 | content: "\25BC"; 63 | margin: -1px 0 0 0; 64 | top: 100%; 65 | left: 0; 66 | text-align: center; 67 | } 68 | 69 | /* Eastward tooltips */ 70 | .d3-flame-graph-tip.e:after { 71 | content: "\25C0"; 72 | margin: -4px 0 0 0; 73 | top: 50%; 74 | left: -8px; 75 | } 76 | 77 | /* Southward tooltips */ 78 | .d3-flame-graph-tip.s:after { 79 | content: "\25B2"; 80 | margin: 0 0 1px 0; 81 | top: -8px; 82 | left: 0; 83 | text-align: center; 84 | } 85 | 86 | /* Westward tooltips */ 87 | .d3-flame-graph-tip.w:after { 88 | content: "\25B6"; 89 | margin: -4px 0 0 -1px; 90 | top: 50%; 91 | left: 100%; 92 | } -------------------------------------------------------------------------------- /doc_extra/assets/incendium_flamegraph_hkctthqlqhcubcsgrazymmvaldzllxbq.js: -------------------------------------------------------------------------------- 1 | 2 | var data_hkctthqlqhcubcsgrazymmvaldzllxbq = {"children":[{"children":[],"name":"","value":1},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.DBConnection.Holder:checkout_call/5","value":1}],"name":"Elixir.DBConnection.Holder:checkout/3","value":1}],"name":"Elixir.DBConnection:checkout/3","value":1},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"erlang:port_control/3","value":2}],"name":"prim_inet:ctl_cmd/3","value":2}],"name":"prim_inet:async_recv/3","value":2}],"name":"prim_inet:recv0/3","value":2}],"name":"Elixir.Postgrex.Protocol:msg_recv/4","value":2}],"name":"Elixir.Postgrex.Protocol:recv_bind/3","value":2},{"children":[{"children":[{"children":[],"name":"Elixir.Postgrex.DefaultTypes:Elixir.Postgrex.Extensions.Raw/6","value":1},{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.NaiveDateTime:new/8","value":1}],"name":"Elixir.NaiveDateTime:from_erl/3","value":1}],"name":"Elixir.NaiveDateTime:from_erl!/3","value":1},{"children":[{"children":[{"children":[{"children":[],"name":"calendar:gregorian_days_to_date/1","value":1}],"name":"calendar:gregorian_seconds_to_datetime/1","value":1}],"name":"Elixir.Postgrex.Extensions.Timestamp:split/2","value":1}],"name":"Elixir.Postgrex.Extensions.Timestamp:microsecond_to_elixir/2","value":2}],"name":"Elixir.Postgrex.DefaultTypes:Elixir.Postgrex.Extensions.Timestamp/6","value":3}],"name":"Elixir.Postgrex.Protocol:rows_recv/4","value":4}],"name":"Elixir.Postgrex.Protocol:recv_execute/5","value":4}],"name":"Elixir.Postgrex.Protocol:bind_execute/4","value":6},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"erlang:port_control/3","value":1}],"name":"prim_inet:ctl_cmd/3","value":1}],"name":"prim_inet:async_recv/3","value":1}],"name":"prim_inet:recv0/3","value":1}],"name":"Elixir.Postgrex.Protocol:msg_recv/4","value":1}],"name":"Elixir.Postgrex.Protocol:recv_close/3","value":1},{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Postgrex.Messages:parse/3","value":1}],"name":"Elixir.Postgrex.Protocol:msg_decode/1","value":1}],"name":"Elixir.Postgrex.Protocol:msg_recv/3","value":1}],"name":"Elixir.Postgrex.Protocol:recv_describe/4","value":1}],"name":"Elixir.Postgrex.Protocol:recv_parse_describe/4","value":1}],"name":"Elixir.Postgrex.Protocol:close_parse_describe_flush/3","value":2}],"name":"Elixir.Postgrex.Protocol:handle_prepare_execute/4","value":2}],"name":"Elixir.DBConnection.Holder:holder_apply/4","value":8}],"name":"Elixir.DBConnection:run_execute/5","value":8}],"name":"Elixir.DBConnection:run/6","value":9}],"name":"Elixir.DBConnection:execute/4","value":9},{"children":[{"children":[{"children":[],"name":"erlang:monotonic_time/0","value":1}],"name":"Elixir.DBConnection.LogEntry:parse_times/2","value":1},{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Kernel:inspect/2","value":1}],"name":"Elixir.Ecto.Adapters.SQL:log_ok_source/1","value":1},{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"io_lib_format:pow5bits/1","value":1}],"name":"io_lib_format:convert_to_decimal/3","value":1}],"name":"io_lib_format:fwrite_g_1/2","value":1}],"name":"io_lib_format:fwrite_g/1","value":1}],"name":"Elixir.Ecto.Adapters.SQL:log_time/4","value":1}],"name":"Elixir.Ecto.Adapters.SQL:log_iodata/2","value":2}],"name":"Elixir.Logger:__do_log__/4","value":2},{"children":[{"children":[{"children":[],"name":"Elixir.Logger.Handler:erlang_metadata_to_elixir_metadata/1","value":1},{"children":[],"name":"gen_event:notify/2","value":1}],"name":"Elixir.Logger.Handler:log/2","value":2}],"name":"logger_backend:call_handlers/3","value":2}],"name":"Elixir.Ecto.Adapters.SQL:log/4","value":4}],"name":"Elixir.DBConnection:log/5","value":5}],"name":"Elixir.Ecto.Adapters.Postgres.Connection:execute/4","value":14}],"name":"Elixir.Ecto.Adapters.SQL:execute!/4","value":14}],"name":"Elixir.Ecto.Adapters.SQL:execute/5","value":14},{"children":[{"children":[{"children":[],"name":"Elixir.Ecto.Query.Planner:plan_sources/2","value":1}],"name":"Elixir.Ecto.Query.Planner:plan/3","value":1}],"name":"Elixir.Ecto.Query.Planner:query/5","value":1},{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Ecto.Type:of_base_type?/2","value":1}],"name":"Elixir.Ecto.Type:adapter_load/3","value":1}],"name":"Elixir.Ecto.Repo.Queryable:struct_load!/6","value":1}],"name":"Elixir.Ecto.Repo.Queryable:-preprocessor/3-fun-0-/5","value":1}],"name":"Elixir.Enum:-map/2-lists^map/1-0-/2","value":1}],"name":"Elixir.Ecto.Repo.Queryable:execute/4","value":16}],"name":"Elixir.Ecto.Repo.Queryable:all/3","value":16},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"erlang:atom_to_binary/2","value":1}],"name":"Elixir.Ecto.Changeset:cast_key/1","value":1}],"name":"Elixir.Ecto.Changeset:process_param/7","value":1}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":1}],"name":"Elixir.Ecto.Changeset:cast/6","value":1},{"children":[{"children":[],"name":"Elixir.Access:get/3","value":1},{"children":[{"children":[],"name":"erlang:atom_to_binary/2","value":1}],"name":"Elixir.Ecto.Changeset:cast_key/1","value":1}],"name":"Elixir.Ecto.Changeset:cast_relation/4","value":2},{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Ecto.Changeset:get_field/3","value":1}],"name":"Elixir.Ecto.Changeset:missing?/3","value":1}],"name":"Elixir.Ecto.Changeset:-validate_required/3-fun-0-/5","value":1}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":1}],"name":"Elixir.Ecto.Changeset:validate_required/3","value":1},{"children":[],"name":"Elixir.Forage:put_assoc/3","value":1}],"name":"Elixir.Mediv.Inpatient.InpatientEpisode:changeset/2","value":5}],"name":"Elixir.MedivWeb.Inpatient.InpatientEpisodeController:-new/2-fun-0-/1","value":21},{"children":[{"children":[{"children":[],"name":"Elixir.Phoenix.Controller:layout/3","value":1}],"name":"Elixir.Phoenix.Controller:prepare_assigns/4","value":1},{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Application:fetch_env!/2","value":1},{"children":[{"children":[],"name":"Elixir.Gettext.Interpolation:to_interpolatable/1","value":1}],"name":"Elixir.MedivWeb.Gettext:handle_missing_translation/4","value":1}],"name":"Elixir.Gettext:dpgettext/5","value":2},{"children":[{"children":[{"children":[],"name":"Elixir.Application:fetch_env!/2","value":1},{"children":[{"children":[],"name":"Elixir.Gettext.Interpolation:interpolate/4","value":1}],"name":"Elixir.MedivWeb.Gettext:handle_missing_translation/4","value":2}],"name":"Elixir.Gettext:dpgettext/5","value":3},{"children":[{"children":[{"children":[],"name":"maps:find/2","value":1}],"name":"Elixir.Phoenix.Router.Helpers:build_own_forward_path/3","value":1}],"name":"Elixir.Phoenix.Router.Helpers:path/3","value":1}],"name":"Elixir.MedivWeb.InpatientLayoutView:sidebar.html/1","value":5},{"children":[{"children":[],"name":"Elixir.Access:fetch/2","value":1}],"name":"Elixir.Phoenix.HTML.Engine:fetch_assign!/2","value":1},{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Keyword:get/3","value":1}],"name":"Elixir.String:replace_guarded/4","value":1}],"name":"Elixir.Phoenix.HTML.Tag:build_attrs/3","value":1}],"name":"Elixir.Phoenix.HTML.Tag:tag/2","value":1}],"name":"Elixir.MedivWeb.InpatientLayoutView:layout.html/1","value":9},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Plug.HTML:to_iodata/5","value":1}],"name":"Elixir.MedivWeb.Inpatient.InpatientEpisodeView:-form.html/1-fun-16-/2","value":1}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":1},{"children":[{"children":[{"children":[],"name":"Elixir.Keyword:get_values/2","value":1},{"children":[],"name":"Elixir.Keyword:get_values/3","value":1}],"name":"Elixir.ForageWeb.ForageView:forage_error_tag/3","value":2},{"children":[{"children":[],"name":"Elixir.Keyword:pop/3","value":1}],"name":"Elixir.ForageWeb.ForageView:forage_generic_input/5","value":1},{"children":[{"children":[],"name":"Elixir.Plug.HTML:to_iodata/5","value":1},{"children":[{"children":[],"name":"erlang:atom_to_binary/2","value":2}],"name":"Elixir.String.Chars.Atom:to_string/1","value":2}],"name":"Elixir.ForageWeb.ForageView:forage_multiple_select/3","value":3},{"children":[{"children":[{"children":[],"name":"lists:keyfind/3","value":1}],"name":"Elixir.Keyword:fetch!/2","value":1},{"children":[],"name":"Elixir.Keyword:get/3","value":1},{"children":[],"name":"Elixir.Plug.HTML:to_iodata/5","value":1},{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.String.Chars.Atom:__impl__/1","value":1}],"name":"Elixir.String.Chars:impl_for/1","value":1}],"name":"Elixir.String.Chars:impl_for!/1","value":1}],"name":"Elixir.String.Chars:to_string/1","value":1}],"name":"Elixir.ForageWeb.ForageView:forage_select/3","value":5},{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"erlang:integer_to_binary/1","value":1}],"name":"Elixir.Phoenix.HTML.Safe.Integer:to_iodata/1","value":1},{"children":[{"children":[{"children":[],"name":"sleep","value":2}],"name":"code_server:call/1","value":2}],"name":"error_handler:undefined_function/3","value":2}],"name":"Elixir.ForageWeb.ForageView:-forage_static_select/3-fun-0-/4","value":3}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":3}],"name":"Elixir.ForageWeb.ForageView:forage_static_select/3","value":3},{"children":[{"children":[],"name":"Elixir.Phoenix.Router.Helpers:path/3","value":1}],"name":"Elixir.MedivWeb.Inpatient.InpatientEpisodeView:-form.html/1-fun-2-/3","value":1},{"children":[{"children":[{"children":[],"name":"Elixir.Phoenix.Router.Helpers:build_own_forward_path/3","value":1}],"name":"Elixir.Phoenix.Router.Helpers:path/3","value":1}],"name":"Elixir.MedivWeb.Inpatient.InpatientEpisodeView:-form.html/1-fun-7-/3","value":1},{"children":[{"children":[{"children":[],"name":"Elixir.Keyword:-take/2-fun-0-/2","value":1}],"name":"Elixir.Keyword:-take/2-lists^filter/1-0-/2","value":1},{"children":[{"children":[{"children":[],"name":"erlang:atom_to_binary/2","value":1}],"name":"Elixir.String.Chars.Atom:to_string/1","value":1}],"name":"Elixir.Phoenix.HTML.Form:input_id/2","value":2},{"children":[{"children":[],"name":"Elixir.Phoenix.HTML.Tag:build_attrs/2","value":1},{"children":[{"children":[{"children":[{"children":[],"name":"lists:keyfind/3","value":1}],"name":"Elixir.Keyword:get/3","value":1}],"name":"Elixir.String:replace_guarded/4","value":1}],"name":"Elixir.Phoenix.HTML.Tag:build_attrs/3","value":1},{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Plug.HTML:to_iodata/5","value":1}],"name":"Elixir.Phoenix.HTML.Tag:-tag_attrs/1-fun-0-/2","value":2}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":2}],"name":"Elixir.Phoenix.HTML.Tag:tag_attrs/1","value":2}],"name":"Elixir.Phoenix.HTML.Tag:tag/2","value":4}],"name":"Elixir.Phoenix.HTML.Form:checkbox/3","value":7},{"children":[{"children":[],"name":"Elixir.Keyword:put_new/3","value":1},{"children":[{"children":[{"children":[],"name":"erlang:atom_to_binary/2","value":1}],"name":"Elixir.String.Chars.Atom:to_string/1","value":1}],"name":"Elixir.Phoenix.HTML.Form:input_id/2","value":1}],"name":"Elixir.Phoenix.HTML.Form:label/4","value":3},{"children":[{"children":[{"children":[],"name":"Elixir.Phoenix.HTML.FormData.Ecto.Changeset:input_value/4","value":1}],"name":"Elixir.Phoenix.HTML.Form:input_value/2","value":1}],"name":"Elixir.Phoenix.HTML.Form:textarea/3","value":1},{"children":[{"children":[{"children":[],"name":"Elixir.String:replace/4","value":2},{"children":[{"children":[{"children":[],"name":"lists:keyfind/3","value":1}],"name":"Elixir.Keyword:get/3","value":1},{"children":[],"name":"sleep","value":6}],"name":"Elixir.String:replace_guarded/4","value":7}],"name":"Elixir.Phoenix.HTML.Tag:build_attrs/3","value":9},{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Plug.HTML:html_escape_to_iodata/1","value":1},{"children":[],"name":"Elixir.Plug.HTML:to_iodata/5","value":4}],"name":"Elixir.Phoenix.HTML.Tag:-tag_attrs/1-fun-0-/2","value":5}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":5}],"name":"Elixir.Phoenix.HTML.Tag:tag_attrs/1","value":5},{"children":[{"children":[],"name":"Elixir.Plug.HTML:html_escape_to_iodata/1","value":1},{"children":[],"name":"Elixir.Plug.HTML:to_iodata/5","value":1}],"name":"Elixir.Phoenix.HTML:html_escape/1","value":2}],"name":"Elixir.Phoenix.HTML.Tag:content_tag/3","value":16},{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Keyword:get/2","value":1},{"children":[{"children":[],"name":"lists:keyfind/3","value":1}],"name":"Elixir.Keyword:get/3","value":2},{"children":[],"name":"sleep","value":3}],"name":"Elixir.String:replace_guarded/4","value":7},{"children":[],"name":"lists:mergel/2","value":1}],"name":"Elixir.Phoenix.HTML.Tag:build_attrs/3","value":8},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Phoenix.HTML.Safe:impl_for/1","value":1}],"name":"Elixir.Phoenix.HTML.Safe:impl_for!/1","value":1}],"name":"Elixir.Phoenix.HTML.Safe:to_iodata/1","value":1},{"children":[],"name":"Elixir.Plug.HTML:to_iodata/5","value":2}],"name":"Elixir.Phoenix.HTML.Tag:-tag_attrs/1-fun-0-/2","value":3}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":4}],"name":"Elixir.Phoenix.HTML.Tag:tag_attrs/1","value":4}],"name":"Elixir.Phoenix.HTML.Tag:tag/2","value":12}],"name":"Elixir.ForageWeb.ForageView:forage_form_group/5","value":55},{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Keyword:fetch/2","value":1}],"name":"Elixir.Keyword:pop/3","value":1}],"name":"Elixir.ForageWeb.ForageView:forage_generic_input/5","value":1},{"children":[{"children":[{"children":[],"name":"Elixir.String.Chars.Atom:to_string/1","value":1}],"name":"Elixir.Phoenix.HTML.Form:input_name/2","value":1},{"children":[{"children":[{"children":[],"name":"Elixir.Phoenix.HTML.Tag:dasherize/1","value":1},{"children":[],"name":"Elixir.String:replace/3","value":1},{"children":[],"name":"Elixir.String:replace/4","value":1},{"children":[{"children":[],"name":"Elixir.Keyword:get/3","value":1},{"children":[],"name":"sleep","value":2}],"name":"Elixir.String:replace_guarded/4","value":3}],"name":"Elixir.Phoenix.HTML.Tag:build_attrs/3","value":6},{"children":[{"children":[{"children":[],"name":"Elixir.Phoenix.HTML.Tag:-tag_attrs/1-fun-0-/2","value":1}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":1}],"name":"Elixir.Phoenix.HTML.Tag:tag_attrs/1","value":1}],"name":"Elixir.Phoenix.HTML.Tag:tag/2","value":7}],"name":"Elixir.Phoenix.HTML.Form:checkbox/3","value":8},{"children":[{"children":[],"name":"Elixir.Keyword:put_new/3","value":1},{"children":[{"children":[{"children":[],"name":"erlang:atom_to_binary/2","value":1}],"name":"Elixir.String.Chars.Atom:to_string/1","value":2}],"name":"Elixir.Phoenix.HTML.Form:input_id/2","value":2}],"name":"Elixir.Phoenix.HTML.Form:label/4","value":3},{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Plug.HTML:to_iodata/5","value":1}],"name":"Elixir.Phoenix.HTML.Tag:-tag_attrs/1-fun-0-/2","value":1}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":1}],"name":"Elixir.Phoenix.HTML.Tag:tag_attrs/1","value":1}],"name":"Elixir.Phoenix.HTML.Tag:content_tag/3","value":1}],"name":"Elixir.ForageWeb.ForageView:forage_generic_form_check/6","value":13},{"children":[{"children":[{"children":[{"children":[],"name":"application_controller:get_env/2","value":1}],"name":"Elixir.Application:fetch_env/2","value":2}],"name":"Elixir.Application:fetch_env!/2","value":2},{"children":[],"name":"Elixir.Gettext:get_locale/1","value":1},{"children":[{"children":[],"name":"Elixir.Gettext.Interpolation:interpolate/4","value":3},{"children":[],"name":"Elixir.Gettext.Interpolation:to_interpolatable/1","value":2}],"name":"Elixir.MedivWeb.Gettext:handle_missing_translation/4","value":6},{"children":[],"name":"Elixir.MedivWeb.Gettext:lgettext/5","value":2}],"name":"Elixir.Gettext:dpgettext/5","value":11},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.String.Chars:impl_for/1","value":1}],"name":"Elixir.String.Chars:impl_for!/1","value":1}],"name":"Elixir.String.Chars:to_string/1","value":1}],"name":"Elixir.Phoenix.HTML.Form:input_id/2","value":1}],"name":"Elixir.Phoenix.HTML.Form:label/4","value":1},{"children":[{"children":[{"children":[],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":1}],"name":"Elixir.Phoenix.HTML.Tag:tag_attrs/1","value":1},{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.String.Chars.Atom:__impl__/1","value":1}],"name":"Elixir.String.Chars:impl_for/1","value":1}],"name":"Elixir.String.Chars:impl_for!/1","value":1}],"name":"Elixir.String.Chars:to_string/1","value":1}],"name":"Elixir.Phoenix.HTML.Tag:content_tag/3","value":3},{"children":[{"children":[{"children":[],"name":"Elixir.Phoenix.HTML.Tag:dasherize/1","value":1}],"name":"Elixir.Phoenix.HTML.Tag:build_attrs/3","value":2},{"children":[{"children":[],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":1}],"name":"Elixir.Phoenix.HTML.Tag:tag_attrs/1","value":1}],"name":"Elixir.Phoenix.HTML.Tag:tag/2","value":3}],"name":"Elixir.ForageWeb.ForageView:forage_form_group/5","value":7},{"children":[{"children":[{"children":[{"children":[],"name":"application_controller:get_env/2","value":1}],"name":"Elixir.Application:fetch_env/2","value":1}],"name":"Elixir.Application:fetch_env!/2","value":1}],"name":"Elixir.Gettext:dpgettext/5","value":1}],"name":"Elixir.MedivWeb.Inpatient.InpatientEpisodeView:-form.html/1-fun-0-/1","value":8},{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Map:get/2","value":1}],"name":"Elixir.ForageWeb.ForageView:forage_select/3","value":1},{"children":[{"children":[{"children":[],"name":"Elixir.Plug.HTML:to_iodata/5","value":1}],"name":"Elixir.Phoenix.HTML:html_escape/1","value":1}],"name":"Elixir.Phoenix.HTML.Tag:content_tag/3","value":1},{"children":[{"children":[{"children":[{"children":[],"name":"sleep","value":1}],"name":"Elixir.String:replace_guarded/4","value":1}],"name":"Elixir.Phoenix.HTML.Tag:build_attrs/3","value":1}],"name":"Elixir.Phoenix.HTML.Tag:tag/2","value":1}],"name":"Elixir.ForageWeb.ForageView:forage_form_group/5","value":3},{"children":[{"children":[{"children":[],"name":"Elixir.Gettext.Interpolation:to_interpolatable/1","value":1}],"name":"Elixir.MedivWeb.Gettext:handle_missing_translation/4","value":1}],"name":"Elixir.Gettext:dpgettext/5","value":1}],"name":"Elixir.MedivWeb.Inpatient.InpatientEpisodeView:-form.html/1-fun-13-/2","value":4}],"name":"Elixir.Phoenix.HTML.Form:-inputs_for/4-fun-0-/3","value":12}],"name":"Elixir.Enum:-map/2-lists^map/1-0-/2","value":12},{"children":[{"children":[],"name":"Elixir.Enum:member?/2","value":1}],"name":"Elixir.Keyword:-take/2-lists^filter/1-0-/2","value":1},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Ecto.Changeset:cast_relation/4","value":1},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"erlang:atom_to_binary/2","value":1}],"name":"Elixir.Ecto.Changeset:cast_key/1","value":1}],"name":"Elixir.Ecto.Changeset:process_param/7","value":1}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":1}],"name":"Elixir.Ecto.Changeset:cast/6","value":1},{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"lists:keyfind/3","value":1}],"name":"Elixir.Access:get/3","value":1}],"name":"Elixir.Ecto.Changeset:-validate_required/3-fun-0-/5","value":1}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":1}],"name":"Elixir.Ecto.Changeset:validate_required/3","value":1}],"name":"Elixir.Mediv.Inpatient.Patient:changeset/2","value":2}],"name":"Elixir.Ecto.Changeset:-on_cast_default/2-fun-0-/4","value":3}],"name":"Elixir.Phoenix.HTML.FormData.Ecto.Changeset:cast!/2","value":3}],"name":"Elixir.Phoenix.HTML.FormData.Ecto.Changeset:to_changeset/4","value":3}],"name":"Elixir.Phoenix.HTML.FormData.Ecto.Changeset:-to_form/4-fun-0-/8","value":3}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":3}],"name":"Elixir.Phoenix.HTML.FormData.Ecto.Changeset:to_form/4","value":3}],"name":"Elixir.Phoenix.HTML.Form:inputs_for/4","value":16}],"name":"Elixir.MedivWeb.Inpatient.InpatientEpisodeView:-form.html/1-fun-19-/2","value":96},{"children":[{"children":[{"children":[],"name":"Elixir.Keyword:pop/3","value":1}],"name":"Elixir.Phoenix.HTML.FormData.Ecto.Changeset:to_form/2","value":1},{"children":[{"children":[{"children":[],"name":"sleep","value":3}],"name":"code_server:call/1","value":3}],"name":"error_handler:undefined_function/3","value":3}],"name":"Elixir.Phoenix.HTML.Form:form_for/3","value":4},{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"crypto:exor/5","value":1}],"name":"Elixir.Plug.CSRFProtection:mask/1","value":1}],"name":"Elixir.Plug.CSRFProtection:get_csrf_token/0","value":1}],"name":"Elixir.Phoenix.HTML.Tag:csrf_token_tag/3","value":1},{"children":[{"children":[{"children":[],"name":"Elixir.Phoenix.HTML.Tag:dasherize/1","value":1}],"name":"Elixir.Phoenix.HTML.Tag:build_attrs/3","value":1}],"name":"Elixir.Phoenix.HTML.Tag:tag/2","value":1}],"name":"Elixir.Phoenix.HTML.Tag:form_tag/2","value":2}],"name":"Elixir.Phoenix.HTML.Form:form_for/4","value":102}],"name":"Elixir.MedivWeb.Inpatient.InpatientEpisodeView:form.html/1","value":102},{"children":[{"children":[{"children":[],"name":"Elixir.Phoenix.HTML.Tag:dasherize/1","value":1}],"name":"Elixir.Phoenix.HTML.Tag:build_attrs/3","value":1}],"name":"Elixir.Phoenix.HTML.Tag:content_tag/3","value":1}],"name":"Elixir.MedivWeb.Inpatient.InpatientEpisodeView:new.html/1","value":103}],"name":"Elixir.Phoenix.View:render_within/3","value":103}],"name":"Elixir.Phoenix.View:render_to_iodata/3","value":112}],"name":"Elixir.Phoenix.Controller:render_and_send/4","value":113},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"sleep","value":1}],"name":"Elixir.String:replace_guarded/4","value":1}],"name":"Elixir.Phoenix.LiveReloader:key/1","value":1}],"name":"Elixir.Phoenix.LiveReloader:-attrs/1-fun-0-/1","value":1}],"name":"Elixir.Enum:-map/2-lists^map/1-0-/2","value":1}],"name":"Elixir.Phoenix.LiveReloader:reload_assets_tag/3","value":1}],"name":"Elixir.Phoenix.LiveReloader:-before_send_inject_reloader/3-fun-1-/3","value":1},{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":1}],"name":"Elixir.Plug.Conn:get_resp_header/2","value":1}],"name":"Elixir.Plug.CSRFProtection:js_content_type?/1","value":1}],"name":"Elixir.Plug.CSRFProtection:ensure_same_origin_and_csrf_token!/3","value":1},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[],"name":"Elixir.Logger.Utils:timestamp/2","value":1}],"name":"Elixir.Logger.Handler:log/2","value":1}],"name":"logger_backend:call_handlers/3","value":1}],"name":"telemetry:-execute/3-fun-0-/4","value":1}],"name":"lists:foreach/2","value":1}],"name":"Elixir.Plug.Telemetry:-call/2-fun-0-/4","value":1}],"name":"Elixir.Enum:-reduce/3-lists^foldl/2-0-/3","value":4}],"name":"Elixir.Plug.Conn:run_before_send/2","value":4}],"name":"Elixir.Plug.Conn:send_resp/1","value":4}],"name":"eflame:apply1/3","value":138}],"name":"<0.861.0>","value":138}],"name":"root","value":139}; 3 | incendiumFlamegraph("hkctthqlqhcubcsgrazymmvaldzllxbq", data_hkctthqlqhcubcsgrazymmvaldzllxbq) 4 | -------------------------------------------------------------------------------- /doc_extra/pages/Example flamegraph.md: -------------------------------------------------------------------------------- 1 | # Example flamegraph 2 | 3 | This is an example flamegraph generated by Incendium. 4 | 5 | Flamegraphs are highly dynamic thanks to the `d3` library. 6 | Ýou can click on a horizontal rectangle (which represents a function call) 7 | to zoom in on the rectangle and its descendants. 8 | You can use the search form on the top right to search for a specific function name. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | The *Download* link allows you to download the flamegraph as a javascript file. 17 | To draw the flamegraph in a webpage you just have to add an inline ` 28 | 29 | 30 | ``` -------------------------------------------------------------------------------- /lib/incendium.ex: -------------------------------------------------------------------------------- 1 | defmodule Incendium do 2 | @external_resource "README.md" 3 | 4 | readme_part = 5 | "README.md" 6 | |> File.read!() 7 | |> String.split("") 8 | |> Enum.at(1) 9 | 10 | @moduledoc """ 11 | Easy flamegraphs to profile for your web applications. 12 | 13 | #{readme_part} 14 | """ 15 | 16 | @doc """ 17 | Runs benchmarks using `Benchee` and render the scenarios as flamegraphs. 18 | 19 | Takes the same parameters as `Benchee.run/2`. 20 | Takes the following `Incendium`-specific options: 21 | 22 | - `:incendium_flamegraph_widths_to_scale` (default `true`) - 23 | sets the width of the flamegraphs according to the scenario runtime. 24 | Scenarios that take a longer time to run will generate wider flamegraphs. 25 | 26 | - `:incendium_file` (default `"incendium/\#{suite.title}`) - 27 | the path to the HTML file were Incendium saves the benchmark results 28 | (in Benchee, this is specified by a Formatter, bue Incendium unfortunatelly 29 | can't reuse the normal Benchee formatting infrastructure) 30 | 31 | All other options are passed into `Benchee.run/2` 32 | """ 33 | def run(benchmarks, options) do 34 | Incendium.Benchee.run(benchmarks, options) 35 | end 36 | 37 | 38 | alias Incendium.Storage 39 | 40 | @doc """ 41 | Adds routes for an Incendium controller. 42 | """ 43 | defmacro routes(controller) do 44 | require Phoenix.Router 45 | quote do 46 | Phoenix.Router.get "/", unquote(controller), :latest_flamegraph 47 | end 48 | end 49 | 50 | @doc """ 51 | Profiles a function call using tracing (as opposed to sampling). 52 | 53 | This profiler uses [`:eflame`](https://github.com/proger/eflame) under the hood. 54 | Profiled code will be very slow. Never use this in production. 55 | Stack data will be written to #{Storage.latest_stacks_path()}. 56 | """ 57 | def profile_with_tracing(fun) do 58 | output_file = Storage.latest_stacks_path() |> to_charlist() 59 | :eflame.apply(:normal_with_children, output_file, fun, []) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/incendium/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Incendium.Application do 2 | use Application 3 | 4 | @moduledoc false 5 | 6 | require Logger 7 | 8 | def start(_type, _args) do 9 | import Supervisor.Spec, warn: false 10 | 11 | children = [ 12 | {Registry, keys: :unique, name: Incendium.BencheeServer.Registry} 13 | ] 14 | 15 | Supervisor.start_link(children, strategy: :one_for_one) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/incendium/assets.ex: -------------------------------------------------------------------------------- 1 | defmodule Incendium.Assets do 2 | @moduledoc false 3 | 4 | @d3_js_url "https://d3js.org/d3.v4.min.js" 5 | @d3_tip_js_url "https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.9.1/d3-tip.min.js" 6 | @d3_flamegraph_js_url "https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.min.js" 7 | @jquery_js_url "https://code.jquery.com/jquery-3.4.1.slim.min.js" 8 | @bootstrap_js_url "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" 9 | 10 | @d3_flamegraph_css_url "https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.css" 11 | @bootstrap_css_url "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" 12 | 13 | extra_css_path = "lib/incendium/assets/extra.css" 14 | extra_js_path = "lib/incendium/assets/extra.js" 15 | 16 | # Recompile this module if we rebuild the assets 17 | @external_resource "priv/assets/incendium.css" 18 | @external_resource "priv/assets/incendium.js" 19 | 20 | @external_resource extra_css_path 21 | @external_resource extra_js_path 22 | 23 | @extra_css File.read!(extra_css_path) 24 | @extra_js File.read!(extra_js_path) 25 | 26 | def css_path() do 27 | Path.join([:code.priv_dir(:incendium), "assets", "incendium.css"]) 28 | end 29 | 30 | def js_path() do 31 | Path.join([:code.priv_dir(:incendium), "assets", "incendium.js"]) 32 | end 33 | 34 | def benchee_css_path() do 35 | Path.join([:code.priv_dir(:incendium), "assets", "benchee-incendium.css"]) 36 | end 37 | 38 | def benchee_js_path() do 39 | Path.join([:code.priv_dir(:incendium), "assets", "benchee-incendium.js"]) 40 | end 41 | 42 | def extra_css() do 43 | @extra_css 44 | end 45 | 46 | def all_js() do 47 | File.read!(js_path()) 48 | end 49 | 50 | def all_css() do 51 | File.read!(css_path()) 52 | end 53 | 54 | def all_benchee_js() do 55 | File.read!(benchee_js_path()) 56 | end 57 | 58 | def all_benchee_css() do 59 | File.read!(benchee_css_path()) 60 | end 61 | 62 | # Build online 63 | # ------------ 64 | 65 | def build_js() do 66 | {:ok, {_, _, d3_js}} = :httpc.request(@d3_js_url) 67 | {:ok, {_, _, d3_tip_js}} = :httpc.request(@d3_tip_js_url) 68 | {:ok, {_, _, d3_flamegraph_js}} = :httpc.request(@d3_flamegraph_js_url) 69 | 70 | all_js = [ 71 | d3_js, 72 | d3_tip_js, 73 | d3_flamegraph_js, 74 | @extra_js 75 | ] 76 | |> Enum.intersperse("\n") 77 | |> remove_js_source_map() 78 | 79 | save_to_local_cache("d3.js", d3_js) 80 | save_to_local_cache("d3-tip.js", d3_tip_js) 81 | save_to_local_cache("d3-flame-graph", d3_flamegraph_js) 82 | 83 | File.write(js_path(), all_js) 84 | end 85 | 86 | def build_benchee_js() do 87 | {:ok, {_, _, d3_js}} = :httpc.request(@d3_js_url) 88 | {:ok, {_, _, d3_tip_js}} = :httpc.request(@d3_tip_js_url) 89 | {:ok, {_, _, d3_flamegraph_js}} = :httpc.request(@d3_flamegraph_js_url) 90 | {:ok, {_, _, jquery_js}} = :httpc.request(@jquery_js_url) 91 | {:ok, {_, _, bootstrap_js}} = :httpc.request(@bootstrap_js_url) 92 | 93 | all_js = [ 94 | d3_js, 95 | d3_tip_js, 96 | d3_flamegraph_js, 97 | jquery_js, 98 | bootstrap_js, 99 | @extra_js 100 | ] 101 | |> Enum.intersperse("\n") 102 | |> remove_js_source_map() 103 | 104 | save_to_local_cache("d3.js", d3_js) 105 | save_to_local_cache("d3-tip.js", d3_tip_js) 106 | save_to_local_cache("d3-flame-graph.js", d3_flamegraph_js) 107 | save_to_local_cache("jquery.js", jquery_js) 108 | save_to_local_cache("bootstrap.js", bootstrap_js) 109 | 110 | File.write(benchee_js_path(), all_js) 111 | end 112 | 113 | def build_css() do 114 | {:ok, {_, _, d3_flamegraph_css}} = :httpc.request(@d3_flamegraph_css_url) 115 | 116 | save_to_local_cache("flamegraph.css", d3_flamegraph_css) 117 | 118 | File.write!(css_path(), remove_css_source_map(d3_flamegraph_css)) 119 | end 120 | 121 | def build_benchee_css() do 122 | {:ok, {_, _, d3_flamegraph_css}} = :httpc.request(@d3_flamegraph_css_url) 123 | {:ok, {_, _, bootstrap_css}} = :httpc.request(@bootstrap_css_url) 124 | 125 | save_to_local_cache("flamegraph.css", d3_flamegraph_css) 126 | save_to_local_cache("bootstrap.css", bootstrap_css) 127 | 128 | all_css = [ 129 | d3_flamegraph_css, 130 | bootstrap_css 131 | ] 132 | |> Enum.intersperse("\n") 133 | |> remove_css_source_map() 134 | 135 | File.write!(benchee_css_path(), all_css) 136 | end 137 | 138 | def build_assets() do 139 | build_css() 140 | build_js() 141 | build_benchee_css() 142 | build_benchee_js() 143 | end 144 | 145 | # Build offline: 146 | # -------------- 147 | 148 | def build_js_offline() do 149 | d3_js = read_from_local_cache("d3.js") 150 | d3_tip_js = read_from_local_cache("d3-tip.js") 151 | d3_flamegraph_js = read_from_local_cache("d3-flame-graph.js") 152 | jquery_js = read_from_local_cache("jquery.js") 153 | bootstrap_js = read_from_local_cache("bootstrap.js") 154 | 155 | all_js = [ 156 | d3_js, 157 | d3_tip_js, 158 | d3_flamegraph_js, 159 | jquery_js, 160 | bootstrap_js, 161 | @extra_js 162 | ] 163 | |> Enum.intersperse("\n") 164 | |> remove_js_source_map() 165 | 166 | File.write(js_path(), all_js) 167 | end 168 | 169 | def build_benchee_js_offline() do 170 | d3_js = read_from_local_cache("d3.js") 171 | d3_tip_js = read_from_local_cache("d3-tip.js") 172 | d3_flamegraph_js = read_from_local_cache("d3-flame-graph.js") 173 | 174 | all_js = [ 175 | d3_js, 176 | d3_tip_js, 177 | d3_flamegraph_js, 178 | @extra_js 179 | ] 180 | |> Enum.intersperse("\n") 181 | |> remove_js_source_map() 182 | 183 | File.write(benchee_js_path(), all_js) 184 | end 185 | 186 | def build_css_offline() do 187 | d3_flamegraph_css = read_from_local_cache("flamegraph.css") 188 | 189 | File.write!(css_path(), d3_flamegraph_css |> remove_css_source_map) 190 | end 191 | 192 | def build_benchee_css_offline() do 193 | d3_flamegraph_css = read_from_local_cache("flamegraph.css") 194 | bootstrap_css = read_from_local_cache("bootstrap.css") 195 | 196 | all_css = [ 197 | d3_flamegraph_css, 198 | bootstrap_css 199 | ] 200 | |> Enum.intersperse("\n") 201 | |> remove_css_source_map() 202 | 203 | File.write!(benchee_css_path(), all_css) 204 | end 205 | 206 | def build_assets_offline() do 207 | build_css_offline() 208 | build_js_offline() 209 | build_benchee_css_offline() 210 | build_benchee_js_offline() 211 | end 212 | 213 | # Helpers 214 | 215 | defp save_to_local_cache(filename, data) do 216 | path = Path.join("lib/incendium/assets/vendor", filename) 217 | File.write!(path, data) 218 | end 219 | 220 | defp read_from_local_cache(filename) do 221 | path = Path.join("lib/incendium/assets/vendor", filename) 222 | File.read!(path) 223 | end 224 | 225 | defp remove_js_source_map(iolist) do 226 | binary = to_string(iolist) 227 | String.replace(binary, ~r'^\s*//#\s*sourceMappingURL=[^\n]+', "\n") 228 | end 229 | 230 | defp remove_css_source_map(iolist) do 231 | binary = to_string(iolist) 232 | # Not semantically correct but the correct regex is way too complex 233 | String.replace(binary, ~r'^\s*/\*#\s*sourceMappingURL=[^\n]+', "\n") 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /lib/incendium/assets/extra.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family:Open Sans,Arial; 3 | color:#454545; 4 | font-size:16px; 5 | margin:2em auto; 6 | max-width:800px; 7 | padding:1em; 8 | line-height:1.4; 9 | text-align:justify 10 | } 11 | html.contrast body { 12 | color:#050505 13 | } 14 | html.contrast blockquote { 15 | color:#11151a 16 | } 17 | html.contrast blockquote:before { 18 | color:#262626 19 | } 20 | html.contrast a { 21 | color:#03f 22 | } 23 | html.contrast a:visited { 24 | color:#7d013e 25 | } 26 | html.contrast span.wr { 27 | color:#800 28 | } 29 | html.contrast span.mfw { 30 | color:#4d0000 31 | } 32 | @media screen and (prefers-color-scheme:light) { 33 | html.inverted { 34 | background-color:#000 35 | } 36 | html.inverted body { 37 | color:#d9d9d9 38 | } 39 | html.inverted #contrast, 40 | html.inverted #invmode { 41 | color:#fff; 42 | background-color:#000 43 | } 44 | html.inverted blockquote { 45 | color:#d3c9be 46 | } 47 | html.inverted blockquote:before { 48 | color:#b8b8b8 49 | } 50 | html.inverted a { 51 | color:#00a2e7 52 | } 53 | html.inverted a:visited { 54 | color:#ca1a70 55 | } 56 | html.inverted span.wr { 57 | color:#d24637 58 | } 59 | html.inverted span.mfw { 60 | color:#b00000 61 | } 62 | html.inverted.contrast { 63 | background-color:#000 64 | } 65 | html.inverted.contrast body { 66 | color:#fff 67 | } 68 | html.inverted.contrast #contrast, 69 | html.inverted.contrast #invmode { 70 | color:#fff; 71 | background-color:#000 72 | } 73 | html.inverted.contrast blockquote { 74 | color:#f8f6f5 75 | } 76 | html.inverted.contrast blockquote:before { 77 | color:#e5e5e5 78 | } 79 | html.inverted.contrast a { 80 | color:#44c7ff 81 | } 82 | html.inverted.contrast a:visited { 83 | color:#e9579e 84 | } 85 | html.inverted.contrast span.wr { 86 | color:#db695d 87 | } 88 | html.inverted.contrast span.mfw { 89 | color:#ff0d0d 90 | } 91 | } 92 | @media (prefers-color-scheme:dark) { 93 | html:not(.inverted) { 94 | background-color:#000 95 | } 96 | html:not(.inverted) body { 97 | color:#d9d9d9 98 | } 99 | html:not(.inverted) #contrast, 100 | html:not(.inverted) #invmode { 101 | color:#fff; 102 | background-color:#000 103 | } 104 | html:not(.inverted) blockquote { 105 | color:#d3c9be 106 | } 107 | html:not(.inverted) blockquote:before { 108 | color:#b8b8b8 109 | } 110 | html:not(.inverted) a { 111 | color:#00a2e7 112 | } 113 | html:not(.inverted) a:visited { 114 | color:#ca1a70 115 | } 116 | html:not(.inverted) span.wr { 117 | color:#d24637 118 | } 119 | html:not(.inverted) span.mfw { 120 | color:#b00000 121 | } 122 | html:not(.inverted).contrast { 123 | background-color:#000 124 | } 125 | html:not(.inverted).contrast body { 126 | color:#fff 127 | } 128 | html:not(.inverted).contrast #contrast, 129 | html:not(.inverted).contrast #invmode { 130 | color:#fff; 131 | background-color:#000 132 | } 133 | html:not(.inverted).contrast blockquote { 134 | color:#f8f6f5 135 | } 136 | html:not(.inverted).contrast blockquote:before { 137 | color:#e5e5e5 138 | } 139 | html:not(.inverted).contrast a { 140 | color:#44c7ff 141 | } 142 | html:not(.inverted).contrast a:visited { 143 | color:#e9579e 144 | } 145 | html:not(.inverted).contrast span.wr { 146 | color:#db695d 147 | } 148 | html:not(.inverted).contrast span.mfw { 149 | color:#ff0d0d 150 | } 151 | html.inverted html { 152 | background-color:#fefefe 153 | } 154 | } 155 | a { 156 | color:#07a 157 | } 158 | a:visited { 159 | color:#941352 160 | } 161 | .noselect { 162 | -webkit-touch-callout:none; 163 | -webkit-user-select:none; 164 | -khtml-user-select:none; 165 | -moz-user-select:none; 166 | -ms-user-select:none; 167 | user-select:none 168 | } 169 | span.citneed { 170 | vertical-align:top; 171 | font-size:.7em; 172 | padding-left:.3em 173 | } 174 | small { 175 | font-size:.4em 176 | } 177 | p.st { 178 | margin-top:-1em 179 | } 180 | div.fancyPositioning div.picture-left { 181 | float:left; 182 | width:40%; 183 | overflow:hidden; 184 | margin-right:1em 185 | } 186 | div.fancyPositioning div.picture-left img { 187 | width:100% 188 | } 189 | div.fancyPositioning div.picture-left figure { 190 | margin:10px 191 | } 192 | div.fancyPositioning div.picture-left figure figcaption { 193 | font-size:.7em 194 | } 195 | div.fancyPositioning div.tleft { 196 | float:left; 197 | width:55% 198 | } 199 | div.fancyPositioning div.tleft p:first-child { 200 | margin-top:0 201 | } 202 | div.fancyPositioning:after { 203 | display:block; 204 | content:""; 205 | clear:both 206 | } 207 | ul li img { 208 | height:1em 209 | } 210 | blockquote { 211 | color:#456; 212 | margin-left:0; 213 | margin-top:2em; 214 | margin-bottom:2em 215 | } 216 | blockquote span { 217 | float:left; 218 | margin-left:1rem; 219 | padding-top:1rem 220 | } 221 | blockquote author { 222 | display:block; 223 | clear:both; 224 | font-size:.6em; 225 | margin-left:2.4rem; 226 | font-style:oblique 227 | } 228 | blockquote author:before { 229 | content:"- "; 230 | margin-right:1em 231 | } 232 | blockquote:before { 233 | font-family:Times New Roman,Times,Arial; 234 | color:#666; 235 | content:open-quote; 236 | font-size:2.2em; 237 | font-weight:600; 238 | float:left; 239 | margin-top:0; 240 | margin-right:.2rem; 241 | width:1.2rem 242 | } 243 | blockquote:after { 244 | content:""; 245 | display:block; 246 | clear:both 247 | } 248 | @media screen and (max-width:500px) { 249 | body { 250 | text-align:left 251 | } 252 | div.fancyPositioning div.picture-left, 253 | div.fancyPositioning div.tleft { 254 | float:none; 255 | width:inherit 256 | } 257 | blockquote span { 258 | width:80% 259 | } 260 | blockquote author { 261 | padding-top:1em; 262 | width:80%; 263 | margin-left:15% 264 | } 265 | blockquote author:before { 266 | content:""; 267 | margin-right:inherit 268 | } 269 | } 270 | span.visited { 271 | color:#941352 272 | } 273 | span.visited-maroon { 274 | color:#85144b 275 | } 276 | span.wr { 277 | color:#c0392b; 278 | font-weight:600 279 | } 280 | button.cont-inv, 281 | span.wr { 282 | text-decoration:underline 283 | } 284 | button.cont-inv { 285 | cursor:pointer; 286 | border-radius:2px; 287 | position:fixed; 288 | right:10px; 289 | font-size:.8em; 290 | border:0; 291 | padding:2px 5px 292 | } 293 | #contrast { 294 | color:#000; 295 | top:10px 296 | } 297 | #contrast, 298 | #invmode { 299 | -webkit-touch-callout:none; 300 | -webkit-user-select:none; 301 | -khtml-user-select:none; 302 | -moz-user-select:none; 303 | -ms-user-select:none; 304 | user-select:none 305 | } 306 | #invmode { 307 | color:#fff; 308 | background-color:#000; 309 | position:fixed; 310 | top:34px; 311 | text-decoration:underline 312 | } 313 | @media screen and (max-width:1080px) { 314 | #contrast, 315 | #invmode { 316 | position:absolute 317 | } 318 | } 319 | span.sb { 320 | color:#00e 321 | } 322 | span.sb, 323 | span.sv { 324 | cursor:not-allowed 325 | } 326 | span.sv { 327 | color:#551a8b 328 | } 329 | span.foufoufou { 330 | color:#444; 331 | font-weight:700 332 | } 333 | span.foufoufou:before { 334 | content:""; 335 | display:inline-block; 336 | width:1em; 337 | height:1em; 338 | margin-left:.2em; 339 | margin-right:.2em; 340 | background-color:#444 341 | } 342 | span.foufivfoufivfoufiv { 343 | color:#454545; 344 | font-weight:700 345 | } 346 | span.foufivfoufivfoufiv:before { 347 | content:""; 348 | display:inline-block; 349 | width:1em; 350 | height:1em; 351 | margin-left:.2em; 352 | margin-right:.2em; 353 | background-color:#454545 354 | } 355 | span.mfw { 356 | color:#730000 357 | } 358 | a.kopimi, 359 | a.kopimi img.kopimi { 360 | display:block; 361 | margin-left:auto; 362 | margin-right:auto 363 | } 364 | a.kopimi img.kopimi { 365 | height:2em 366 | } 367 | p.fakepre { 368 | font-family:monospace; 369 | font-size:.9em 370 | } -------------------------------------------------------------------------------- /lib/incendium/assets/extra.js: -------------------------------------------------------------------------------- 1 | function incendiumSearch(id) { 2 | var term = document.getElementById("term-" + id).value; 3 | window['incendiumFlameGraph_' + id].search(term); 4 | } 5 | 6 | function incendiumClear(id) { 7 | document.getElementById('term-' + id).value = ''; 8 | window['incendiumFlameGraph_' + id].clear(); 9 | } 10 | 11 | function incendiumResetZoom(id) { 12 | window['incendiumFlameGraph_' + id].resetZoom(); 13 | } 14 | 15 | // Saves the javascript as a file 16 | function incendiumDownload(id) { 17 | var text = window['incendiumFlameGraphScript_' + id].innerHTML 18 | var name = "incendium_flamegraph_" + id + ".js"; 19 | var type = "text/javascript"; 20 | var a = document.getElementById("downloader-" + id); 21 | var file = new Blob([text], { type: type }); 22 | a.href = URL.createObjectURL(file); 23 | a.download = name; 24 | } 25 | 26 | // This function creates an HTML and places it just before 27 | // the script tag that has called the function. 28 | // This is a bit dirty, but it's probably the easiest way of 29 | // having embedded flamegraphs which play well with both HTML 30 | // and markdown. 31 | // Having embedded flamegraphs that play well with markdown 32 | // is important because it makes it possible to embed them 33 | // in places like ExDoc pages 34 | function incendiumFlameGraph(id, data) { 35 | // Convenience function to make it easier to generate HTML elements from javascript 36 | function h(type, attributes, children) { 37 | var el = document.createElement(type); 38 | 39 | for (key in attributes) { 40 | el.setAttribute(key, attributes[key]) 41 | } 42 | 43 | children.forEach(child => { 44 | if (typeof child === 'string') { 45 | el.appendChild(document.createTextNode(child)) 46 | } else { 47 | el.appendChild(child) 48 | } 49 | }) 50 | 51 | return el 52 | } 53 | 54 | var clearAction = "javascript: incendiumClear(\"" + id + "\");"; 55 | var searchAction = "javascript: incendiumSearch(\"" + id + "\");"; 56 | var resetZoomAction = "javascript: incendiumResetZoom(\"" + id + "\");"; 57 | var downloadAction = "javascript: incendiumDownload(\"" + id + "\");"; 58 | 59 | var formId = "form-" + id; 60 | var termId = "term-" + id; 61 | var chartId = "chart-" + id; 62 | var detailsId = "details-" + id; 63 | var downloaderId = "downloader-" + id; 64 | 65 | var newElement = 66 | h("div", { class: "incendium" }, [ 67 | h("div", { id: chartId, style: "" }, []), 68 | h("div", { id: detailsId, style: "height: 2.15em;" }, []), 69 | h("nav", {}, [ 70 | h("div", {}, [ 71 | // h("a", { class: "incendium", href: downloadAction, id: downloaderId, style: styleSeparate }, ["Download"]), 72 | h("input", { class: "incendium", id: termId }, []), 73 | h("a", { class: "incendium", href: searchAction, style: "margin-left: 0.75em;" }, ["Search"]), 74 | h("a", { class: "incendium", href: clearAction, style: "margin-left: 2em;" }, ["Clear search"]), 75 | h("a", { class: "incendium", href: resetZoomAction, style: "margin-left: 1.25em;" }, ["Reset zoom"]) 76 | ]) 77 | ]), 78 | ]); 79 | 80 | // Insert the HTML just before the current script 81 | var script = document.currentScript; 82 | script.parentNode.insertBefore(newElement, script); 83 | 84 | document.getElementById(termId).addEventListener("keyup", function (event) { 85 | if (event.key === "Enter") { 86 | incendiumSearch(id); 87 | } 88 | }) 89 | 90 | var flameGraphWidthMultiplier = script.dataset.flameGraphWidthMultiplier || 1.0; 91 | var flameGraphWidth = (script.parentElement.clientWidth || 960) * flameGraphWidthMultiplier; 92 | 93 | var flameGraph = d3.flamegraph() 94 | .cellHeight(18) 95 | .transitionDuration(500) 96 | .width(flameGraphWidth) 97 | .minFrameSize(0) 98 | .transitionEase(d3.easeCubic) 99 | .sort(true) 100 | .title("") 101 | .differential(false) 102 | .selfValue(false) 103 | .tooltip(false) 104 | 105 | var details = document.getElementById(detailsId); 106 | flameGraph.setDetailsElement(details); 107 | 108 | window["incendiumFlameGraph_" + id] = flameGraph; 109 | window["incendiumFlameGraphScript_" + id] = document.currentScript; 110 | 111 | d3.select("#" + chartId) 112 | .datum(data) 113 | .call(flameGraph); 114 | } -------------------------------------------------------------------------------- /lib/incendium/assets/vendor/bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.4.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Y.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Se,popperConfig:null},Fe="show",Ue="out",We={HIDE:"hide"+Oe,HIDDEN:"hidden"+Oe,SHOW:"show"+Oe,SHOWN:"shown"+Oe,INSERTED:"inserted"+Oe,CLICK:"click"+Oe,FOCUSIN:"focusin"+Oe,FOCUSOUT:"focusout"+Oe,MOUSEENTER:"mouseenter"+Oe,MOUSELEAVE:"mouseleave"+Oe},qe="fade",Me="show",Ke=".tooltip-inner",Qe=".arrow",Be="hover",Ve="focus",Ye="click",ze="manual",Xe=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Me))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(qe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,this._getPopperConfig(a)),g(o).addClass(Me),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===Ue&&e._leave(null,e)};if(g(this.tip).hasClass(qe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){function e(){n._hoverState!==Fe&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),g(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),t&&t()}var n=this,i=this.getTipElement(),o=g.Event(this.constructor.Event.HIDE);if(g(this.element).trigger(o),!o.isDefaultPrevented()){if(g(i).removeClass(Me),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ye]=!1,this._activeTrigger[Ve]=!1,this._activeTrigger[Be]=!1,g(this.tip).hasClass(qe)){var r=_.getTransitionDurationFromElement(i);g(i).one(_.TRANSITION_END,e).emulateTransitionEnd(r)}else e();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Pe+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ke)),this.getTitle()),g(t).removeClass(qe+" "+Me)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=we(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t=t||("function"==typeof this.config.title?this.config.title.call(this.element):this.config.title)},t._getPopperConfig=function(t){var e=this;return l({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:Qe},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},{},this.config.popperConfig)},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,{},e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Re[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==ze){var e=t===Be?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Be?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),this._hideModalHandler=function(){i.element&&i.hide()},g(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");!this.element.getAttribute("title")&&"string"==t||(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Ve:Be]=!0),g(e.getTipElement()).hasClass(Me)||e._hoverState===Fe?e._hoverState=Fe:(clearTimeout(e._timeout),e._hoverState=Fe,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Fe&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Ve:Be]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=Ue,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===Ue&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==je.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,{},e,{},"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(Ae,t,this.constructor.DefaultType),t.sanitize&&(t.template=we(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Le);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(qe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ne),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ne,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.4.1"}},{key:"Default",get:function(){return xe}},{key:"NAME",get:function(){return Ae}},{key:"DATA_KEY",get:function(){return Ne}},{key:"Event",get:function(){return We}},{key:"EVENT_KEY",get:function(){return Oe}},{key:"DefaultType",get:function(){return He}}]),i}();g.fn[Ae]=Xe._jQueryInterface,g.fn[Ae].Constructor=Xe,g.fn[Ae].noConflict=function(){return g.fn[Ae]=ke,Xe._jQueryInterface};var $e="popover",Ge="bs.popover",Je="."+Ge,Ze=g.fn[$e],tn="bs-popover",en=new RegExp("(^|\\s)"+tn+"\\S+","g"),nn=l({},Xe.Default,{placement:"right",trigger:"click",content:"",template:''}),on=l({},Xe.DefaultType,{content:"(string|element|function)"}),rn="fade",sn="show",an=".popover-header",ln=".popover-body",cn={HIDE:"hide"+Je,HIDDEN:"hidden"+Je,SHOW:"show"+Je,SHOWN:"shown"+Je,INSERTED:"inserted"+Je,CLICK:"click"+Je,FOCUSIN:"focusin"+Je,FOCUSOUT:"focusout"+Je,MOUSEENTER:"mouseenter"+Je,MOUSELEAVE:"mouseleave"+Je},hn=function(t){function i(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}(i,t);var e=i.prototype;return e.isWithContent=function(){return this.getTitle()||this._getContent()},e.addAttachmentClass=function(t){g(this.getTipElement()).addClass(tn+"-"+t)},e.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},e.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(an),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ln),e),t.removeClass(rn+" "+sn)},e._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},e._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(en);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t 51 | # Return a function that doesn't take an input 52 | fn -> 53 | # We need a new filename for each function evocation 54 | filename = BencheeServer.get_new_filename_for_scenario(suite_name, scenario_name) 55 | :eflame.apply(:normal_with_children, filename, fun, []) 56 | end 57 | 58 | # The function we're about to profile expects an input. 59 | # The function wrapped in the profiler must receive an input too. 60 | {:arity, 1} -> 61 | fn input -> 62 | # We need a new filename for each function evocation 63 | filename = BencheeServer.get_new_filename_for_scenario(suite_name, scenario_name) 64 | :eflame.apply(:normal_with_children, filename, fn -> fun.(input) end, []) 65 | end 66 | 67 | {:arity, arity} -> raise "Invalid function arity #{arity}; function arity must be 0 or 1" 68 | end 69 | end 70 | 71 | defp prepare_benchmarks_for_profiling(suite_name, benchmarks) do 72 | for {scenario_name, args} <- benchmarks, into: %{} do 73 | # Args can either contain just a function or a pair 74 | # containing both a function and a hook 75 | case args do 76 | {fun, hooks} when is_function(fun) and is_list(hooks) -> 77 | new_fun = profiled_function(suite_name, scenario_name, fun) 78 | {scenario_name, {new_fun, hooks}} 79 | 80 | fun when is_function(fun) -> 81 | new_fun = profiled_function(suite_name, scenario_name, fun) 82 | {scenario_name, new_fun} 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/incendium/benchee_formatter_common.ex: -------------------------------------------------------------------------------- 1 | defmodule Incendium.BencheeFormatterCommon do 2 | alias Incendium.BencheeServer 3 | alias Incendium.Flamegraph 4 | alias Incendium.BencheeFormatterData 5 | 6 | @moduledoc false 7 | 8 | def format(incendium_suite, original_suite, _options) do 9 | suite_name = incendium_suite.configuration.title 10 | # To draw the flamegraphs we only care about the incendium_suite 11 | scenarios = incendium_suite.scenarios 12 | groups = BencheeServer.get_filenames_by_scenario(suite_name) 13 | data_size = get_humanized_data_size(groups) 14 | 15 | medians = Enum.map(scenarios, fn scenario -> scenario.run_time_data.statistics.median end) 16 | max_median = Enum.max(medians) 17 | flamegraph_multipliers = Enum.map(medians, fn median -> median / max_median end) 18 | 19 | scenarios_data = 20 | for {scenario, multiplier} <- List.zip([scenarios, flamegraph_multipliers]) do 21 | filenames = Map.fetch!(groups, scenario.name) 22 | hierarchy = Flamegraph.files_to_hierarchy(filenames) 23 | json_hieararchy = Jason.encode!(hierarchy) 24 | 25 | data = %{ 26 | id: unique_id(), 27 | name: scenario.name, 28 | hierarchy: json_hieararchy, 29 | flamegraph_width_multiplier: multiplier, 30 | slug: Slug.slugify(scenario.name) 31 | } 32 | 33 | data 34 | end 35 | 36 | # We've already extracted data from the files 37 | # We can delete them now 38 | delete_tmp_files(groups) 39 | 40 | %BencheeFormatterData{ 41 | incendium_suite: incendium_suite, 42 | incendium_scenarios_data: scenarios_data, 43 | original_suite: original_suite, 44 | disk_space_usage: data_size 45 | } 46 | end 47 | 48 | defp delete_tmp_files(groups) do 49 | groups 50 | # lists of files 51 | |> Map.values() 52 | # Concat them all into a big list of files 53 | |> Enum.concat() 54 | # Delete them all 55 | |> Enum.map(&File.rm!/1) 56 | end 57 | 58 | defp get_humanized_data_size(groups) do 59 | data_size = get_data_size(groups) 60 | 61 | data_size 62 | |> Enum.map(fn {k, v} -> {k, Size.humanize!(v)} end) 63 | |> Enum.into(%{}) 64 | end 65 | 66 | defp get_data_size(groups) do 67 | groups 68 | |> Enum.map(&data_size_in_bytes_for_group/1) 69 | |> Enum.into(%{}) 70 | end 71 | 72 | defp data_size_in_bytes_for_group({scenario, filenames} = _group) do 73 | size = 74 | filenames 75 | |> Enum.map(&File.stat!/1) 76 | |> Enum.map(fn stat -> stat.size end) 77 | |> Enum.sum() 78 | 79 | {scenario, size} 80 | end 81 | 82 | defp unique_id() do 83 | suffix = Enum.map(1..32, fn _ -> random_char() end) 84 | to_string(["incendium_", suffix]) 85 | end 86 | 87 | defp random_char() do 88 | [?a..?z, ?A..?Z] |> Enum.random() |> Enum.random() 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/incendium/benchee_formatter_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Incendium.BencheeFormatterData do 2 | @moduledoc false 3 | 4 | defstruct incendium_suite: nil, 5 | incendium_scenarios_data: [], 6 | original_suite: nil, 7 | disk_space_usage: %{} 8 | end 9 | -------------------------------------------------------------------------------- /lib/incendium/benchee_html_formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule Incendium.BencheeHtmlFormatter do 2 | alias Incendium.BencheeFormatterData 3 | alias Incendium.BencheeFormatterCommon 4 | # this module will be used in the templates to embed the static assets 5 | alias Incendium.Assets 6 | 7 | alias Benchee.Conversion 8 | alias Benchee.Conversion.{DeviationPercent, Format, Scale} 9 | 10 | require EEx 11 | 12 | @moduledoc false 13 | 14 | @behaviour Benchee.Formatter 15 | 16 | function_from_eex_file = 17 | fn kind, name, path -> 18 | @external_resource path 19 | 20 | EEx.function_from_file( 21 | kind, 22 | name, 23 | path, 24 | [:assigns], 25 | # Use the Phoenix engine to handle escaping automatically 26 | engine: Phoenix.HTML.Engine 27 | ) 28 | end 29 | 30 | @impl true 31 | def format(incendium_suite, options) do 32 | original_suite = Map.fetch!(options, :original_suite) 33 | BencheeFormatterCommon.format(incendium_suite, original_suite, options) 34 | end 35 | 36 | @impl true 37 | def write(formatter_data = %BencheeFormatterData{}, options) do 38 | %{incendium_suite: incendium_suite, 39 | incendium_scenarios_data: incendium_scenarios_data, 40 | original_suite: original_suite, 41 | disk_space_usage: disk_space_usage 42 | } = formatter_data 43 | 44 | incendium_config = incendium_suite.configuration 45 | incendium_scaling_strategy = incendium_config.unit_scaling 46 | incendium_units = Conversion.units(incendium_suite.scenarios, incendium_scaling_strategy) 47 | 48 | original_config = original_suite.configuration 49 | original_scaling_strategy = original_config.unit_scaling 50 | original_units = Conversion.units(original_suite.scenarios, original_scaling_strategy) 51 | 52 | default_file = Path.join("incendium", incendium_suite.configuration.title <> ".html") 53 | file = Map.get(options, :incendium_file, default_file) 54 | _embed = Map.get(options, :embed, true) 55 | flamegraph_widths_to_scale = Map.get(options, :incendium_flamegraph_widths_to_scale, true) 56 | 57 | html = 58 | suite_html( 59 | flamegraph_widths_to_scale: flamegraph_widths_to_scale, 60 | original_suite: original_suite, 61 | original_units: original_units, 62 | incendium_suite: incendium_suite, 63 | incendium_scenarios_data: incendium_scenarios_data, 64 | incendium_units: incendium_units, 65 | disk_space_usage: disk_space_usage, 66 | # Always embed the JS and CSS to make it easier to share benchmarks 67 | embed: true 68 | ) 69 | 70 | # Create the directory if it doesn't exist already 71 | file |> Path.dirname() |> File.mkdir_p!() 72 | # Save the HTMl as a file 73 | File.write!(file, html) 74 | end 75 | 76 | # Helpers 77 | 78 | flamegraph_template_path = "lib/incendium/templates/partials/flamegraph.html.eex" 79 | data_table_template_path = "lib/incendium/templates/partials/data_table.html.eex" 80 | disk_space_usage_template_path = "lib/incendium/templates/partials/disk_space_usage.html.eex" 81 | system_info_template_path = "lib/incendium/templates/partials/system_info.html.eex" 82 | flamegraph_js_template = "lib/incendium/templates/flamegraph.js.eex" 83 | suite_template = "lib/incendium/templates/suite.html.eex" 84 | 85 | @external_resource flamegraph_template_path 86 | @external_resource data_table_template_path 87 | @external_resource disk_space_usage_template_path 88 | @external_resource system_info_template_path 89 | @external_resource flamegraph_js_template 90 | @external_resource suite_template 91 | 92 | 93 | function_from_eex_file.( 94 | :defp, 95 | :flamegraph_html, 96 | flamegraph_template_path 97 | ) 98 | 99 | function_from_eex_file.( 100 | :defp, 101 | :data_table_html, 102 | data_table_template_path 103 | ) 104 | 105 | function_from_eex_file.( 106 | :defp, 107 | :disk_space_usage_html, 108 | disk_space_usage_template_path 109 | ) 110 | 111 | function_from_eex_file.( 112 | :defp, 113 | :system_info_html, 114 | system_info_template_path 115 | ) 116 | 117 | function_from_eex_file.( 118 | :defp, 119 | :flamegraph_js, 120 | flamegraph_js_template 121 | ) 122 | 123 | function_from_eex_file.( 124 | :defp, 125 | :suite_html_raw, 126 | suite_template 127 | ) 128 | 129 | @doc false 130 | def suite_html(assigns) do 131 | suite_html_raw(assigns) |> Phoenix.HTML.safe_to_string() 132 | end 133 | 134 | # Stuff literally stolen from Benchee 135 | 136 | defp format_mode(nil, _unit) do 137 | "none" 138 | end 139 | 140 | defp format_mode(modes = [_ | _], unit) do 141 | modes 142 | |> Enum.map(fn mode -> format_property(mode, unit) end) 143 | |> Enum.join(", ") 144 | end 145 | 146 | defp format_mode(value, unit) do 147 | format_property(value, unit) 148 | end 149 | 150 | defp format_property(value, unit) do 151 | Format.format({Scale.scale(value, unit), unit}) 152 | end 153 | 154 | defp format_percent(deviation_percent) do 155 | DeviationPercent.format(deviation_percent) 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/incendium/benchee_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Incendium.BencheeServer do 2 | use Agent 3 | 4 | @moduledoc false 5 | 6 | # Public API 7 | 8 | def start_link(suite_name) do 9 | # The state is basically a map from filenames to scenarios, so that we 10 | # can aggregate the stack frames from multiple executions. 11 | initial_state = %{ 12 | suite: suite_name, 13 | filenames: %{} 14 | } 15 | 16 | # We've setup a registry when we started out Incendium application 17 | name = registered_name_for(suite_name) 18 | 19 | Agent.start_link(fn -> initial_state end, name: name) 20 | end 21 | 22 | def get_new_filename_for_scenario(suite_name, scenario_name) do 23 | filename = get_tmp_file() 24 | 25 | name = registered_name_for(suite_name) 26 | # Update the map to tell the agent which scenario the filename corresponds to. 27 | Agent.update(name, fn state -> 28 | update_filenames(state, filename, scenario_name) 29 | end) 30 | 31 | # Return the filename to be used later 32 | filename 33 | end 34 | 35 | def get_filenames_by_scenario(suite_name) do 36 | state = get_state_from_suite_name(suite_name) 37 | group_filenames_by_scenario(state.filenames) 38 | end 39 | 40 | # Helpers: 41 | 42 | defp get_state_from_suite_name(suite_name) do 43 | name = registered_name_for(suite_name) 44 | Agent.get(name, & &1) 45 | end 46 | 47 | defp registered_name_for(suite_name) do 48 | {:via, Registry, {Incendium.BencheeServer.Registry, suite_name}} 49 | end 50 | 51 | defp random_char() do 52 | [?a..?z, ?A..?Z] |> Enum.random() |> Enum.random() 53 | end 54 | 55 | defp random_filename() do 56 | suffix = Enum.map(1..32, fn _ -> random_char() end) 57 | to_string(["incendium_call_stack_", suffix]) 58 | end 59 | 60 | defp get_tmp_file() do 61 | dir = System.tmp_dir!() 62 | filename = random_filename() 63 | Path.join(dir, filename) 64 | end 65 | 66 | def update_filenames(state, filename, scenario_name) do 67 | %{state | filenames: Map.put(state.filenames, filename, scenario_name)} 68 | end 69 | 70 | defp group_filenames_by_scenario(filenames) do 71 | get_scenario = fn {_filename, scenario_name} -> scenario_name end 72 | get_filename = fn {filename, _scenario_name} -> filename end 73 | 74 | # Walk the filename/scenario pairs, grouping them according to the scenario. 75 | # Keep the filename as a value (the scenario will be the key) 76 | Enum.group_by(filenames, get_scenario, get_filename) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/incendium/controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Incendium.Controller do 2 | alias Incendium.{ 3 | Flamegraph, 4 | Assets, 5 | Storage 6 | } 7 | 8 | @doc """ 9 | Turn the current module into an Icendium controller. 10 | 11 | Options: 12 | 13 | * `routes_module` (required) - The module that contains the routes 14 | in your application (it should be something like `YourApp.Router.Helpers`) 15 | 16 | * `:otp_app` (required) - The main OTP application 17 | 18 | ## Examples 19 | 20 | defmodule MyApp.IncendiumController do 21 | use Incendium.Controller, 22 | routes_module: MyApp.Router.Helpers, 23 | otp_app: :my_app 24 | end 25 | """ 26 | defmacro __using__(opts) do 27 | routes_module = Keyword.fetch!(opts, :routes_module) 28 | otp_app = Keyword.fetch!(opts, :otp_app) 29 | 30 | app_priv = :code.priv_dir(otp_app) 31 | 32 | 33 | static_js_dest = Path.join([app_priv, "static", "assets", "incendium.js"]) 34 | static_css_dest = Path.join([app_priv, "static", "assets", "incendium.css"]) 35 | 36 | File.cp!(Assets.css_path(), static_css_dest) 37 | File.cp!(Assets.js_path(), static_js_dest) 38 | 39 | quote do 40 | use Phoenix.Controller 41 | 42 | # Recompile the controller if the Incendium assets have changed 43 | require Incendium.Assets, 44 | as: Incendium_Assets__JUST_A_WAY_OF_ENSURING_ASSETS_DONT_BECOME_STALE, 45 | warn: false 46 | 47 | # Recompile the controller if the files in the Phoenix app's priv dir 48 | # are changed (to ovewrite those same files) 49 | @external_resource "priv/static/assets/incendium.js" 50 | @external_resource "priv/static/assets/incendium.css" 51 | 52 | def latest_flamegraph(conn, params) do 53 | Incendium.Controller.latest_flamegraph(conn, params, unquote(routes_module)) 54 | end 55 | end 56 | end 57 | 58 | @doc false 59 | def latest_flamegraph(conn, _params, routes_module) do 60 | latest_hierarchy = 61 | Storage.latest_stacks_path() 62 | |> Flamegraph.file_to_hierarchy() 63 | |> Jason.encode!() 64 | 65 | id = random_id() 66 | 67 | body = 68 | Incendium.View.render("latest-flamegraph.html", 69 | conn: conn, 70 | id: id, 71 | routes_module: routes_module, 72 | hierarchy: latest_hierarchy 73 | ) 74 | |> Phoenix.HTML.Safe.to_iodata() 75 | 76 | Plug.Conn.send_resp(conn, 200, body) 77 | end 78 | 79 | defp random_id() do 80 | to_string(Enum.map(1..32, fn _ -> Enum.random(?a..?z) end)) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/incendium/decorator.ex: -------------------------------------------------------------------------------- 1 | defmodule Incendium.Decorator do 2 | @moduledoc """ 3 | Defines decorators to profile function calls. 4 | """ 5 | 6 | use Decorator.Define, [ 7 | incendium_profile_with_tracing: 0 8 | ] 9 | 10 | alias Incendium.Storage 11 | 12 | @doc """ 13 | *Decorator*. When the decorated function is invoked, 14 | it will bd profiled using tracing. 15 | 16 | Stack data will be written to #{Storage.latest_stacks_path()}. 17 | 18 | **Note**: profiling will be disabled if `Mix.env() == :prod`. 19 | This means it's safe(ish) to leave these decorators in a production 20 | environment. 21 | It's never a good idea to profile in prod using the `:eflame` 22 | library, as it makes your code run ~10x slower. 23 | 24 | 25 | ## Example 26 | 27 | defmodule MyApp.UserController do 28 | # ... 29 | use Incendium.Decorator 30 | 31 | @decorate incendium_profile_with_tracing() 32 | def index(conn, params) do 33 | # ... 34 | end 35 | end 36 | """ 37 | def incendium_profile_with_tracing(body, _context) do 38 | # If we're not in prod, it's safe to profile 39 | if Mix.env() in [:dev, :test] do 40 | Storage.latest_stacks_path() 41 | |> Path.dirname() 42 | |> File.mkdir_p!() 43 | 44 | quote do 45 | Incendium.profile_with_tracing(fn -> unquote(body) end) 46 | end 47 | # It's never a good idea to profile in prod 48 | # (your functions will run ~10x slower) 49 | else 50 | body 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/incendium/flamegraph.ex: -------------------------------------------------------------------------------- 1 | defmodule Incendium.Flamegraph do 2 | @moduledoc false 3 | 4 | def file_to_hierarchy(path) do 5 | path 6 | |> file_to_stacks() 7 | |> stacks_to_hierarchy() 8 | |> maybe_wrap_in_root() 9 | end 10 | 11 | def files_to_hierarchy(paths) do 12 | paths 13 | |> Enum.map(&file_to_stacks/1) 14 | |> Enum.concat() 15 | |> stacks_to_hierarchy() 16 | |> maybe_wrap_in_root() 17 | end 18 | 19 | def file_to_stacks(path) do 20 | # A stack file consists of a number of sampled stack frames. 21 | # Each stack frame is separated from the following one by a newline. 22 | # Function calls in the stack frame are separated by semicolons. 23 | stacks = 24 | File.read!(path) 25 | |> String.split("\n") 26 | |> Enum.reject(fn line -> line == "" end) 27 | |> Enum.map(fn line -> String.split(line, ";") end) 28 | 29 | Enum.map(stacks, &drop_pid_and_eflame_apply/1) 30 | end 31 | 32 | defp drop_pid_and_eflame_apply([_, "eflame:apply1/3" | rest]), do: rest 33 | defp drop_pid_and_eflame_apply(stack), do: stack 34 | 35 | def stacks_to_hierarchy(stacks) do 36 | # d3-flamegraph expects a pretty specific format for the data. 37 | level = 38 | Enum.reduce(stacks, %{}, fn 39 | [], acc -> 40 | acc 41 | 42 | 43 | [frame | stack], acc -> 44 | case acc do 45 | %{^frame => stacks_for_frame} -> 46 | %{acc | frame => [stack | stacks_for_frame]} 47 | 48 | _acc -> 49 | Map.put(acc, frame, [stack]) 50 | end 51 | end) 52 | 53 | handle_level(level) 54 | end 55 | 56 | defp handle_level(level) do 57 | for {frame, stacks} <- level do 58 | levels = stacks_to_hierarchy(stacks) 59 | %{name: frame, value: length(stacks), children: levels} 60 | end 61 | end 62 | 63 | def maybe_wrap_in_root(hierarchy) when is_map(hierarchy), do: hierarchy 64 | 65 | def maybe_wrap_in_root(hierarchy) when is_list(hierarchy) do 66 | value = 67 | hierarchy 68 | |> Enum.map(fn node -> node.value end) 69 | |> Enum.sum() 70 | 71 | %{name: "root", value: value, children: hierarchy} 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/incendium/storage.ex: -------------------------------------------------------------------------------- 1 | defmodule Incendium.Storage do 2 | @moduledoc false 3 | 4 | # TODO: maybe expand this into a real solution which stores 5 | # profile data in a persistent backend such as ETS tables. 6 | 7 | def latest_stacks_path() do 8 | Path.join("incendium", "stacks.out") 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/incendium/templates/flamegraph.js.eex: -------------------------------------------------------------------------------- 1 | var data_<%= @id %> = <%= {:safe, @hierarchy} %>; 2 | incendiumFlameGraph("<%= @id %>", data_<%= @id %>) -------------------------------------------------------------------------------- /lib/incendium/templates/partials/data_table.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% measurement_key = :run_time_data %> 5 | 6 | <%= if measurement_key == :run_time_data do %> 7 | 8 | <% end %> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <%= for {scenario, data} <- List.zip([@scenarios, @scenarios_data]) do %> 20 | <% statistics = Map.fetch!(scenario, measurement_key).statistics %> 21 | 22 | 23 | <%= if measurement_key == :run_time_data do %> 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | <% else %> 32 | 33 | 34 | 35 | 36 | 37 | 38 | <% end %> 39 | 40 | 41 | <% end %> 42 | 43 |
NameIterations per SecondAverageDeviationMedianModeMinimumMaximumSample size
<%= scenario.name %><%= format_property statistics.ips, @units.ips %><%= format_property statistics.average, @units.run_time %><%= format_percent statistics.std_dev_ratio %><%= format_property statistics.median, @units.run_time %><%= format_mode statistics.mode, @units.run_time %><%= format_property statistics.minimum, @units.run_time %><%= format_property statistics.maximum, @units.run_time %><%= format_property statistics.average, @units.memory %><%= format_percent statistics.std_dev_ratio %><%= format_property statistics.median, @units.memory %><%= format_mode statistics.mode, @units.memory %><%= format_property statistics.minimum, @units.memory %><%= format_property statistics.maximum, @units.memory %><%= statistics.sample_size %>
44 | -------------------------------------------------------------------------------- /lib/incendium/templates/partials/disk_space_usage.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= for scenario_data <- @scenarios_data do %> 10 | <% usage = Map.fetch!(@disk_space_usage, scenario_data.name) %> 11 | 12 | 15 | 16 | 17 | <% end %> 18 | 19 |
NameDisk space usage
13 | <%= scenario_data.name %> 14 | <%= usage %>
20 | -------------------------------------------------------------------------------- /lib/incendium/templates/partials/flamegraph.html.eex: -------------------------------------------------------------------------------- 1 | <%# Maybe scale the flamegraph widths %> 2 | <% multiplier = if @flamegraph_widths_to_scale, 3 | do: @scenario_data.flamegraph_width_multiplier, 4 | else: 1 %> 5 | <% statistics = Map.fetch!(@scenario, :run_time_data).statistics %> 6 | <% average = format_property(statistics.average, @units.run_time) %> 7 | <% std_dev = format_property(statistics.std_dev, @units.run_time) %> 8 | 9 |

10 | <%= @scenario_data.name %> 11 | <%= average %> (±<%= std_dev %>) 12 |

13 | 14 | 21 |
22 | -------------------------------------------------------------------------------- /lib/incendium/templates/partials/system_info.html.eex: -------------------------------------------------------------------------------- 1 |
    2 |
  • Elixir: <%= @system.elixir%>
  • 3 |
  • Erlang: <%= @system.erlang %>
  • 4 |
  • Operating system: <%= @system.os %>
  • 5 |
  • Available memory: <%= @system.available_memory %>
  • 6 |
  • CPU Information: <%= @system.cpu_speed %>
  • 7 |
  • Number of Available Cores: <%= @system.num_cores %>
  • 8 |
9 | -------------------------------------------------------------------------------- /lib/incendium/templates/suite.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <%= if @embed do %> 12 | 18 | 19 | 22 | 23 | <% else %> 24 | 25 | 26 | <% end %> 27 | 28 | <%= @incendium_suite.configuration.title %> 29 | 30 | 31 | 32 |
33 |
Benchee ❤ Incendium
34 | 38 |
39 | 40 |
41 |
42 |

<%= @incendium_suite.configuration.title %>

43 |

Benchmark suite run with Benchee. Flamegraphs drawn using Incendium

44 |
45 |
46 | 47 |
48 |

Statistics

49 | 50 |

Performance data for code run without the profiler.

51 | <%= data_table_html( 52 | scenarios: @original_suite.scenarios, 53 | units: @original_units, 54 | scenarios_data: @incendium_scenarios_data 55 | ) %> 56 | 57 |

Performance data for code run with the profiler:

58 | <%= data_table_html( 59 | scenarios: @incendium_suite.scenarios, 60 | units: @incendium_units, 61 | scenarios_data: @incendium_scenarios_data 62 | ) %> 63 | 64 |

Scenarios

65 | 66 |

67 | Incendium generates one flamegraph per benchmark scenario. 68 | Each flamegraph is generated from the stackframes collected wach time the scenario was run. 69 | <%= if @flamegraph_widths_to_scale do %> 70 | The width of each flamegraph is proportional to the median run time for the scenario 71 | (i.e. a shorter flamegraph corresponds to a faster scenario). 72 | <% end %> 73 |

74 | 75 | <%= for {scenario, scenario_data} <- List.zip([ 76 | @original_suite.scenarios, 77 | @incendium_scenarios_data 78 | ]) do %> 79 | <%= flamegraph_html( 80 | scenario_data: scenario_data, 81 | scenario: scenario, 82 | flamegraph_widths_to_scale: @flamegraph_widths_to_scale, 83 | units: @original_units 84 | ) %> 85 | <% end %> 86 | 87 |

System information

88 | 89 |

Information about the system where the benchmarks were run.

90 | 91 | <%= system_info_html(system: @original_suite.system) %> 92 | 93 |

Disk space usage

94 | 95 |

96 | Disk space used when profiling the functions. 97 | Disk space is used by Incendium (actually by eflame) 98 | to write the stack frames for the profiles functions. 99 |

100 | 101 | <%= disk_space_usage_html( 102 | scenarios_data: @incendium_scenarios_data, 103 | disk_space_usage: @disk_space_usage 104 | ) %> 105 | 106 | 107 |
108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /lib/incendium/templates/view/flamegraph.js.eex: -------------------------------------------------------------------------------- 1 | var data_<%= @id %> = <%= {:safe, @hierarchy} %>; 2 | incendiumFlameGraph("<%= @id %>", data_<%= @id %>) -------------------------------------------------------------------------------- /lib/incendium/templates/view/latest-flamegraph.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | "/> 11 | 12 | 15 | 16 | Incendium Flamegraph 17 | 18 | 19 | 20 |

Incendium (based on d3-flamegraph)

21 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/incendium/view.ex: -------------------------------------------------------------------------------- 1 | defmodule Incendium.View do 2 | @moduledoc false 3 | 4 | # Helpers to render pages in the IncendiumController. 5 | 6 | alias Incendium.Assets 7 | 8 | @external_resource "#{__DIR__}/templates/view/latest-flamegraph.html.eex" 9 | @external_resource "#{__DIR__}/templates/view/flamegraph.js.eex" 10 | 11 | use Phoenix.View, 12 | root: "#{__DIR__}/templates/view", 13 | path: "" 14 | 15 | def extra_css() do 16 | Assets.extra_css() 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/mix/incendium.assets.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Incendium.Assets do 2 | use Mix.Task 3 | 4 | @impl Mix.Task 5 | def run(args) do 6 | _dir = 7 | case args do 8 | [dir] -> dir 9 | _ -> raise "Task requires a directory name as a single argument" 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/mix/incendium.build_assets.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Incendium.BuildAssets do 2 | use Mix.Task 3 | alias Incendium.Assets 4 | 5 | @impl Mix.Task 6 | def run(args) do 7 | case "--offline" in args do 8 | true -> Assets.build_assets_offline() 9 | false -> Assets.build_assets() 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Incendium.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.3.1" 5 | @url "https://github.com/tmbb/incendium" 6 | 7 | def project do 8 | [ 9 | app: :incendium, 10 | name: "Incendium", 11 | description: "Easy flamegraphs for your application or benchmarks", 12 | version: @version, 13 | source_url: @url, 14 | homepage: @url, 15 | elixir: "~> 1.10", 16 | start_permanent: Mix.env() == :prod, 17 | deps: deps(), 18 | aliases: aliases(), 19 | package: [ 20 | licenses: ["MIT"], 21 | links: %{ 22 | "GitHub" => @url 23 | } 24 | ], 25 | docs: [ 26 | main: "Incendium", 27 | extras: [ 28 | # Don't add a README; add usage instructions to the main module instead 29 | "doc_extra/pages/Example flamegraph.md" 30 | ], 31 | assets: "doc_extra/assets/" 32 | ] 33 | ] 34 | end 35 | 36 | # Run "mix help compile.app" to learn about applications. 37 | def application do 38 | [ 39 | extra_applications: [:logger, :inets], 40 | mod: {Incendium.Application, []} 41 | ] 42 | end 43 | 44 | # Run "mix help deps" to learn about dependencies. 45 | defp deps do 46 | [ 47 | {:eflame, "~> 1.0"}, 48 | 49 | # Dependencies required to integrate with Phoenix applications: 50 | {:phoenix, "~> 1.6"}, 51 | {:decorator, "~> 1.4"}, 52 | 53 | # Dependencies required for benchmarks: 54 | {:size, "~> 0.1"}, 55 | {:benchee, "~> 1.0"}, 56 | # TODO: do we really need slugify? 57 | {:slugify, "~> 1.3"}, 58 | 59 | # Dependencies required for both Phoenix applications and benchmarks: 60 | # NOTE: `phoenix_html` is NOT phoenix-specific. 61 | # It merely provides utilities to work with HTML templates 62 | {:phoenix_html, "~> 3.0"}, 63 | {:jason, "~> 1.2"}, 64 | 65 | # Documentation 66 | {:ex_doc, "~> 0.23", only: :dev} 67 | ] 68 | end 69 | 70 | defp aliases() do 71 | [ 72 | publish: "run scripts/release.exs" 73 | ] 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, 3 | "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, 4 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"}, 6 | "eflame": {:hex, :eflame, "1.0.1", "0664d287e39eef3c413749254b3af5f4f8b00be71c1af67d325331c4890be0fc", [:mix], [], "hexpm", "e0b08854a66f9013129de0b008488f3411ae9b69b902187837f994d7a99cf04e"}, 7 | "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"}, 8 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 9 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 10 | "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"}, 11 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 12 | "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, 13 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 14 | "phoenix": {:hex, :phoenix, "1.6.9", "648e660040cdc758c5401972e0f592ce622d4ce9cd16d2d9c33dda32d0c9f7fa", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be2fe497597d6bf297dcbf9f4416b4929dbfbdcc25edc1acf6d4dcaecbe898a6"}, 15 | "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, 16 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, 17 | "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, 18 | "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.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.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, 19 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, 20 | "size": {:hex, :size, "0.1.1", "9864eac57370bfad1cc22891f6f9215955fac7e2644ffcedbb12930ae11f7d3b", [:mix], [], "hexpm", "1449f569b6dbd400b52ae50df7e0487133192b0fe6f4fd3617f27c1e4848fe1a"}, 21 | "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, 22 | "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, 23 | "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, 24 | } 25 | -------------------------------------------------------------------------------- /priv/assets/incendium.css: -------------------------------------------------------------------------------- 1 | .d3-flame-graph rect { 2 | stroke: #EEEEEE; 3 | fill-opacity: .8; 4 | } 5 | 6 | .d3-flame-graph rect:hover { 7 | stroke: #474747; 8 | stroke-width: 0.5; 9 | cursor: pointer; 10 | } 11 | 12 | .d3-flame-graph-label { 13 | pointer-events: none; 14 | white-space: nowrap; 15 | text-overflow: ellipsis; 16 | overflow: hidden; 17 | font-size: 12px; 18 | font-family: Verdana; 19 | margin-left: 4px; 20 | margin-right: 4px; 21 | line-height: 1.5; 22 | padding: 0 0 0; 23 | font-weight: 400; 24 | color: black; 25 | text-align: left; 26 | } 27 | 28 | .d3-flame-graph .fade { 29 | opacity: 0.6 !important; 30 | } 31 | 32 | .d3-flame-graph .title { 33 | font-size: 20px; 34 | font-family: Verdana; 35 | } 36 | 37 | .d3-flame-graph-tip { 38 | line-height: 1; 39 | font-family: Verdana; 40 | font-size: 12px; 41 | padding: 12px; 42 | background: rgba(0, 0, 0, 0.8); 43 | color: #fff; 44 | border-radius: 2px; 45 | pointer-events: none; 46 | } 47 | 48 | /* Creates a small triangle extender for the tooltip */ 49 | .d3-flame-graph-tip:after { 50 | box-sizing: border-box; 51 | display: inline; 52 | font-size: 10px; 53 | width: 100%; 54 | line-height: 1; 55 | color: rgba(0, 0, 0, 0.8); 56 | position: absolute; 57 | pointer-events: none; 58 | } 59 | 60 | /* Northward tooltips */ 61 | .d3-flame-graph-tip.n:after { 62 | content: "\25BC"; 63 | margin: -1px 0 0 0; 64 | top: 100%; 65 | left: 0; 66 | text-align: center; 67 | } 68 | 69 | /* Eastward tooltips */ 70 | .d3-flame-graph-tip.e:after { 71 | content: "\25C0"; 72 | margin: -4px 0 0 0; 73 | top: 50%; 74 | left: -8px; 75 | } 76 | 77 | /* Southward tooltips */ 78 | .d3-flame-graph-tip.s:after { 79 | content: "\25B2"; 80 | margin: 0 0 1px 0; 81 | top: -8px; 82 | left: 0; 83 | text-align: center; 84 | } 85 | 86 | /* Westward tooltips */ 87 | .d3-flame-graph-tip.w:after { 88 | content: "\25B6"; 89 | margin: -4px 0 0 -1px; 90 | top: 50%; 91 | left: 100%; 92 | } -------------------------------------------------------------------------------- /scripts/release.exs: -------------------------------------------------------------------------------- 1 | defmodule Releaser.VersionUtils do 2 | @doc """ 3 | Some utilities to get and set version numbers in the `mix.exs` file 4 | and to programatically transform version numbers. 5 | 6 | Maybe the `bump_*` functions should be in the standard library? 7 | 8 | This script doesn't support pre-release versions or versions with build information. 9 | """ 10 | @version_line_regex ~r/(\n\s*@version\s+")([^\n]+)("\n)/ 11 | 12 | def bump_major(%Version{} = version) do 13 | %{version | major: version.major + 1, minor: 0, patch: 0} 14 | end 15 | 16 | def bump_minor(%Version{} = version) do 17 | %{version | minor: version.minor + 1, patch: 0} 18 | end 19 | 20 | def bump_patch(%Version{} = version) do 21 | %{version | patch: version.patch + 1} 22 | end 23 | 24 | def version_to_string(%Version{} = version) do 25 | "#{version.major}.#{version.minor}.#{version.patch}" 26 | end 27 | 28 | def get_version() do 29 | config = File.read!("mix.exs") 30 | 31 | case Regex.run(@version_line_regex, config) do 32 | [_line, _pre, version, _post] -> 33 | Version.parse!(version) 34 | 35 | _ -> 36 | raise "Invalid project version in your mix.exs file" 37 | end 38 | end 39 | 40 | def set_version(version) do 41 | contents = File.read!("mix.exs") 42 | version_string = version_to_string(version) 43 | 44 | replaced = 45 | Regex.replace(@version_line_regex, contents, fn _, pre, _version, post -> 46 | "#{pre}#{version_string}#{post}" 47 | end) 48 | 49 | File.write!("mix.exs", replaced) 50 | end 51 | 52 | def update_version(%Version{} = version, "major"), do: bump_major(version) 53 | def update_version(%Version{} = version, "minor"), do: bump_minor(version) 54 | def update_version(%Version{} = version, "patch"), do: bump_patch(version) 55 | def update_version(%Version{} = _version, type), do: raise("Invalid version type: #{type}") 56 | end 57 | 58 | defmodule Releaser.Changelog do 59 | @doc """ 60 | Functions to append entries to the changelog. 61 | """ 62 | alias Releaser.VersionUtils 63 | 64 | @release_filename "RELEASE.md" 65 | @release_type_regex ~r/^(RELEASE_TYPE:\s+)(\w+)(.*)/s 66 | 67 | @changelog_filename "CHANGELOG.md" 68 | @changelog_entry_header_level 3 69 | @changelog_entries_marker "\n\n" 70 | 71 | def remove_release_file() do 72 | File.rm!(@release_filename) 73 | end 74 | 75 | def extract_release_type() do 76 | contents = File.read!(@release_filename) 77 | 78 | {type, text} = 79 | case Regex.run(@release_type_regex, contents) do 80 | [_line, _pre, type, text] -> 81 | {type, String.trim(text)} 82 | 83 | _ -> 84 | raise "Invalid project version in your mix.exs file" 85 | end 86 | 87 | {type, text} 88 | end 89 | 90 | def changelog_entry(%Version{} = version, %DateTime{} = date_time, text) do 91 | header_prefix = String.duplicate("#", @changelog_entry_header_level) 92 | version_string = VersionUtils.version_to_string(version) 93 | 94 | date_time_string = 95 | date_time 96 | |> DateTime.truncate(:second) 97 | |> NaiveDateTime.to_string() 98 | 99 | """ 100 | 101 | #{header_prefix} #{version_string} - #{date_time_string} 102 | 103 | #{text} 104 | 105 | """ 106 | end 107 | 108 | def add_changelog_entry(entry) do 109 | contents = File.read!(@changelog_filename) 110 | [first, last] = String.split(contents, @changelog_entries_marker) 111 | 112 | replaced = 113 | Enum.join([ 114 | first, 115 | @changelog_entries_marker, 116 | entry, 117 | last 118 | ]) 119 | 120 | File.write!(@changelog_filename, replaced) 121 | end 122 | end 123 | 124 | defmodule Releaser.Git do 125 | @doc """ 126 | This module contains some git-specific functionality 127 | """ 128 | alias Releaser.VersionUtils 129 | 130 | def add_commit_and_tag(version) do 131 | version_string = VersionUtils.version_to_string(version) 132 | Mix.Shell.IO.cmd("git add .", []) 133 | Mix.Shell.IO.cmd(~s'git commit -m "Bumped version number"') 134 | Mix.Shell.IO.cmd(~s'git tag -a v#{version_string} -m "Version #{version_string}"') 135 | end 136 | end 137 | 138 | defmodule Releaser.Tests do 139 | def run_tests!() do 140 | error_code = Mix.Shell.IO.cmd("mix test", []) 141 | 142 | if error_code != 0 do 143 | raise "This version can't be released because tests are failing." 144 | end 145 | 146 | :ok 147 | end 148 | end 149 | 150 | defmodule Releaser do 151 | alias Releaser.VersionUtils 152 | alias Releaser.Changelog 153 | alias Releaser.Git 154 | alias Releaser.Tests 155 | alias Releaser.Publish 156 | 157 | def run() do 158 | # Run the tests before generating the release. 159 | # If any test fails, stop. 160 | Tests.run_tests!() 161 | # Get the current version from the mix.exs file. 162 | version = VersionUtils.get_version() 163 | # Extract the changelog entry and add it to the changelog. 164 | # Use the information in the RELEASE.md file to bump the version number. 165 | {release_type, text} = Changelog.extract_release_type() 166 | new_version = VersionUtils.update_version(version, release_type) 167 | entry = Changelog.changelog_entry(new_version, DateTime.utc_now(), text) 168 | Changelog.add_changelog_entry(entry) 169 | # Set a new version on the mix.exs file 170 | VersionUtils.set_version(new_version) 171 | # Commit the changes and ad a new 'v*.*.*' tag 172 | Git.add_commit_and_tag(new_version) 173 | # Now that we have commited the changes, we can remove the release file 174 | Changelog.remove_release_file() 175 | end 176 | end 177 | 178 | # Generate a new release 179 | Releaser.run() 180 | -------------------------------------------------------------------------------- /test/incendium_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IncendiumTest do 2 | use ExUnit.Case 3 | doctest Incendium 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------