@@ -6,8 +6,10 @@ defmodule Mix.Tasks.Profile.Tprof do
6
6
@ moduledoc """
7
7
Profiles the given file or expression using Erlang's `tprof` tool.
8
8
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.
11
13
12
14
Before running the code, it invokes the `app.start` task which compiles
13
15
and loads your project. After that, the target expression is profiled together
@@ -21,15 +23,26 @@ defmodule Mix.Tasks.Profile.Tprof do
21
23
$ mix profile.tprof -e "[1, 2, 3] |> Enum.reverse |> Enum.map(&Integer.to_string/1)"
22
24
$ mix profile.tprof my_script.exs arg1 arg2 arg3
23
25
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
+
24
35
This task is automatically re-enabled, so you can profile multiple times
25
36
in the same Mix invocation.
26
37
27
38
## Command line options
28
39
29
40
* `--matching` - only profile calls matching the given `Module.function/arity` pattern
41
+ * `--type` - the type of profiling, `calls`, `time` or `memory` (default: `time`)
30
42
* `--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`)
33
46
* `--eval`, `-e` - evaluates the given code
34
47
* `--require`, `-r` - requires pattern before running the command
35
48
* `--parallel`, `-p` - makes all requires parallel
@@ -106,8 +119,10 @@ defmodule Mix.Tasks.Profile.Tprof do
106
119
halt: :boolean ,
107
120
compile: :boolean ,
108
121
deps_check: :boolean ,
122
+ type: :string ,
109
123
calls: :integer ,
110
124
time: :integer ,
125
+ memory: :integer ,
111
126
sort: :string ,
112
127
start: :boolean ,
113
128
archives_check: :boolean ,
@@ -163,8 +178,14 @@ defmodule Mix.Tasks.Profile.Tprof do
163
178
end
164
179
end
165
180
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
+
166
186
defp parse_opt ( { :sort , "time" } ) , do: { :sort , :time }
167
187
defp parse_opt ( { :sort , "calls" } ) , do: { :sort , :calls }
188
+ defp parse_opt ( { :sort , "memory" } ) , do: { :sort , :memory }
168
189
defp parse_opt ( { :sort , other } ) , do: Mix . raise ( "Invalid sort option: #{ other } " )
169
190
defp parse_opt ( other ) , do: other
170
191
@@ -178,18 +199,25 @@ defmodule Mix.Tasks.Profile.Tprof do
178
199
* `:matching` - only profile calls matching the given pattern in form of
179
200
`{module, function, arity}`, where each element may be replaced by `:_`
180
201
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
+
181
205
* `: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`)
184
212
* `:warmup` - if the code should be warmed up before profiling (default: `true`)
185
213
* `:set_on_spawn` - if newly spawned processes should be measured (default: `true`)
186
214
187
215
"""
188
216
@ spec profile ( ( -> result ) , keyword ( ) ) :: result when result: any ( )
189
217
def profile ( fun , opts \\ [ ] ) when is_function ( fun , 0 ) do
190
218
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 )
193
221
return_value
194
222
end
195
223
@@ -202,101 +230,141 @@ defmodule Mix.Tasks.Profile.Tprof do
202
230
:tprof . start ( )
203
231
matching = Keyword . get ( opts , :matching , { :_ , :_ , :_ } )
204
232
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 ( )
206
256
207
257
results =
208
- Enum . map ( :tprof . dump ( ) , fn { pid , call_results } ->
258
+ inspected
259
+ |> Enum . map ( fn { pid , { ^ tprof_type , measurement_total , call_results } } ->
209
260
parsed_calls =
210
261
call_results
211
- |> filter_results ( opts )
212
- |> sort_results ( opts )
213
- |> add_totals ( )
262
+ |> filter_results ( type , opts )
263
+ |> add_totals ( measurement_total )
214
264
215
265
{ pid , parsed_calls }
216
266
end )
217
267
218
- :tprof . stop ( )
219
-
220
- { return_value , results }
268
+ { type , return_value , results }
221
269
end
222
270
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
224
276
calls_opt = Keyword . get ( opts , :calls , 0 )
225
- time_opt = Keyword . get ( opts , :time , 0 )
226
277
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 )
230
284
end
231
285
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 } `" )
234
288
end
235
289
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
238
298
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 }
244
304
end )
245
305
246
- { function_count , call_results , call_count , total_time }
306
+ { function_count , call_results , calls , measurement_total }
247
307
end
248
308
249
- @ header [ "#" , "CALLS" , "%" , "TIME" , "µS/CALL" ]
250
-
251
- defp print_output ( [ ] ) do
309
+ defp print_output ( _type , [ ] ) do
252
310
print_function_count ( 0 )
253
311
end
254
312
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 ) )
257
315
end
258
316
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 )
262
322
263
- column_lengths = column_lengths ( @ header , formatted_rows )
323
+ column_lengths = column_lengths ( header , [ formatted_total | formatted_rows ] )
264
324
265
325
IO . puts ( "" )
266
326
267
327
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 ) )
271
331
272
332
IO . puts ( "" )
273
333
274
334
print_function_count ( function_count )
275
335
end
276
336
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
278
346
IO . puts ( "Profile results of #{ inspect ( pid ) } " )
279
347
end
280
348
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
282
350
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 } ] )
285
353
count = Integer . to_string ( count )
286
- time = Integer . to_string ( time )
354
+ measurement = Integer . to_string ( measurement )
287
355
288
- [ mfa , count , time_percentage , time , time_per_call ]
356
+ [ mfa , count , percentage , measurement , per_call ]
289
357
end
290
358
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 } ] )
293
361
294
362
[
295
363
"Total" ,
296
364
Integer . to_string ( total_count ) ,
297
365
"100.00" ,
298
- Integer . to_string ( total_time ) ,
299
- time_per_call
366
+ Integer . to_string ( total_measurement ) ,
367
+ per_call
300
368
]
301
369
end
302
370
@@ -307,24 +375,23 @@ defmodule Mix.Tasks.Profile.Tprof do
307
375
max_lengths = Enum . map ( header , & String . length / 1 )
308
376
309
377
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 )
313
379
end )
314
380
end
315
381
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"
320
384
321
- defp print_row ( row , column_lengths ) do
385
+ defp print_row ( row , column_lengths , type ) do
322
386
to_print =
323
387
column_lengths
324
388
|> Stream . zip ( Stream . map ( row , & String . to_charlist / 1 ) )
325
389
|> Enum . flat_map ( & Tuple . to_list / 1 )
326
390
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
328
395
end
329
396
330
397
defp print_function_count ( count ) do
0 commit comments