Skip to content

Commit ced76f9

Browse files
committed
Actually implement tprof
1 parent a88c9ba commit ced76f9

File tree

2 files changed

+198
-68
lines changed

2 files changed

+198
-68
lines changed

lib/mix/lib/mix/tasks/profile.tprof.ex

Lines changed: 129 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ defmodule Mix.Tasks.Profile.Tprof do
66
@moduledoc """
77
Profiles the given file or expression using Erlang's `tprof` tool.
88
9-
[`:tprof`](`:tprof`) provides time information of each function call and can be useful
10-
when you want to discover the bottlenecks related to this.
9+
Requires Erlang/OTP27 or above.
10+
11+
[`:tprof`](`:tprof`) can provide time or memory information of each function call
12+
and can be useful when you want to discover the bottlenecks related to these.
1113
1214
Before running the code, it invokes the `app.start` task which compiles
1315
and loads your project. After that, the target expression is profiled together
@@ -21,15 +23,26 @@ defmodule Mix.Tasks.Profile.Tprof do
2123
$ mix profile.tprof -e "[1, 2, 3] |> Enum.reverse |> Enum.map(&Integer.to_string/1)"
2224
$ mix profile.tprof my_script.exs arg1 arg2 arg3
2325
26+
By default, tprof uses the `time` type, but you can profile memory too:
27+
28+
$ mix profile.tprof -e "Enum.map([1, 2, 3], &Integer.to_string/1)" --type memory
29+
30+
Call count is present with both type `time` and `memory`, but if you only need
31+
the call count information, you can use the type `calls`:
32+
33+
$ mix profile.tprof -e "Enum.map([1, 2, 3], &Integer.to_string/1)" --type calls
34+
2435
This task is automatically re-enabled, so you can profile multiple times
2536
in the same Mix invocation.
2637
2738
## Command line options
2839
2940
* `--matching` - only profile calls matching the given `Module.function/arity` pattern
41+
* `--type` - the type of profiling, `calls`, `time` or `memory` (default: `time`)
3042
* `--calls` - filters out any results with a call count lower than this
31-
* `--time` - filters out any results that took lower than specified (in µs)
32-
* `--sort` - sorts the results by `time` or `calls` (default: `time`)
43+
* `--time` - filters out any results that took lower than specified (in µs), the `type` needs to be `time`
44+
* `--memory` - filters out any results that used less memory than specified (in words), the `type` needs to be `memory`
45+
* `--sort` - sorts the results by `calls` or by the value of `type` (default: the value of `type`)
3346
* `--eval`, `-e` - evaluates the given code
3447
* `--require`, `-r` - requires pattern before running the command
3548
* `--parallel`, `-p` - makes all requires parallel
@@ -106,8 +119,10 @@ defmodule Mix.Tasks.Profile.Tprof do
106119
halt: :boolean,
107120
compile: :boolean,
108121
deps_check: :boolean,
122+
type: :string,
109123
calls: :integer,
110124
time: :integer,
125+
memory: :integer,
111126
sort: :string,
112127
start: :boolean,
113128
archives_check: :boolean,
@@ -163,8 +178,14 @@ defmodule Mix.Tasks.Profile.Tprof do
163178
end
164179
end
165180

181+
defp parse_opt({:type, "time"}), do: {:type, :time}
182+
defp parse_opt({:type, "calls"}), do: {:type, :calls}
183+
defp parse_opt({:type, "memory"}), do: {:type, :memory}
184+
defp parse_opt({:type, other}), do: Mix.raise("Invalid type option: #{other}")
185+
166186
defp parse_opt({:sort, "time"}), do: {:sort, :time}
167187
defp parse_opt({:sort, "calls"}), do: {:sort, :calls}
188+
defp parse_opt({:sort, "memory"}), do: {:sort, :memory}
168189
defp parse_opt({:sort, other}), do: Mix.raise("Invalid sort option: #{other}")
169190
defp parse_opt(other), do: other
170191

@@ -178,18 +199,25 @@ defmodule Mix.Tasks.Profile.Tprof do
178199
* `:matching` - only profile calls matching the given pattern in form of
179200
`{module, function, arity}`, where each element may be replaced by `:_`
180201
to allow any value
202+
* `:type` - the type of profiling, possible values are `:time`, `:memory` or `:calls`,
203+
(default: `:time`), see [moduledoc](`Mix.Tasks.Profile.Tprof`) for more information
204+
181205
* `:calls` - filters out any results with a call count lower than this
182-
* `:time` - filters out any results that took lower than specified (in µs)
183-
* `:sort` - sort the results by `:time` or `:calls` (default: `:time`)
206+
* `:time` - filters out any results that took lower than specified (in µs),
207+
`type` needs to be `:time`
208+
* `:memory` - filters out any results that used less memory than specified (in words),
209+
`type` needs to be `:memory`
210+
* `:sort` - sort the results by `:calls` or by the value of `type`
211+
(default: the value of `type`)
184212
* `:warmup` - if the code should be warmed up before profiling (default: `true`)
185213
* `:set_on_spawn` - if newly spawned processes should be measured (default: `true`)
186214
187215
"""
188216
@spec profile((-> result), keyword()) :: result when result: any()
189217
def profile(fun, opts \\ []) when is_function(fun, 0) do
190218
Mix.ensure_application!(:tools)
191-
{return_value, results} = profile_and_analyse(fun, opts)
192-
print_output(results)
219+
{type, return_value, results} = profile_and_analyse(fun, opts)
220+
print_output(type, results)
193221
return_value
194222
end
195223

@@ -202,101 +230,141 @@ defmodule Mix.Tasks.Profile.Tprof do
202230
:tprof.start()
203231
matching = Keyword.get(opts, :matching, {:_, :_, :_})
204232
set_on_spawn = Keyword.get(opts, :set_on_spawn, true)
205-
{:ok, return_value} = :tprof.profile([], fun, matching, set_on_spawn: set_on_spawn)
233+
type = Keyword.get(opts, :type, :time)
234+
235+
sort_by =
236+
case Keyword.get(opts, :sort) do
237+
nil -> :measurement
238+
:calls -> :calls
239+
^type -> :measurement
240+
other -> Mix.raise("Incompatible sort option `#{other}` with type `#{type}`")
241+
end
242+
243+
tprof_type = to_tprof_type(type)
244+
245+
{return_value, {^tprof_type, traces}} =
246+
:tprof.profile(fun, %{
247+
set_on_spawn: set_on_spawn,
248+
pattern: matching,
249+
type: tprof_type,
250+
report: :return
251+
})
252+
253+
inspected = :tprof.inspect({tprof_type, traces}, :process, sort_by)
254+
255+
:tprof.stop()
206256

207257
results =
208-
Enum.map(:tprof.dump(), fn {pid, call_results} ->
258+
inspected
259+
|> Enum.map(fn {pid, {^tprof_type, measurement_total, call_results}} ->
209260
parsed_calls =
210261
call_results
211-
|> filter_results(opts)
212-
|> sort_results(opts)
213-
|> add_totals()
262+
|> filter_results(type, opts)
263+
|> add_totals(measurement_total)
214264

215265
{pid, parsed_calls}
216266
end)
217267

218-
:tprof.stop()
219-
220-
{return_value, results}
268+
{type, return_value, results}
221269
end
222270

223-
defp filter_results(call_results, opts) do
271+
defp to_tprof_type(:calls), do: :call_count
272+
defp to_tprof_type(:time), do: :call_time
273+
defp to_tprof_type(:memory), do: :call_memory
274+
275+
defp filter_results(call_results, type, opts) do
224276
calls_opt = Keyword.get(opts, :calls, 0)
225-
time_opt = Keyword.get(opts, :time, 0)
226277

227-
call_results
228-
|> Stream.filter(fn {_mfa, {count, _time}} -> count >= calls_opt end)
229-
|> Stream.filter(fn {_mfa, {_count, time}} -> time >= time_opt end)
278+
measurement_opt =
279+
get_filter_value!(type, Keyword.get(opts, :time), Keyword.get(opts, :memory))
280+
281+
Enum.filter(call_results, fn {_module, _fa, count, measurement, _, _} ->
282+
count >= calls_opt and measurement >= measurement_opt
283+
end)
230284
end
231285

232-
defp sort_results(call_results, opts) do
233-
Enum.sort_by(call_results, sort_function(Keyword.get(opts, :sort, :time)))
286+
defp get_filter_value!(type, time, _memory) when is_integer(time) and type != :time do
287+
Mix.raise("Incompatible use of time option with type `#{type}`")
234288
end
235289

236-
defp sort_function(:time), do: fn {_mfa, {_count, time}} -> time end
237-
defp sort_function(:calls), do: fn {_mfa, {count, _time}} -> count end
290+
defp get_filter_value!(type, _time, memory) when is_integer(memory) and type != :memory do
291+
Mix.raise("Incompatible use of memory option with type `#{type}`")
292+
end
293+
294+
defp get_filter_value!(:time, time, nil) when is_integer(time), do: time
295+
defp get_filter_value!(:memory, nil, memory) when is_integer(memory), do: memory
296+
297+
defp get_filter_value!(_, nil, nil), do: 0
238298

239-
defp add_totals(call_results) do
240-
{function_count, call_count, total_time} =
241-
Enum.reduce(call_results, {0, 0, 0}, fn {_, {count, time}}, acc ->
242-
{function_count, call_count, total_time} = acc
243-
{function_count + 1, call_count + count, total_time + time}
299+
defp add_totals(call_results, measurement_total) do
300+
{function_count, calls} =
301+
Enum.reduce(call_results, {0, 0}, fn {_mod, _fa, count, _, _, _}, acc ->
302+
{function_count, calls} = acc
303+
{function_count + 1, calls + count}
244304
end)
245305

246-
{function_count, call_results, call_count, total_time}
306+
{function_count, call_results, calls, measurement_total}
247307
end
248308

249-
@header ["#", "CALLS", "%", "TIME", "µS/CALL"]
250-
251-
defp print_output([]) do
309+
defp print_output(_type, []) do
252310
print_function_count(0)
253311
end
254312

255-
defp print_output(results) do
256-
Enum.each(results, &print_result/1)
313+
defp print_output(type, results) do
314+
Enum.each(results, &print_result(type, &1))
257315
end
258316

259-
defp print_result({pid, {function_count, call_results, call_count, total_time}}) do
260-
formatted_rows = Enum.map(call_results, &format_row(&1, total_time))
261-
formatted_total = format_total(total_time, call_count)
317+
defp print_result(type, {pid, {function_count, call_results, calls, total_measurement}}) do
318+
header = header(type)
319+
320+
formatted_rows = Enum.map(call_results, &format_row/1)
321+
formatted_total = format_total(total_measurement, calls)
262322

263-
column_lengths = column_lengths(@header, formatted_rows)
323+
column_lengths = column_lengths(header, [formatted_total | formatted_rows])
264324

265325
IO.puts("")
266326

267327
print_pid_row(pid)
268-
print_row(@header, column_lengths)
269-
print_row(formatted_total, column_lengths)
270-
Enum.each(formatted_rows, &print_row(&1, column_lengths))
328+
print_row(header, column_lengths, type)
329+
print_row(formatted_total, column_lengths, type)
330+
Enum.each(formatted_rows, &print_row(&1, column_lengths, type))
271331

272332
IO.puts("")
273333

274334
print_function_count(function_count)
275335
end
276336

277-
defp print_pid_row(pid) do
337+
defp header(:calls), do: ["#", "CALLS", "%"]
338+
defp header(:time), do: ["#", "CALLS", "%", "TIME", "µS/CALL"]
339+
defp header(:memory), do: ["#", "CALLS", "%", "WORDS", "PER CALL"]
340+
341+
defp print_pid_row(:all) do
342+
IO.puts("Profile results over all processes")
343+
end
344+
345+
defp print_pid_row(pid) when is_pid(pid) do
278346
IO.puts("Profile results of #{inspect(pid)}")
279347
end
280348

281-
defp format_row({{module, function, arity}, {count, time}}, total_time) do
349+
defp format_row({module, {function, arity}, count, measurement, per_call, percentage}) do
282350
mfa = Exception.format_mfa(module, function, arity)
283-
time_percentage = :erlang.float_to_binary(100 * divide(time, total_time), [{:decimals, 2}])
284-
time_per_call = :erlang.float_to_binary(divide(time, count), [{:decimals, 2}])
351+
percentage = :erlang.float_to_binary(percentage, [{:decimals, 2}])
352+
per_call = :erlang.float_to_binary(per_call, [{:decimals, 2}])
285353
count = Integer.to_string(count)
286-
time = Integer.to_string(time)
354+
measurement = Integer.to_string(measurement)
287355

288-
[mfa, count, time_percentage, time, time_per_call]
356+
[mfa, count, percentage, measurement, per_call]
289357
end
290358

291-
defp format_total(total_time, total_count) do
292-
time_per_call = :erlang.float_to_binary(divide(total_time, total_count), [{:decimals, 2}])
359+
defp format_total(total_measurement, total_count) do
360+
per_call = :erlang.float_to_binary(divide(total_measurement, total_count), [{:decimals, 2}])
293361

294362
[
295363
"Total",
296364
Integer.to_string(total_count),
297365
"100.00",
298-
Integer.to_string(total_time),
299-
time_per_call
366+
Integer.to_string(total_measurement),
367+
per_call
300368
]
301369
end
302370

@@ -307,24 +375,23 @@ defmodule Mix.Tasks.Profile.Tprof do
307375
max_lengths = Enum.map(header, &String.length/1)
308376

309377
Enum.reduce(rows, max_lengths, fn row, max_lengths ->
310-
Stream.map(row, &String.length/1)
311-
|> Stream.zip(max_lengths)
312-
|> Enum.map(&max/1)
378+
Enum.zip_with(row, max_lengths, fn cell, length -> String.length(cell) |> max(length) end)
313379
end)
314380
end
315381

316-
defp max({a, b}) when a >= b, do: a
317-
defp max({_, b}), do: b
318-
319-
@format "~-*s ~*s ~*s ~*s ~*s~n"
382+
@call_format "~-*s ~*s ~*s~n"
383+
@measurement_format "~-*s ~*s ~*s ~*s ~*s~n"
320384

321-
defp print_row(row, column_lengths) do
385+
defp print_row(row, column_lengths, type) do
322386
to_print =
323387
column_lengths
324388
|> Stream.zip(Stream.map(row, &String.to_charlist/1))
325389
|> Enum.flat_map(&Tuple.to_list/1)
326390

327-
:io.format(@format, to_print)
391+
case type do
392+
:calls -> :io.format(@call_format, to_print)
393+
_ -> :io.format(@measurement_format, to_print)
394+
end
328395
end
329396

330397
defp print_function_count(count) do

0 commit comments

Comments
 (0)