File 6671-Process-heap-profiler.patch of Package erlang
From ce539592d9b5948f39d51de045bd94149686500d Mon Sep 17 00:00:00 2001
From: Maxim Fedorov <maximfca@gmail.com>
Date: Thu, 22 Sep 2022 13:47:27 -0700
Subject: [PATCH] Process heap profiler
Similar to eprof for time profiling, hprof enables
heap profiling for Erlang processes. It simplifies selection
of processes to trace and allows programmatic analysis.
---
lib/tools/doc/src/Makefile | 1 +
lib/tools/doc/src/hprof.xml | 726 +++++++++++++++++++++++++++++++++
lib/tools/doc/src/ref_man.xml | 4 +
lib/tools/doc/src/specs.xml | 1 +
lib/tools/src/Makefile | 1 +
lib/tools/src/hprof.erl | 690 +++++++++++++++++++++++++++++++
lib/tools/test/Makefile | 1 +
lib/tools/test/hprof_SUITE.erl | 310 ++++++++++++++
8 files changed, 1734 insertions(+)
create mode 100644 lib/tools/doc/src/hprof.xml
create mode 100644 lib/tools/src/hprof.erl
create mode 100644 lib/tools/test/hprof_SUITE.erl
diff --git a/lib/tools/doc/src/Makefile b/lib/tools/doc/src/Makefile
index 6b1e2b050f..18e51cc51f 100644
--- a/lib/tools/doc/src/Makefile
+++ b/lib/tools/doc/src/Makefile
@@ -35,6 +35,7 @@ XML_APPLICATION_FILES = ref_man.xml
XML_REF3_FILES = \
cover.xml \
eprof.xml \
+ hprof.xml \
fprof.xml \
cprof.xml \
lcnt.xml \
diff --git a/lib/tools/doc/src/hprof.xml b/lib/tools/doc/src/hprof.xml
new file mode 100644
index 0000000000..d21a74cb9a
--- /dev/null
+++ b/lib/tools/doc/src/hprof.xml
@@ -0,0 +1,726 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE erlref SYSTEM "erlref.dtd">
+
+<!-- %ExternalCopyright% -->
+
+<erlref>
+ <header>
+ <copyright>
+ <year>2023</year><year>2023</year>
+ <holder>Maxim Fedorov, WhatsApp Inc.</holder>
+ </copyright>
+ <legalnotice>
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ </legalnotice>
+
+ <title>hprof</title>
+ <prepared>maximfca@gmail.com</prepared>
+ <docno></docno>
+ <date></date>
+ <rev></rev>
+ </header>
+ <module since="OTP 26.0">hprof</module>
+ <modulesummary>Process Heap Profiling Tool</modulesummary>
+ <description>
+ <p><c>hprof</c> provides convenience helpers for Erlang process heap
+ profiling. Underlying mechanism is the Erlang trace BIFs.</p>
+
+ <p>Heap profiling can be done ad-hoc, to understand heap allocations
+ done by your program, or run in a server-aided mode for deeper
+ introspection of the code running in production.</p>
+
+ <warning><p>
+ Avoid hot code reload for modules that are participating in
+ the memory tracing. Reloading a module turns tracing off,
+ discarding accumulated statistics. <c>hprof</c>
+ may not report correct amounts when code reload happened
+ during profiling session.
+ </p></warning>
+
+ <p>Heap allocations happen for all Erlang terms that do not fit
+ a single machine word. For example, a function returning tuple
+ of 2 elements needs to allocate this tuple on the process heap.
+ Actual consumption is more than 2 machine words, because Erlang
+ runtime needs to store tuple size and other internal information.
+ </p>
+ <note>
+ <p>When profiling is enabled, expect a slowdown in the program
+ execution.</p>
+ <p>
+ For profiling convenience, heap allocations are accumulated for
+ functions that are not enabled in trace pattern. Consider this
+ call stack example:</p>
+ <code type="none"><![CDATA[
+ top_traced_function(...)
+ not_traced_function()
+ bottom_traced_function()
+ ]]></code>
+ <p>Allocations that happened within <c>not_traced_function</c>
+ will be accounted into <c>top_traced_function</c>. However
+ allocations happened within <c>bottom_traced_function</c> are
+ not included in the <c>top_traced_function</c>. If you want to
+ account only own allocations, you need to trace all functions.</p>
+ </note>
+ </description>
+
+ <section>
+ <title>Ad-hoc profiling</title>
+ <p>
+ Basic profiling providing accumulated memory allocation data. You can choose
+ to print per-process statistics, total statistics, or omit printing and
+ extract machine-readable data that you can later sort/print:
+ </p>
+
+ <code type="none"><![CDATA[
+ 1> hprof:profile(lists, seq, [1, 16]).
+
+ ****** Process <0.179.0> -- 100.00 % of total allocations ***
+ MODUL FUN/ARITY CALLS WORDS PER CALL [ %]
+ lists seq_loop/3 5 32 6 [100.00]
+ 32 [ 100.0]
+ ok
+ ]]></code>
+
+ <p>
+ By default tracing is enabled for all functions of all modules. When
+ functions are created in the interactive shell, parts of shell code
+ are also traced. It is however possible to limit the trace to specific
+ functions or modules:
+ </p>
+
+ <code type="none"><![CDATA[
+ 1> hprof:profile(fun() -> lists:seq(1, 16) end).
+
+ ****** Process <0.224.0> -- 100.00 % of total allocations ***
+ MODULE FUN/ARITY CALLS WORDS PER CALL [ %]
+ erl_eval match_list/6 1 3 3 [ 3.19]
+ erl_eval do_apply/7 1 3 3 [ 3.19]
+ lists reverse/1 1 4 4 [ 4.26]
+ erl_eval add_bindings/2 1 5 5 [ 5.32]
+ erl_eval expr_list/7 3 7 2 [ 7.45]
+ erl_eval ret_expr/3 4 16 4 [17.02]
+ erl_eval merge_bindings/4 3 24 8 [25.53]
+ lists seq_loop/3 5 32 6 [34.04]
+
+ 2> hprof:profile(fun() -> lists:seq(1, 16) end, #{pattern => [{lists, seq_loop, '_'}]}).
+ ****** Process <0.247.0> -- 100.00 % of total allocations ***
+ MODUL FUN/ARITY CALLS WORDS PER CALL [ %]
+ lists seq_loop/3 5 32 6 [100.00]
+ ]]></code>
+
+ <p>
+ Ad-hoc profiling results may be printed in a few different ways. Following
+ examples are using <c>test</c> module defined like this:
+ </p>
+
+ <code type="erl">
+ -module(test).
+ -export([test_spawn/0]).
+ test_spawn() ->
+ {Pid, MRef} = spawn_monitor(fun () -> lists:seq(1, 32) end),
+ receive
+ {'DOWN', MRef, process, Pid, normal} ->
+ done
+ end.
+ </code>
+
+ <p>Default format prints per-process statistics.</p>
+ <code type="none"><![CDATA[
+ 2> hprof:profile(test, test_spawn, []).
+
+ ****** Process <0.176.0> -- 23.66 % of total allocations ***
+ MODULE FUN/ARITY CALLS WORDS PER CALL [ %]
+ erlang spawn_monitor/1 1 2 2 [ 9.09]
+ erlang spawn_opt/4 1 6 6 [27.27]
+ test test_spawn/0 1 14 14 [63.64]
+ 22 [100.0]
+
+ ****** Process <0.177.0> -- 76.34 % of total allocations ***
+ MODULE FUN/ARITY CALLS WORDS PER CALL [ %]
+ erlang apply/2 1 7 7 [ 9.86]
+ lists seq_loop/3 9 64 7 [90.14]
+ 71 [100.0]
+ ]]></code>
+
+ <p>This example prints the combined memory allocation of
+ all processes, sorted by total allocated words in the descending order</p>
+ <code type="none"><![CDATA[
+ 5> hprof:profile(test, test_spawn, [], #{report => {total, {words, descending}}}).
+
+ MODULE FUN/ARITY CALLS WORDS PER CALL [ %]
+ lists seq_loop/3 9 64 7 [68.82]
+ test test_spawn/0 1 14 14 [15.05]
+ erlang apply/2 1 7 7 [ 7.53]
+ erlang spawn_opt/4 1 6 6 [ 6.45]
+ erlang spawn_monitor/1 1 2 2 [ 2.15]
+ 93 [100.0]
+ ]]></code>
+
+ <p>You can also collect the profile for further inspection.</p>
+ <code type="none"><![CDATA[
+ 6> {done, ProfileData} = hprof:profile(fun test:test_spawn/0, #{report => return}).
+ <...>
+ 7> hprof:format(hprof:inspect(ProfileData, process, {percent, descending})).
+
+ ****** Process <0.223.0> -- 23.66 % of total allocations ***
+ MODULE FUN/ARITY CALLS WORDS PER CALL [ %]
+ test test_spawn/0 1 14 14 [63.64]
+ erlang spawn_opt/4 1 6 6 [27.27]
+ erlang spawn_monitor/1 1 2 2 [ 9.09]
+ 22 [100.0]
+
+ ****** Process <0.224.0> -- 76.34 % of total allocations ***
+ MODULE FUN/ARITY CALLS WORDS PER CALL [ %]
+ lists seq_loop/3 9 64 7 [90.14]
+ erlang apply/2 1 7 7 [ 9.86]
+ 71 [100.0]
+ ]]></code>
+
+ <p>
+ By default, basic profiling takes into account all processes spawned
+ from the user-provided function (using <c>set_on_spawn</c> argument for
+ trace/3 BIF). You can limit the trace to a single process:
+ </p>
+
+ <code type="none"><![CDATA[
+ 2> hprof:profile(test, test_spawn, [], #{set_on_spawn => false}).
+
+ ****** Process <0.183.0> -- 100.00 % of total allocations ***
+ MODULE FUN/ARITY CALLS WORDS PER CALL [ %]
+ erlang spawn_monitor/1 1 2 2 [ 9.09]
+ erlang spawn_opt/4 1 6 6 [27.27]
+ test test_spawn/0 1 14 14 [63.64]
+ ]]></code>
+
+ <marker id="pg_example"/>
+ <p>
+ Erlang programs may perform memory-intensive operations in processes
+ that are different from the original one. You can include multiple, new or
+ even all processes in the trace.
+ </p>
+
+ <code type="none"><![CDATA[
+ 7> pg:start_link().
+ {ok,<0.252.0>}
+ 8> hprof:profile(fun () -> pg:join(group, self()) end, #{rootset => [pg]}).
+ ****** Process <0.252.0> -- 52.86 % of total allocations ***
+ MODULE FUN/ARITY CALLS WORDS PER CALL [ %]
+ pg leave_local_update_ets/5 1 2 2 [ 1.80]
+ gen reply/2 1 3 3 [ 2.70]
+ erlang monitor/2 1 3 3 [ 2.70]
+ gen_server try_handle_call/4 1 3 3 [ 2.70]
+ gen_server try_dispatch/4 1 3 3 [ 2.70]
+ maps iterator/1 2 4 2 [ 3.60]
+ maps take/2 1 6 6 [ 5.41]
+ pg join_local_update_ets/5 1 8 8 [ 7.21]
+ pg handle_info/2 1 8 8 [ 7.21]
+ pg handle_call/3 1 9 9 [ 8.11]
+ gen_server loop/7 2 9 4 [ 8.11]
+ ets lookup/2 2 10 5 [ 9.01]
+ pg join_local/3 1 11 11 [ 9.91]
+ pg notify_group/5 2 16 8 [14.41]
+ erlang setelement/3 2 16 8 [14.41]
+ 111 [100.0]
+
+ ****** Process <0.255.0> -- 47.14 % of total allocations ***
+ MODULE FUN/ARITY CALLS WORDS PER CALL [ %]
+ erl_eval match_list/6 1 3 3 [ 3.03]
+ erlang monitor/2 1 3 3 [ 3.03]
+ lists reverse/1 2 4 2 [ 4.04]
+ pg join/3 1 4 4 [ 4.04]
+ erl_eval add_bindings/2 1 5 5 [ 5.05]
+ erl_eval do_apply/7 2 6 3 [ 6.06]
+ gen call/4 1 8 8 [ 8.08]
+ erl_eval expr_list/7 4 10 2 [10.10]
+ gen do_call/4 1 16 16 [16.16]
+ erl_eval ret_expr/3 4 16 4 [16.16]
+ erl_eval merge_bindings/4 3 24 8 [24.24]
+ 99 [100.0]
+ ]]></code>
+
+ <p>
+ There is no default limit on the profiling time. It is possible to
+ define such limit for ad-hoc profile. If function being profiled
+ does not return in a specified amount of time, process is terminated
+ with <c>kill</c> reason. Any unlinked children started by the
+ user-supplied function are kept, it is developer's responsibility
+ to ensure cleanup.
+ </p>
+
+ <code type="none">
+ 9> hprof:profile(timer, sleep, [100000], #{timeout => 1000}).
+ </code>
+
+ <p>By default, only one ad-hoc or server-aided profiling session is allowed
+ at any point in time. It is possible to force multiple ad-hoc sessions
+ concurrently, but it is developer responsibility to ensure non-overlapping
+ trace patterns.
+ </p>
+
+ <code type="none">
+ 1> hprof:profile(fun() -> lists:seq(1, 32) end,
+ #{registered => false, pattern => [{lists, '_', '_'}]}).
+ </code>
+
+ </section>
+
+ <section>
+ <title>Server-aided profiling</title>
+ <p>
+ Memory profiling can be done when your system is up and running. You
+ can start the <c>hprof</c> server, add trace patterns and processes
+ to trace while your system handles actual traffic. You can extract
+ the data any time, inspect, and print. The example below traces
+ activity of all processes supervised by kernel:
+ </p>
+
+ <code type="none"><![CDATA[
+ 1> hprof:start().
+ {ok,<0.200.0>}
+ 2> hprof:enable_trace({all_children, kernel_sup}).
+ 34
+ 3> hprof:set_pattern('_', '_' , '_').
+ 16728
+ 4> Sample = hprof:collect().
+ [{gen_server,try_dispatch,4,[{<0.154.0>,2,6}]},
+ {erlang,iolist_to_iovec,1,[{<0.161.0>,1,8}]},
+ <...>
+ 5 > hprof:format(hprof:inspect(Sample)).
+
+ ****** Process <0.154.0> -- 14.21 % of total allocations ***
+ MODULE FUN/ARITY CALLS WORDS PER CALL [ %]
+ maps iterator/1 2 4 2 [15.38]
+ gen_server try_dispatch/4 2 6 3 [23.08]
+ net_kernel handle_info/2 2 16 8 [61.54]
+ 26 [100.0]
+
+ ****** Process <0.161.0> -- 85.79 % of total allocations ***
+ MODULE FUN/ARITY CALLS WORDS PER CALL [ %]
+ disk_log handle/2 2 2 1 [ 1.27]
+ disk_log_1 maybe_start_timer/1 1 3 3 [ 1.91]
+ disk_log_1 mf_write_cache/1 1 3 3 [ 1.91]
+ <...>
+ ]]></code>
+
+ <marker id="inspect_example"/>
+ <p>
+ It is possible to profile the entire running system, and then examine
+ individual processes:
+ </p>
+ <code type="none"><![CDATA[
+ 1> hprof:start(), hprof:enable_trace(processes), hprof:set_pattern('_', '_' , '_').
+ 9041
+ 2> timer:sleep(10000), hprof:disable_trace(processes), Sample = hprof:collect().
+ [{user_drv,server,3,[{<0.64.0>,12,136}]},
+ {user_drv,contains_ctrl_g_or_ctrl_c,1,[{<0.64.0>,80,10}]},
+ <...>
+ 3> Inspected = hprof:inspect(Sample, process, words), Shell = maps:get(self(), Inspected).
+ {2743,
+ [{shell,{enc,0},1,2,2,0.07291286912139992},
+ <...>
+ 4> hprof:format(Shell).
+
+ MODULE FUN/ARITY CALLS WORDS PER CALL [ %]
+ <...>
+ erl_lint start/2 2 300 150 [10.94]
+ shell used_records/1 114 342 3 [12.47]
+ ]]></code>
+ </section>
+
+ <datatypes>
+ <datatype>
+ <name name="process"/>
+ <desc>
+ <p>Either process identified (pid), or a registered process name.</p>
+ </desc>
+ </datatype>
+ <datatype>
+ <name name="trace_map"/>
+ <desc>
+ <p>Traced functions (with their arities) grouped by module name.</p>
+ </desc>
+ </datatype>
+ <datatype>
+ <name name="trace_pattern"/>
+ </datatype>
+ <datatype>
+ <name name="trace_options"/>
+ <desc>
+ <p>Options for enabling heap profiling of the selected processes,
+ see <seemfa marker="#enable_trace/2"><c>enable_trace/2</c></seemfa>.</p>
+ </desc>
+ </datatype>
+ <datatype>
+ <name name="trace_info"/>
+ <desc>
+ <p>Raw data extracted from tracing BIFs.</p>
+ </desc>
+ </datatype>
+ <datatype>
+ <name name="rootset"/>
+ </datatype>
+ <datatype>
+ <name name="profile_options"/>
+ <desc>
+ <p>Ad-hoc profiler options,
+ see <seemfa marker="#profile/2"><c>profile</c></seemfa>.</p>
+ </desc>
+ </datatype>
+ <datatype>
+ <name name="profile_line"/>
+ <desc>
+ <p>Inspected data for a single function of the specified <c>Module</c>.</p>
+ </desc>
+ </datatype>
+ <datatype>
+ <name name="profile_result"/>
+ <desc>
+ <p>Profile of a single process, or combined profile of multiple
+ processes, sorted by a selected column.</p>
+ </desc>
+ </datatype>
+ <datatype>
+ <name name="sort_by"/>
+ </datatype>
+ <datatype>
+ <name name="column"/>
+ <desc>
+ <p>Column to sort by <seemfa marker="#inspect/3"><c>inspect/3</c></seemfa>, or
+ <seemfa marker="#profile/2"><c>profile</c></seemfa>.</p>
+ <taglist>
+ <tag><c>module</c></tag>
+ <item>Module name.
+ </item>
+ <tag><c>function</c></tag>
+ <item>Function name.
+ </item>
+ <tag><c>calls</c></tag>
+ <item>Number of calls to the function.
+ </item>
+ <tag><c>words</c></tag>
+ <item>Total number of words allocated throughout all calls to the function.
+ </item>
+ <tag><c>words_per_call</c></tag>
+ <item>Number of words allocated on average per function call.
+ </item>
+ <tag><c>percent</c></tag>
+ <item>Percentage of <c>words</c> to a total amount allocated during the
+ entire profile collection.
+ </item>
+ </taglist>
+ </desc>
+ </datatype>
+ </datatypes>
+
+ <funcs>
+ <func>
+ <name name="start" arity="0" since="OTP 26.0"/>
+ <fsummary>Start profiling server.</fsummary>
+ <desc>
+ <p> Starts the server, not supervised.
+ Profiling server stores current trace patterns and
+ ensures a single instance of heap profiler is running.</p>
+ </desc>
+ </func>
+ <func>
+ <name name="start_link" arity="0" since="OTP 26.0"/>
+ <fsummary>Start profiling server.</fsummary>
+ <desc>
+ <p> Starts the server, supervised by the calling process.</p>
+ </desc>
+ </func>
+ <func>
+ <name name="stop" arity="0" since="OTP 26.0"/>
+ <fsummary>Stop profiling server.</fsummary>
+ <desc>
+ <p>Stops the <c>hprof</c>, disabling memory tracing that has
+ been enabled.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name name="set_pattern" arity="3" since="OTP 26.0"/>
+ <fsummary>Enables memory tracing for a specific trace pattern.</fsummary>
+ <desc>
+ <p>Turns tracing on for the supplied pattern.
+ Requires running <c>hprof</c>. Patterns are additive, following
+ the same rules as <seemfa marker="erts:erlang#trace_pattern/3">
+ <c>erlang:trace_pattern/3</c></seemfa>. Returns number of functions
+ matching the supplied pattern.</p>
+ <code type="none"><![CDATA[
+1> hprof:set_pattern(lists, seq, '_').
+2
+2> hprof:set_pattern(lists, keyfind, 3).
+1
+3> hprof:get_trace_map().
+#{lists => [{keyfind,3},{seq,2},{seq,3}]}
+ ]]></code>
+ <p>If there are no functions matching the pattern, error
+ is returned</p>
+ <code><![CDATA[
+> hprof:set_pattern(no_module, func, '_').
+{error,{trace_pattern,no_module,func,'_'}}
+ ]]></code>
+ </desc>
+ </func>
+
+ <func>
+ <name name="clear_pattern" arity="3" since="OTP 26.0"/>
+ <fsummary>Disables memory tracing for a specific trace pattern.</fsummary>
+ <desc>
+ <p>Turns tracing off for the supplied pattern.</p>
+ <code type="none"><![CDATA[
+1> hprof:set_pattern(lists, seq, '_').
+2
+2> hprof:clear_pattern(lists, seq, 3).
+1
+3> hprof:get_trace_map().
+#{lists => [{seq,2}]}
+ ]]></code>
+ </desc>
+ </func>
+
+ <func>
+ <name name="get_trace_map" arity="0" since="OTP 26.0"/>
+ <fsummary>Returns trace map.</fsummary>
+ <desc>
+ <p>Returns a map of module names to functions with their arities.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name name="enable_trace" arity="1" clause_i="1" since="OTP 26.0"/>
+ <name name="enable_trace" arity="1" clause_i="2" since="OTP 26.0"/>
+ <fsummary>Enables memory tracing for the specified processes</fsummary>
+ <desc><p>
+ The same as <seemfa marker="#enable_trace/2"><c>enable_trace</c>
+ </seemfa><c>(<anno>Spec</anno>, #{set_on_spawn => true})</c>.
+ </p></desc>
+ </func>
+
+ <func>
+ <name name="enable_trace" arity="2" clause_i="1" since="OTP 26.0"/>
+ <name name="enable_trace" arity="2" clause_i="2" since="OTP 26.0"/>
+ <fsummary>Enables memory tracing for the specified processes</fsummary>
+ <desc><p>
+ Similar to <seemfa marker="erts:erlang#trace/3"><c>erlang:trace/3</c></seemfa>,
+ but supports a few more options for
+ heap tracing convenience.
+ </p>
+ <p><c><anno>Spec</anno></c> is either a process identifier
+ (pid) for a local process, one of the following atoms, or a
+ list of local process identifiers or their registered names:</p>
+ <taglist>
+ <tag><c>processes</c></tag>
+ <item>All currently existing processes and all that will be created
+ in the future.
+ </item>
+ <tag><c>existing_processes</c></tag>
+ <item>All currently existing processes.
+ </item>
+ <tag><c>new_processes</c></tag>
+ <item>All processes that will be created in the future.
+ </item>
+ <tag><c>children</c></tag>
+ <item>All currently running processes that were directly spawned by
+ the specified process. This mode is helpful for tracing workers of
+ a single supervisor.
+ </item>
+ <tag><c>all_children</c></tag>
+ <item>All currently running processes that were spawned by the
+ specified process, or any recursive descendant of it. This mode is
+ designed to facilitate tracing of supervision trees.
+ </item>
+ </taglist>
+ <note><p>
+ Heap profiling server does not keep track of processes that were added
+ to the tracing set. It is permitted to stop the profiling server (wiping
+ out any accumulated data), restart the server, set entirely different
+ tracing pattern keeping the list of traced processes for future use.
+ Use <seemfa marker="#disable_trace/2"><c>disable_trace</c>(processes)</seemfa>
+ to clear the list of traced processes.
+ </p></note>
+ <p>Specify <c><anno>Options</anno></c> to modify heap tracing behaviour:</p>
+ <taglist>
+ <tag><c>set_on_spawn</c></tag>
+ <item>Automatically start heap tracing for processes spawned by the traced
+ process. On by default.</item>
+ </taglist>
+ </desc>
+ </func>
+
+ <func>
+ <name name="disable_trace" arity="1" clause_i="1" since="OTP 26.0"/>
+ <name name="disable_trace" arity="1" clause_i="2" since="OTP 26.0"/>
+ <fsummary>Disables memory tracing for the specified processes</fsummary>
+ <desc><p>
+ The same as <seemfa marker="#disable_trace/2"><c>disable_trace</c>
+ </seemfa><c>(<anno>Spec</anno>, #{set_on_spawn => true})</c>.
+ </p></desc>
+ </func>
+
+ <func>
+ <name name="disable_trace" arity="2" clause_i="1" since="OTP 26.0"/>
+ <name name="disable_trace" arity="2" clause_i="2" since="OTP 26.0"/>
+ <fsummary>Disables memory tracing for the specified processes</fsummary>
+ <desc><p>Stops accumulating heap traces for specified processes. See
+ <seemfa marker="#enable_trace/2"><c>enable_trace/2</c></seemfa> for
+ options description.
+ </p>
+ <p>Profile accumulated before process is removed from the traced
+ list is retained. This allows to enable tracing for many or even
+ all processes in the system, sleep for a short period of time,
+ then disable tracing for all processes, avoiding system overload,
+ but keeping profile data.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name name="pause" arity="0" since="OTP 26.0"/>
+ <fsummary>Pauses heap tracing.</fsummary>
+ <desc>
+ <p>Pauses trace collection for all currently traced functions, keeping all
+ traces intact. Use <seemfa marker="#continue/0"><c>continue/0</c></seemfa> to
+ resume trace collection.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name name="continue" arity="0" since="OTP 26.0"/>
+ <fsummary>Resumes heap tracing.</fsummary>
+ <desc>
+ <p>Resumes previously paused heap profiling.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name name="restart" arity="0" since="OTP 26.0"/>
+ <fsummary>Clears profiling data and restarts tracing.</fsummary>
+ <desc>
+ <p>Clears accumulated profiles. If profiling was paused prior to
+ calling <c>restart</c>, it gets continued.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name name="profile" arity="1" since="OTP 26.0"/>
+ <name name="profile" arity="2" since="OTP 26.0"/>
+ <name name="profile" arity="3" since="OTP 26.0"/>
+ <name name="profile" arity="4" since="OTP 26.0"/>
+ <fsummary>Produces ad-hoc heap profile for the function.</fsummary>
+ <desc>
+ <p>Produces ad-hoc heap profile for function <c>Fun</c> or
+ <c>Module</c>:<c>Function</c> call. By default, result
+ is formatted to the output device, use <c>report</c> option to change
+ this behaviour.</p>
+ <p>Ad-hoc profiling starts a new instance of <c>hprof</c> server, runs
+ the profiling routine, extracts results and shuts the server down. If
+ <c>hprof</c> is already running (for server-aided profiling), default
+ ad-hoc profiler options block this call to avoid mixing results from
+ several independent instances. Use <c>registered => false</c> option to
+ override this behaviour.</p>
+ <p>Ad-hoc profiler supports following<c>Options</c>:</p>
+ <taglist>
+ <tag><c>device</c></tag>
+ <item>Specifies I/O devices to print the profile to. Useful to redirect
+ text output to console or <c>standard_error</c>.
+ </item>
+ <tag><c>pattern</c></tag>
+ <item>Specifies trace pattern, or a list of trace patterns to enable.
+ By default, all functions (<c>{'_', '_', '_'}</c>) are traced.
+ </item>
+ <tag><c>registered</c></tag>
+ <item>Specifies <c>hprof</c> registered process name. Use <c>false</c>
+ to leave the process unregistered, or <c>{local, myname}</c> to register
+ the process under a different name.
+ </item>
+ <tag><c>report</c></tag>
+ <item>Controls output format. The default is <c>process</c>, printing
+ per-process heap profiling data sorted by percentage of the total
+ allocation. Specify <c>report => return</c> to suppress printing and
+ get the raw data for further evaluation with
+ <seemfa marker="#inspect/3"><c>inspect/3</c></seemfa> and formatting
+ with <seemfa marker="#format/2"><c>format/2</c></seemfa>.
+ </item>
+ <tag><c>rootset</c></tag>
+ <item>Includes extra processes in the trace list. Useful for profiling
+ allocations for <seeerl marker="stdlib:gen_server"><c>gen_server</c></seeerl>,
+ calls, or other allocations caused by inter-process communications. See
+ <seeerl marker="hprof#pg_example">example</seeerl>.
+ </item>
+ <tag><c>set_on_spawn</c></tag>
+ <item>Automatically start heap tracing for processes spawned by the traced
+ process. On by default.
+ </item>
+ <tag><c>timeout</c></tag>
+ <item>Terminate profiling after the specified amount of time (milliseconds).
+ </item>
+ </taglist>
+ </desc>
+ </func>
+
+ <func>
+ <name name="format" arity="1" since="OTP 26.0"/>
+ <name name="format" arity="2" since="OTP 26.0"/>
+ <fsummary>Formats transformed profile to a specified device.</fsummary>
+ <desc>
+ <p>Formats profile transformed with <seemfa marker="#inspect/3"><c>inspect</c>
+ </seemfa> to a specified device.</p>
+ </desc>
+ </func>
+
+ <func>
+ <name name="inspect" arity="1" clause_i="1" since="OTP 26.0"/>
+ <fsummary>Transforms raw data returned by heap profiler.</fsummary>
+ <desc><p>
+ The same as <seemfa marker="#inspect/3"><c>inspect</c>
+ </seemfa><c>(<anno>Profile</anno>, process, percent)</c>. Transforms
+ raw profile into a map of process identifiers to a tuple containing
+ total count of words allocated, and a list of all traced functions
+ sorted in the ascending order by the allocation percentage.
+ </p></desc>
+ </func>
+
+ <func>
+ <name name="inspect" arity="3" clause_i="1" since="OTP 26.0"/>
+ <fsummary>Transforms raw data returned by heap profiler.</fsummary>
+ <desc>
+ <p>Transforms raw data returned by tracing BIFs into a form
+ convenient for subsequent analysis and formatting. Returns
+ a map of process identifiers with corresponding profile data sorted by
+ the selected column.</p>
+ <p>
+ Inspected profile can be leveraged to print
+ <seeerl marker="hprof#inspect_example">individual process
+ allocations</seeerl>.
+ </p>
+ </desc>
+ </func>
+
+ <func>
+ <name name="inspect" arity="3" clause_i="2" since="OTP 26.0"/>
+ <fsummary>Transforms raw data returned by heap profiler into a summary.</fsummary>
+ <desc>
+ <p>Combines raw profile from multiple processes into a single summary
+ sorted by the selected column.</p>
+ <p>
+ A single profiling session may contain data from thousands or even millions
+ processes. This inspection mode allows to quickly glance through the allocation
+ summary, discarding process identifiers and keeping only totals.
+ </p>
+ </desc>
+ </func>
+ </funcs>
+</erlref>
+
diff --git a/lib/tools/doc/src/ref_man.xml b/lib/tools/doc/src/ref_man.xml
index c2962eeb84..1fe9c6e261 100644
--- a/lib/tools/doc/src/ref_man.xml
+++ b/lib/tools/doc/src/ref_man.xml
@@ -52,6 +52,9 @@
Erlang programs. Uses trace to file to minimize runtime
performance impact, and displays time for calling and called
functions.</item>
+ <tag><em>hprof</em></tag>
+ <item>A heap profiling tool; measures how much heap space is
+ allocated by Erlang processes.</item>
<tag><em>lcnt</em></tag>
<item>A lock profiling tool for the Erlang runtime system.</item>
@@ -71,6 +74,7 @@
<xi:include href="eprof.xml"/>
<xi:include href="erlang_mode.xml"/>
<xi:include href="fprof.xml"/>
+ <xi:include href="hprof.xml"/>
<xi:include href="lcnt.xml"/>
<xi:include href="make.xml"/>
<xi:include href="tags.xml"/>
diff --git a/lib/tools/doc/src/specs.xml b/lib/tools/doc/src/specs.xml
index 143b54f0a9..d7e1002a6c 100644
--- a/lib/tools/doc/src/specs.xml
+++ b/lib/tools/doc/src/specs.xml
@@ -5,6 +5,7 @@
<xi:include href="../specs/specs_make.xml"/>
<xi:include href="../specs/specs_lcnt.xml"/>
<xi:include href="../specs/specs_eprof.xml"/>
+ <xi:include href="../specs/specs_hprof.xml"/>
<xi:include href="../specs/specs_tags.xml"/>
<xi:include href="../specs/specs_cover.xml"/>
<xi:include href="../specs/specs_xref.xml"/>
diff --git a/lib/tools/src/Makefile b/lib/tools/src/Makefile
index 254e0c7196..67826bfd37 100644
--- a/lib/tools/src/Makefile
+++ b/lib/tools/src/Makefile
@@ -38,6 +38,7 @@ RELSYSDIR = $(RELEASE_PATH)/lib/tools-$(VSN)
MODULES= \
cover \
eprof \
+ hprof \
fprof \
cprof \
lcnt \
diff --git a/lib/tools/src/hprof.erl b/lib/tools/src/hprof.erl
new file mode 100644
index 0000000000..0222c5a39e
--- /dev/null
+++ b/lib/tools/src/hprof.erl
@@ -0,0 +1,690 @@
+%%
+%%
+%% Copyright WhatsApp Inc. and its affiliates. All rights reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%%
+%%-------------------------------------------------------------------
+%%
+%% @author Maxim Fedorov <maximfca@gmail.com>
+%% Erlang Process Heap profiler.
+%%
+-module(hprof).
+
+%% API
+-export([
+ start/0,
+ start_link/0,
+ stop/0,
+ set_pattern/3,
+ clear_pattern/3,
+ get_trace_map/0,
+ enable_trace/1, enable_trace/2,
+ disable_trace/1, disable_trace/2,
+ pause/0,
+ continue/0,
+ restart/0,
+ collect/0,
+ %% ad-hoc profiling
+ profile/1, profile/2, profile/3, profile/4,
+ %% Analysis API
+ inspect/1, inspect/3,
+ format/1, format/2
+]).
+
+%% gen_server callbacks
+-export([
+ init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2
+]).
+
+-compile([warn_missing_spec]).
+
+%% typedefs for easier digestion
+
+%% Trace spec: module() or '_', function or '_', arity or '_'
+-type trace_pattern() :: {module(), Fun :: atom(), arity() | '_'}.
+
+%% Trace map: accumulated view of multiple trace patterns
+-type trace_map() :: #{module() => [{Fun :: atom(), arity()}]}.
+
+%% Single trace_info call with associated module/function/arity
+-type trace_info() :: {module(), Fun :: atom(), Arity :: non_neg_integer(),
+ [{pid(), Count :: pos_integer(), Words :: pos_integer()}]}.
+
+%% Combined report for a single function (one or all processes).
+-type profile_line() :: {module(), Function :: {atom(), arity()},
+ Count :: pos_integer(), Words :: pos_integer(), WordsPerCall :: non_neg_integer(), Percent :: float()}.
+
+%% Single profiling attempt result.
+-type profile_result() :: {TotalWords :: non_neg_integer(), [profile_line()]}.
+
+%% Convenience type used to sort the profiling results.
+-type column() :: module | function | calls | words | words_per_call | percent.
+
+%% Sort by
+-type sort_by() :: column() | {column(), ascending} | {column(), descending}.
+
+%% Selected options allowed for enable/disable trace
+-type trace_options() :: #{
+ set_on_spawn => boolean()
+}.
+
+%% Convenience type to define which processes to trace
+-type rootset() :: [process()] | %% list of pids/registered names
+ processes |
+ existing_processes |
+ new_processes.
+
+-type profile_options() :: #{
+ timeout => timeout(), %% stop profiling after the timeout
+ pattern => trace_pattern() | [trace_pattern()], %% list of patterns to trace
+ set_on_spawn => boolean(), %% trace spawned processes or not (true by default)
+ rootset => rootset(), %% extra processes to trace
+ report => return | process | total | {process, sort_by()} | {total, sort_by()}, %% print or return results
+ device => io:device(), %% device to report to
+ registered => false | {local, atom()} %% register the profiler process (to detect concurrent attempts)
+}.
+
+%%--------------------------------------------------------------------
+%% Server-aided API
+-spec start() -> {'ok', Pid} | {'error', Reason} when Pid :: pid(), Reason :: {'already_started', Pid}.
+start() ->
+ gen_server:start({local, ?MODULE}, ?MODULE, [], []).
+
+%% @doc Starts the process and links it to the caller.
+-spec start_link() -> {'ok', Pid} | {'error', Reason} when Pid :: pid(), Reason :: {'already_started', Pid}.
+start_link() ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+-spec stop() -> ok.
+stop() ->
+ gen_server:stop(?MODULE).
+
+-spec set_pattern(module(), atom(), arity() | '_') -> ok | {error, {trace_pattern, trace_pattern()}}.
+set_pattern(Mod, Fun, Arity) ->
+ gen_server:call(?MODULE, {set_pattern, Mod, Fun, Arity}, infinity).
+
+%% @doc Stops tracing all or specific function patterns.
+-spec clear_pattern(module(), atom(), arity() | '_') -> ok.
+clear_pattern(Mod, Fun, Arity) ->
+ gen_server:call(?MODULE, {clear_pattern, Mod, Fun, Arity}, infinity).
+
+%% @doc Returns current trace map.
+-spec get_trace_map() -> trace_map().
+get_trace_map() ->
+ gen_server:call(?MODULE, get_trace_map).
+
+%% @doc Returns statistics for current trace map.
+-spec collect() -> [trace_info()].
+collect() ->
+ gen_server:call(?MODULE, collect, infinity).
+
+%% Process identified by a PID or a registered name.
+-type process() :: pid() | atom().
+
+%% @doc Shortcut for erlang:trace/3 BIF touching only memory tracing flags.
+%% Returns number of successful operations, and list of those unsuccessful
+%% if the list was supplied. By default applies set_on_spawn flag.
+-spec enable_trace(Spec) -> non_neg_integer()
+ when Spec :: pid() |
+ processes |
+ new_processes |
+ existing_processes |
+ {children | all_children, process()};
+ ([process()]) -> non_neg_integer() | {non_neg_integer(), [process()]}.
+enable_trace(Rootset) ->
+ enable_trace(Rootset, #{set_on_spawn => true}).
+
+-spec enable_trace(Spec, trace_options()) -> non_neg_integer()
+ when Spec :: pid() |
+ processes |
+ new_processes |
+ existing_processes |
+ {children | all_children, process()};
+ ([process()], trace_options()) -> non_neg_integer() | {non_neg_integer(), [process()]}.
+enable_trace(Procs, Options) when Procs =:= processes; Procs =:= new_processes; Procs =:= existing_processes ->
+ erlang:trace(Procs, true, trace_options(Options));
+enable_trace({Children, PidOrName}, Options) when Children =:= children; Children =:= all_children ->
+ Pids = children(Children, PidOrName),
+ toggle_trace(Pids, true, trace_options(Options), 0, []);
+enable_trace(Pid, Options) when is_pid(Pid); is_atom(Pid) ->
+ toggle_process_trace(Pid, true, trace_options(Options));
+enable_trace(List, Options) when is_list(List) ->
+ toggle_trace(List, true, trace_options(Options), 0, []).
+
+-spec disable_trace(Spec) -> non_neg_integer()
+ when Spec :: pid() |
+ processes |
+ new_processes |
+ existing_processes |
+ {children | all_children, process()};
+ ([process()]) -> non_neg_integer() | {non_neg_integer(), [process()]}.
+disable_trace(Rootset) ->
+ disable_trace(Rootset, #{set_on_spawn => true}).
+
+-spec disable_trace(Spec, trace_options()) -> non_neg_integer()
+ when Spec :: pid() |
+ processes |
+ new_processes |
+ existing_processes |
+ {children | all_children, process()};
+ ([process()], trace_options()) -> non_neg_integer() | {non_neg_integer(), [process()]}.
+disable_trace(Procs, Options) when Procs =:= processes; Procs =:= new_processes; Procs =:= existing_processes ->
+ erlang:trace(Procs, false, trace_options(Options));
+disable_trace({Children, PidOrName}, Options) when Children =:= children; Children =:= all_children ->
+ Pids = children(Children, PidOrName),
+ toggle_trace(Pids, false, trace_options(Options), 0, []);
+disable_trace(Pid, Options) when is_pid(Pid); is_atom(Pid) ->
+ toggle_process_trace(Pid, false, trace_options(Options));
+disable_trace(List, Options) when is_list(List) ->
+ toggle_trace(List, false, trace_options(Options), 0, []).
+
+%% @doc Pauses tracing for the entire trace_map
+-spec pause() -> ok | not_running.
+pause() ->
+ gen_server:call(?MODULE, pause, infinity).
+
+%% @doc Continues paused tracing.
+-spec continue() -> ok | not_paused.
+continue() ->
+ gen_server:call(?MODULE, continue, infinity).
+
+%% @doc Restarts tracing, clearing current statistics. Profiling could be
+%% running or paused.
+-spec restart() -> ok.
+restart() ->
+ gen_server:call(?MODULE, restart, infinity).
+
+%%--------------------------------------------------------------------
+%% Common API
+
+%% @doc Transforms raw collected data into shape suitable for analysis and printing.
+-spec inspect([trace_info()]) -> #{pid() => profile_result()}.
+inspect(Profile) ->
+ inspect(Profile, process, percent).
+
+-spec inspect([trace_info()], Report :: process, sort_by()) -> #{pid() => profile_result()};
+ ([trace_info()], Report :: total, sort_by()) -> profile_result().
+inspect(Profile, process, SortBy) ->
+ maps:map(
+ fun (_Pid, {Total, Stats}) ->
+ {Total, inspect_sort(Stats, SortBy)}
+ end, inspect_processes(Profile, #{}));
+inspect(Profile, total, SortBy) ->
+ GrandTotal = lists:sum([Words || {_M, _F, _A, Mem} <- Profile, {_P, _C, Words} <- Mem]),
+ TotalStats = [inspect_total(M, F, A, GrandTotal, Mem) || {M, F, A, Mem} <- Profile],
+ {GrandTotal, inspect_sort(TotalStats, SortBy)}.
+
+%% @doc Formats inspect()-ed totals and per-function data
+-spec format(profile_result() | #{pid => profile_result()}) -> ok.
+format(Inspected) ->
+ format_impl([], Inspected).
+
+-spec format(io:device(), profile_result() | #{pid => profile_result()}) -> ok.
+format(IoDevice, Inspected) ->
+ format_impl(IoDevice, Inspected).
+
+%%--------------------------------------------------------------------
+%% Ad-hoc API
+
+%% @doc Runs the function/MFA with heap tracing enabled.
+-spec profile(fun(() -> term())) -> ok | {term(), [trace_info()]}.
+profile(Fun) when is_function(Fun) ->
+ profile(Fun, #{}).
+
+-spec profile(fun(() -> term()), profile_options()) -> ok | {term(), [trace_info()]}.
+profile(Fun, Options) when is_function(Fun) ->
+ do_profile(Fun, Options).
+
+-spec profile(module(), Fun :: atom(), Args :: [term()]) -> ok | {term(), [trace_info()]}.
+profile(Module, Function, Args) when is_atom(Module), is_atom(Function), is_list(Args) ->
+ profile(Module, Function, Args, #{}).
+
+-spec profile(module(), Fun :: atom(), Args :: [term()], profile_options()) -> ok | {term(), [trace_info()]}.
+profile(Module, Function, Args, Options) when is_atom(Module), is_atom(Function), is_list(Args) ->
+ do_profile({Module, Function, Args}, Options).
+
+%%--------------------------------------------------------------------
+%% gen_server implementation
+-record(hprof_state, {
+ trace_map = #{} :: trace_map(),
+ paused = false :: boolean(),
+ ad_hoc = undefined :: undefined |
+ {pid(), Timer :: reference() | false, Patterns :: [trace_pattern()],
+ RootSet :: rootset(), ReplyTo :: gen_server:from()}
+}).
+
+-type state() :: #hprof_state{}.
+
+-spec init([]) -> {ok, state()}.
+init([]) ->
+ false = erlang:process_flag(trap_exit, true), %% need this for reliable terminate/2 call
+ {ok, #hprof_state{}}.
+
+-spec handle_call(term(), gen_server:from(), state()) -> {reply | noreply, term(), state()}.
+handle_call({set_pattern, M, F, A}, _From, #hprof_state{trace_map = Map} = State) ->
+ {Reply, NewMap} = enable_pattern(M, F, A, Map),
+ {reply, Reply, State#hprof_state{trace_map = NewMap}};
+handle_call({clear_pattern, M, F, A}, _From, #hprof_state{trace_map = Map} = State) ->
+ {Ret, NewMap} = disable_pattern(M, F, A, Map),
+ {reply, Ret, State#hprof_state{trace_map = NewMap}};
+handle_call(get_trace_map, _From, #hprof_state{trace_map = Map} = State) ->
+ {reply, Map, State};
+handle_call(pause, _From, #hprof_state{paused = true} = State) ->
+ {reply, not_running, State};
+handle_call(pause, _From, #hprof_state{trace_map = Map, paused = false} = State) ->
+ foreach(Map, pause),
+ {reply, ok, State#hprof_state{paused = true}};
+handle_call(continue, _From, #hprof_state{paused = false} = State) ->
+ {reply, running, State};
+handle_call(continue, _From, #hprof_state{trace_map = Map} = State) ->
+ foreach(Map, true),
+ {reply, ok, State#hprof_state{paused = false}};
+handle_call(restart, _From, #hprof_state{trace_map = Map} = State) ->
+ foreach(Map, restart),
+ {reply, ok, State#hprof_state{paused = false}};
+handle_call(collect, _From, #hprof_state{trace_map = Map} = State) ->
+ {reply, collect(Map), State};
+handle_call({profile, What, Options}, From, #hprof_state{ad_hoc = undefined, trace_map = Map} = State) ->
+ %% ad-hoc profile routed via gen_server to handle 'EXIT' signal
+ {Pid, Timer, Patterns, RootSet, NewMap} = ad_hoc_run(What, Options, Map),
+ {noreply, State#hprof_state{ad_hoc = {Pid, Timer, Patterns, RootSet, From}, trace_map = NewMap}};
+handle_call({profile, _What, _Options}, _From, State) ->
+ {reply, {error, running}, State}.
+
+-spec handle_cast(term(), state()) -> no_return().
+handle_cast(_Req, _State) ->
+ erlang:error(notsup).
+
+-spec handle_info(term(), state()) -> {noreply, state()}.
+handle_info({'EXIT', Pid, Reason}, #hprof_state{ad_hoc = {Pid, Timer, Patterns, RootSet, From},
+ trace_map = Map} = State) ->
+ _ = disable_trace(RootSet),
+ Profile = collect(Map),
+ gen:reply(From, {Reason, Profile}),
+ Timer =/= false andalso erlang:cancel_timer(Timer),
+ {noreply, State#hprof_state{ad_hoc = undefined, trace_map = disable_patterns(Patterns, Map)}};
+
+handle_info({cancel, Pid}, #hprof_state{ad_hoc = {Pid, _Timer, Patterns, RootSet, From},
+ trace_map = Map} = State) ->
+ _ = disable_trace(RootSet),
+ Profile = collect(Map),
+ gen:reply(From, {{'EXIT', timeout}, Profile}),
+ {noreply, State#hprof_state{ad_hoc = undefined, trace_map = disable_patterns(Patterns, Map)}}.
+
+-spec terminate(term(), state()) -> ok.
+terminate(_Reason, #hprof_state{trace_map = Map}) ->
+ clear_pattern(Map),
+ ok.
+
+%%--------------------------------------------------------------------
+%% Internal implementation
+
+-include_lib("kernel/include/logger.hrl").
+
+%% Add the trace of the specified module to the accumulator
+collect_trace(Mod, FunList, Acc) ->
+ {Fail, Ret} = lists:foldl(
+ fun ({Fun, Arity}, {Fail, Prev}) ->
+ case combine_trace(erlang:trace_info({Mod, Fun, Arity}, call_memory)) of
+ skip ->
+ {Fail, Prev};
+ fail ->
+ {true, Prev};
+ Tr ->
+ {Fail, [{Mod, Fun, Arity, Tr} | Prev]}
+ end
+ end, {false, Acc}, FunList),
+ %% module may have been hot-code reloaded, or tracing was broken by something else
+ Fail andalso begin
+ ?LOG_WARNING(
+ "hprof encountered an error tracing module ~s, was it reloaded or untraced?",
+ [Mod])
+ end,
+ Ret.
+
+combine_trace({call_memory, []}) ->
+ skip;
+%% It is possible that due to hot code reload event
+%% some function is no longer traced, while it was supposed to.
+%% Reinstating tracing automatically is wrong thing to do, because
+%% statistics won't be correct anyway. Hence the warning in the user
+%% guide, guarding against hot code reload while tracing.
+combine_trace({call_memory, false}) ->
+ fail;
+combine_trace({call_memory, Mem}) ->
+ case [{Pid, Calls, Words} || {Pid, Calls, Words} <- Mem, Words > 0] of
+ [] ->
+ skip;
+ NonZero ->
+ NonZero
+ end.
+
+%% Inspection: iterate over collected traces, return map of
+%% #{Pid => [{M, {F, A}, Calls, TotalWords, WordsPerCall, Percentage}], unsorted.
+inspect_processes([], Acc) ->
+ maps:map(
+ fun (_Pid, {Total, Lines}) ->
+ {Total, [{M, {F, A}, Calls, Words, PerCall, Words * 100 / Total}
+ || {M, F, A, Calls, Words, PerCall} <- Lines]}
+ end, Acc);
+inspect_processes([{_M, _F, _A, []} | Tail], Acc) ->
+ inspect_processes(Tail, Acc);
+inspect_processes([{M, F, A, [{Pid, Calls, Words} | MemTail]} | Tail], Acc) ->
+ ProfLine = {M, F, A, Calls, Words, Words div Calls},
+ inspect_processes([{M, F, A, MemTail} | Tail],
+ maps:update_with(Pid, fun ({Grand, L}) -> {Grand + Words, [ProfLine | L]} end, {Words, [ProfLine]}, Acc)).
+
+%% Inspection: remove Pid information from the Profile, return list of
+%% [{M, F, A, TotalCalls, TotalWords, WordsPerCall, Percentage}]
+inspect_total(M, F, A, GrandTotal, Mem) ->
+ {TC, TW} = lists:foldl(
+ fun ({_Pid, Calls, Words}, {TotalCalls, TotalWords}) ->
+ {TotalCalls + Calls, TotalWords + Words}
+ end, {0, 0}, Mem),
+ {M, {F, A}, TC, TW, TW div TC, TW * 100 / GrandTotal}.
+
+%% Returns "sort by" column index
+column(module) -> {1, ascending};
+column(function) -> {2, ascending};
+column(calls) -> {3, ascending};
+column(words) -> {4, ascending};
+column(words_per_call) -> {5, ascending};
+column(percent) -> {6, ascending}.
+
+%% Sorts by column name, ascending/descending
+inspect_sort(Profile, undefined) ->
+ Profile;
+inspect_sort(Profile, {Column, ascending}) when is_integer(Column) ->
+ lists:keysort(Column, Profile);
+inspect_sort(Profile, {Column, descending}) when is_integer(Column) ->
+ lists:reverse(lists:keysort(Column, Profile));
+inspect_sort(Profile, {Column, Direction}) when is_atom(Column) ->
+ {Col, _Skip} = column(Column),
+ inspect_sort(Profile, {Col, Direction});
+inspect_sort(Profile, Column) when is_atom(Column) ->
+ inspect_sort(Profile, column(Column)).
+
+%% Formats the inspected profile to the Device, which could be [] meaning
+%% default output.
+format_impl(Device, Empty) when Empty =:= #{} ->
+ format_out(Device, "Memory trace is empty~n", []);
+format_impl(Device, Inspected) when is_map(Inspected) ->
+ %% grab the total-total words
+ GrandTotal = maps:fold(fun (_Pid, {Total, _Profile}, Acc) -> Acc + Total end, 0, Inspected),
+ %% per-process printout
+ maps:foreach(
+ fun(Pid, {Total, _} = Profile) ->
+ format_out(Device, "~n****** Process ~w -- ~.2f % of total allocations *** ~n",
+ [Pid, 100 * Total / GrandTotal]),
+ format_impl(Device, Profile)
+ end, Inspected);
+format_impl(Device, {Total, Inspected}) when is_list(Inspected) ->
+ %% viewport size
+ %% Viewport = case io:columns() of {ok, C} -> C; _ -> 80 end,
+ %% layout: module and fun/arity columns are resizable, the rest are not
+ %% convert all lines to strings
+ {Widths, Lines} = lists:foldl(
+ fun ({Mod, {F, A}, Calls, Words, WPC, Percent}, {Widths, Ln}) ->
+ Line = [atom_to_list(Mod), lists:flatten(io_lib:format("~tw/~w", [F, A])),
+ integer_to_list(Calls), integer_to_list(Words), integer_to_list(WPC),
+ float_to_list(Percent, [{decimals, 2}])],
+ NewWidths = [erlang:max(Old, New) || {Old, New} <- lists:zip([string:length(L) || L <- Line], Widths)],
+ {NewWidths, [Line | Ln]}
+ end, {[0, 0, 5, 5, 8, 5], []}, Inspected),
+ %% figure our max column widths according to viewport (cut off module/funArity)
+ FilteredWidths = Widths,
+ %% figure out formatting line
+ Fmt = lists:flatten(io_lib:format("~~.~ws ~~.~wts ~~~ws ~~~ws ~~~ws [~~~ws]~~n", FilteredWidths)),
+ %% print using this format
+ format_out(Device, Fmt, ["MODULE", "FUN/ARITY", "CALLS", "WORDS", "PER CALL", "%"]),
+ [format_out(Device, Fmt, Line) || Line <- lists:reverse(Lines)],
+ format_out(Device, Fmt, [" ", " ", " ", integer_to_list(Total), " ", "100.0"]).
+
+%% format implementation that uses [] as a way to tell "default output"
+format_out([], Fmt, Args) ->
+ io:format(Fmt, Args);
+format_out(Device, Fmt, Args) ->
+ io:format(Device, Fmt, Args).
+
+%% pattern collapse code
+enable_pattern('_', '_', '_', _Acc) ->
+ %% need to re-trace everything, probably some new modules were loaded
+ %% discard any existing trace pattern
+ lists:foldl(
+ fun({Mod, _}, {Total, Acc}) ->
+ Plus = erlang:trace_pattern({Mod, '_', '_'}, true, [call_memory]),
+ {Total + Plus, Acc#{Mod => Mod:module_info(functions)}}
+ end, {0, #{}}, code:all_loaded());
+enable_pattern(Mod, '_', '_', Acc) ->
+ %% code may have been hot-loaded, redo the trace
+ case erlang:trace_pattern({Mod, '_', '_'}, true, [call_memory]) of
+ 0 ->
+ {{error, {trace_pattern, Mod, '_', '_'}}, Acc};
+ Traced ->
+ {Traced, Acc#{Mod => Mod:module_info(functions)}}
+ end;
+enable_pattern(Mod, Fun, '_', Acc) ->
+ case erlang:trace_pattern({Mod, Fun, '_'}, true, [call_memory]) of
+ 0 ->
+ {{error, {trace_pattern, Mod, Fun, '_'}}, Acc};
+ Traced ->
+ Added = [{F, A} || {F, A} <- Mod:module_info(functions), F =:= Fun],
+ NewMap = maps:update_with(Mod,
+ fun (FAs) ->
+ Added ++ [{F, A} || {F, A} <- FAs, F =/= Fun]
+ end, Added, Acc),
+ {Traced, NewMap}
+ end;
+enable_pattern(Mod, Fun, Arity, Acc) ->
+ case erlang:trace_pattern({Mod, Fun, Arity}, true, [call_memory]) of
+ 0 ->
+ {{error, {trace_pattern, Mod, Fun, Arity}}, Acc};
+ 1 ->
+ {1, maps:update_with(Mod,
+ fun (FAs) -> [{Fun, Arity} | FAs -- [{Fun, Arity}]] end, [{Fun, Arity}], Acc)}
+ end.
+
+%% pattern collapse code for un-tracing
+disable_pattern('_', '_', '_', _Acc) ->
+ Traced = erlang:trace_pattern({'_', '_', '_'}, false, [call_memory]),
+ {Traced, #{}};
+disable_pattern(Mod, '_', '_', Acc) when is_map_key(Mod, Acc) ->
+ Traced = erlang:trace_pattern({Mod, '_', '_'}, false, [call_memory]),
+ {Traced, maps:remove(Mod, Acc)};
+disable_pattern(Mod, Fun, '_', Acc) when is_map_key(Mod, Acc) ->
+ Traced = erlang:trace_pattern({Mod, Fun, '_'}, false, [call_memory]),
+ {Traced, maps:update_with(Mod,
+ fun (FAs) -> [{F, A} || {F, A} <- FAs, F =/= Fun] end, Acc)};
+disable_pattern(Mod, Fun, Arity, Acc) when is_map_key(Mod, Acc) ->
+ Traced = erlang:trace_pattern({Mod, Fun, Arity}, false, [call_memory]),
+ {Traced, maps:update_with(Mod, fun (FAs) -> FAs -- [{Fun, Arity}] end, Acc)};
+disable_pattern(Mod, Fun, Arity, Acc) ->
+ {{error, {not_traced, Mod, Fun, Arity}}, Acc}.
+
+disable_patterns(Patterns, Map) ->
+ lists:foldl(fun ({M, F, A}, Acc) -> {_, New} = disable_pattern(M, F, A, Acc), New end, Map, Patterns).
+
+%% ad-hoc profiler implementation
+do_profile(What, Options) ->
+ %% start a new hprof server, potentially registered to a new name
+ Pid = start_result(start_internal(maps:get(registered, Options, {local, ?MODULE}))),
+ try
+ {Ret, Profile} = gen_server:call(Pid, {profile, What, Options}, infinity),
+ return_profile(maps:get(report, Options, process), Profile, Ret,
+ maps:get(device, Options, []))
+ after
+ gen_server:stop(Pid)
+ end.
+
+start_internal(false) ->
+ gen_server:start_link(?MODULE, [], []);
+start_internal({local, Name}) ->
+ gen_server:start_link({local, Name}, ?MODULE, [], []).
+
+start_result({ok, Pid}) -> Pid;
+start_result({error, Reason}) -> erlang:error(Reason).
+
+return_profile(return, Profile, Ret, _Device) ->
+ {Ret, Profile};
+return_profile(process, Profile, Ret, Device) ->
+ return_profile({process, percent}, Profile, Ret, Device);
+return_profile(total, Profile, Ret, Device) ->
+ return_profile({total, percent}, Profile, Ret, Device);
+return_profile({Agg, Sort}, Profile, _Ret, Device) ->
+ format_impl(Device, inspect(Profile, Agg, Sort)).
+
+%% @doc clears tracing for the entire trace map passed
+-spec clear_pattern(trace_map()) -> ok.
+clear_pattern(Existing) ->
+ maps:foreach(
+ fun (Mod, FunArity) ->
+ [erlang:trace_pattern({Mod, F, A}, false, [call_memory]) || {F, A} <- FunArity]
+ end, Existing).
+
+trace_options(#{set_on_spawn := false}) ->
+ [call, silent];
+trace_options(_) ->
+ [call, silent, set_on_spawn].
+
+children(Children, PidOrName) when is_atom(PidOrName) ->
+ case erlang:whereis(PidOrName) of
+ undefined -> [];
+ Pid -> children(Children, Pid)
+ end;
+children(children, Pid) when is_pid(Pid) ->
+ [P || P <- erlang:processes(), erlang:process_info(P, parent) =:= {parent, Pid}];
+children(all_children, Pid) when is_pid(Pid) ->
+ %% build a process tree (could use a digraph too)
+ Tree = maps:groups_from_list(
+ fun (P) ->
+ case erlang:process_info(P, parent) of
+ undefined -> undefined;
+ {parent, Parent} -> Parent
+ end
+ end, erlang:processes()),
+ select_pids(Tree, Pid).
+
+select_pids(Tree, Pid) ->
+ case maps:find(Pid, Tree) of
+ error -> [];
+ {ok, Children} ->
+ Children ++ lists:concat([select_pids(Tree, C) || C <- Children])
+ end.
+
+toggle_process_trace(Pid, On, Flags) when is_pid(Pid) ->
+ try
+ 1 = erlang:trace(Pid, On, Flags)
+ catch _:_ ->
+ 0
+ end;
+toggle_process_trace(Name, On, Flags) when is_atom(Name) ->
+ case erlang:whereis(Name) of
+ undefined ->
+ 0;
+ Pid ->
+ toggle_process_trace(Pid, On, Flags)
+ end.
+
+toggle_trace([], _On, _Flags, Success, []) ->
+ Success;
+toggle_trace([], _On, _Flags, Success, Failure) ->
+ {Success, lists:reverse(Failure)};
+toggle_trace([Pid | Tail], On, Flags, Success, Failure) when is_pid(Pid) ->
+ {NS, NF} =
+ try
+ 1 = erlang:trace(Pid, On, Flags),
+ {Success + 1, Failure}
+ catch _:_ ->
+ {Success, [Pid | Failure]}
+ end,
+ toggle_trace(Tail, On, Flags, NS, NF);
+toggle_trace([Name | Tail], On, Flags, Success, Failure) when is_atom(Name) ->
+ case erlang:whereis(Name) of
+ undefined ->
+ toggle_trace(Tail, On, Flags, Success, [Name | Failure]);
+ Pid ->
+ {NS, NF} =
+ try
+ 1 = erlang:trace(Pid, On, Flags),
+ {Success + 1, Failure}
+ catch _:_ ->
+ {Success, [Name | Failure]}
+ end,
+ toggle_trace(Tail, On, Flags, NS, NF)
+ end.
+
+%% @doc Collects memory tracing data (usable for inspect()) for
+%% all traced functions.
+-spec collect(trace_map()) -> [trace_info()].
+collect(Pattern) ->
+ maps:fold(fun collect_trace/3, [], Pattern).
+
+foreach(Map, Action) ->
+ maps:foreach(
+ fun (Mod, Funs) ->
+ [erlang:trace_pattern({Mod, F, A}, Action, [call_memory]) || {F, A} <- Funs]
+ end, Map).
+
+ad_hoc_run(What, Options, Map) ->
+ %% add missing patterns
+ Patterns = make_list(maps:get(pattern, Options, {'_', '_', '_'})),
+ NewMap = lists:foldl(
+ fun({M, F, A}, Acc) ->
+ {_, NewMap} = enable_pattern(M, F, A, Acc),
+ NewMap
+ end, Map, Patterns),
+ %% check whether spawned processes are also traced
+ OnSpawn = maps:get(set_on_spawn, Options, true),
+ %% enable tracing for items in the rootset
+ RootSet = maps:get(rootset, Options, []),
+ _ = enable_trace(RootSet), %% ignore errors when setting up rootset trace
+ %% spawn a separate process to run the user-supplied MFA
+ %% if RootSet is 'processes' or 'new_processes', skip the trace flags
+ Flags = trace_flags(RootSet, OnSpawn),
+ Pid = spawn_profiled(What, Flags),
+ %% start timer to terminate the function being profiled if it takes too long
+ %% to complete
+ Timer = is_map_key(timeout, Options) andalso
+ erlang:send_after(maps:get(timeout, Options), self(), {cancel, Pid}),
+ {Pid, Timer, Patterns, RootSet, NewMap}.
+
+trace_flags(processes, _) -> [];
+trace_flags(new_processes, _) -> [];
+trace_flags(_, true) -> [call, silent, set_on_spawn];
+trace_flags(_, false) -> [call, silent].
+
+make_list({M, F, A}) -> [{M, F, A}];
+make_list(List) -> List.
+
+spawn_profiled(Fun, Flags) when is_function(Fun) ->
+ spawn_link(
+ fun() ->
+ Flags =/= [] andalso begin 1 = erlang:trace(self(), true, Flags) end,
+ Ret = catch Fun(),
+ Flags =/= [] andalso begin 1 = erlang:trace(self(), false, Flags) end,
+ exit(Ret)
+ end);
+spawn_profiled({M, F, A}, Flags) ->
+ spawn_link(
+ fun() ->
+ Flags =/= [] andalso begin 1 = erlang:trace(self(), true, Flags) end,
+ Ret = catch erlang:apply(M, F, A),
+ Flags =/= [] andalso begin 1 = erlang:trace(self(), false, Flags) end,
+ exit(Ret)
+ end).
diff --git a/lib/tools/test/Makefile b/lib/tools/test/Makefile
index bfa1887445..0b233211b0 100644
--- a/lib/tools/test/Makefile
+++ b/lib/tools/test/Makefile
@@ -23,6 +23,7 @@ include $(ERL_TOP)/make/$(TARGET)/otp.mk
MODULES = \
cover_SUITE \
eprof_SUITE \
+ hprof_SUITE \
emacs_SUITE \
fprof_SUITE \
cprof_SUITE \
diff --git a/lib/tools/test/hprof_SUITE.erl b/lib/tools/test/hprof_SUITE.erl
new file mode 100644
index 0000000000..80c40eed1c
--- /dev/null
+++ b/lib/tools/test/hprof_SUITE.erl
@@ -0,0 +1,310 @@
+%%
+%%
+%% Copyright WhatsApp Inc. and its affiliates. All rights reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%%-------------------------------------------------------------------
+%% @author Maxim Fedorov <maximfca@gmail.com>
+%% Basic heap profiler tests.
+-module(hprof_SUITE).
+-author("maximfca@gmail.com").
+
+%% Test server callbacks
+-export([suite/0, all/0]).
+
+%% Test cases exports
+-export([
+ ad_hoc/0, ad_hoc/1,
+ sort/0, sort/1,
+ rootset/0, rootset/1,
+ set_on_spawn/0, set_on_spawn/1, seq/1,
+ live_trace/0, live_trace/1,
+ patterns/0, patterns/1, pattern_fun/1, pattern_fun/2, pattern_fun/3,
+ processes/0, processes/1,
+ server/0, server/1,
+ hierarchy/0, hierarchy/1,
+ code_reload/0, code_reload/1
+]).
+
+-include_lib("stdlib/include/assert.hrl").
+
+%%--------------------------------------------------------------------
+%% COMMON TEST CALLBACK FUNCTIONS
+
+suite() ->
+ [{timetrap, {seconds, 60}}].
+
+all() ->
+ [ad_hoc, sort, rootset, set_on_spawn, live_trace, patterns,
+ processes, server, hierarchy, code_reload].
+
+%%--------------------------------------------------------------------
+%% TEST CASES
+
+ad_hoc() ->
+ [{doc, "Ad-hoc examples from documentation"}].
+
+ad_hoc(Config) when is_list(Config) ->
+ ct:capture_start(),
+ ok = hprof:profile(lists, seq, [1, 16]),
+ ct:capture_stop(),
+ Output = string:lexemes(lists:flatten(ct:capture_get()), "\n"),
+ %% expect third line to contain lists:seq_loop: "lists seq_loop/3 5 32 6 [100.00]",
+ ?assertMatch(["lists", "seq_loop/3", "5", "32" | _], string:lexemes(lists:nth(3, Output), " ")),
+ %% spawn examples
+ SpawnFun =
+ fun () ->
+ {Pid, MRef} = spawn_monitor(fun () -> lists:seq(1, 32) end),
+ receive {'DOWN', MRef, process, Pid, normal} -> done end
+ end,
+ %% trace subset examples
+ {done, Profile1} = hprof:profile(SpawnFun, #{pattern => [{lists, seq_loop, '_'}], report => return}),
+ ?assertMatch([{lists, seq_loop, 3, [{_, 9, 64}]}], Profile1),
+ %% timer
+ {{'EXIT', timeout}, Profile2} = hprof:profile(
+ fun () -> Delay = hd(lists:seq(5001, 5032)), timer:sleep(Delay) end,
+ #{timeout => 1000, report => return}),
+ ?assertMatch([{lists, seq_loop, 3, [{_, 9, 64}]}], Profile2).
+
+sort() ->
+ [{doc, "Tests sorting methods work"}].
+
+sort(Config) when is_list(Config) ->
+ %% sort examples
+ ct:capture_start(),
+ ok = hprof:profile(
+ fun () ->
+ Group = lists:seq(100, 120),
+ rand:uniform(hd(Group))
+ end, #{report => {process, {words_per_call, descending}}}),
+ ct:capture_stop(),
+ Out = string:lexemes(lists:flatten(ct:capture_get()), "\n"),
+ %% words per call is 5th column
+ Col = 5,
+ Column5 = [string:to_integer(lists:nth(Col, Lexemes)) || Ln <- Out,
+ length(Lexemes = string:lexemes(Ln, " ")) >= Col],
+ WPC = [Words || {Words, []} <- Column5],
+ %% ensure descending sort
+ ?assertEqual(lists:reverse(lists:sort(WPC)), WPC).
+
+rootset() ->
+ [{doc, "Tests rootset of processes supplied to ad-hoc profiler"}].
+
+rootset(Config) when is_list(Config) ->
+ TestCase = ?FUNCTION_NAME,
+ {ok, Scope} = pg:start_link(TestCase),
+ Fun = fun () -> ok = pg:join(TestCase, lists:seq(1, 2), self()) end,
+ %% rootset tests
+ {ok, Profile} = hprof:profile(Fun, #{rootset => [TestCase], report => return}),
+ TwoProcs = hprof:inspect(Profile),
+ ?assertEqual(2, maps:size(TwoProcs)), %% must be pg and "profiled process"
+ %% now trace all processes, existing and new
+ {ok, ProfAll} = hprof:profile(Fun, #{rootset => processes, report => return}),
+ %% at least hprof, pg2 and new profiled processes are traced
+ ?assert(map_size(hprof:inspect(ProfAll)) >= 3),
+ gen_server:stop(Scope).
+
+set_on_spawn() ->
+ [{doc, "Tests hprof running with extra spawned processes"}].
+
+set_on_spawn(Config) when is_list(Config) ->
+ %% profile a function that spawns additional process
+ {done, Profile} = hprof:profile(
+ fun () ->
+ {Pid, MRef} = spawn_monitor(fun () -> lists:seq(1, 32) end),
+ receive {'DOWN', MRef, process, Pid, normal} -> done end
+ end, #{report => return, set_on_spawn => false}),
+ {done, ProfileMFA} = hprof:profile(?MODULE, seq, [32], #{report => return, set_on_spawn => false}),
+ %% check totals
+ {_G1, TotalProfile} = hprof:inspect(Profile, total, words),
+ {_G2, TotalProfileMFA} = hprof:inspect(ProfileMFA, total, words),
+ %% only 1 process must be there
+ ?assertEqual(1, maps:size(hprof:inspect(Profile)), {set_on_spawn, Profile}),
+ %% check per-process stats
+ ?assertMatch({?MODULE, {_, 0}, 1, 9, 9, _}, lists:keyfind(?MODULE, 1, TotalProfile)),
+ %% MFA takes 6 more words. This test should be improved to depend less on the internal
+ %% implementation.
+ ?assertMatch({?MODULE, {seq, 1}, 1, 15, 15, _}, lists:keyfind(?MODULE, 1, TotalProfileMFA)).
+
+seq(Max) ->
+ {Pid, MRef} = spawn_monitor(fun () -> lists:seq(1, Max) end),
+ receive {'DOWN', MRef, process, Pid, normal} -> done end.
+
+live_trace() ->
+ [{doc, "Tests memory tracing for pre-existing processes"}].
+
+live_trace(Config) when is_list(Config) ->
+ {ok, _Srv} = hprof:start_link(),
+ Pid = spawn_link(
+ fun () ->
+ receive
+ {From, C} ->
+ [lists:seq(1, 2) || _ <- lists:seq(1, C)],
+ From ! {self(), done}
+ end
+ end),
+ _ = hprof:set_pattern(?MODULE, '_', '_'),
+ 1 = hprof:enable_trace(Pid),
+ Pid ! {self(), 12},
+ receive {Pid, done} -> ok end,
+ catch hprof:disable_trace(Pid),
+ Profile = hprof:collect(),
+ ProcInspected = hprof:inspect(Profile),
+ %% white box check: list comprehension with 1-arity, and 100% allocation
+ %% hprof:format(user, ProcInspected),
+ #{Pid := {48, [{?MODULE, {_LC, 1}, 13, 48, 3, 100.0}]}} = ProcInspected,
+ hprof:stop().
+
+patterns() ->
+ [{doc, "Tests pattern enable/disable correctness"}].
+
+patterns(Config) when is_list(Config) ->
+ {ok, _Srv} = hprof:start_link(),
+ %% test errors
+ ?assertEqual({error, {not_traced, pg, get_members, '_'}}, hprof:clear_pattern(pg, get_members, '_')),
+ ?assertEqual({error, {trace_pattern, ?MODULE, seq, 2}}, hprof:set_pattern(?MODULE, seq, 2)),
+ %% successful patterns
+ 1 = hprof:set_pattern(?MODULE, seq, 1),
+ 3 = hprof:set_pattern(?MODULE, pattern_fun, '_'),
+ 1 = hprof:clear_pattern(?MODULE, pattern_fun, 2),
+ Expected = [{pattern_fun, 1}, {pattern_fun, 3}, {seq, 1}],
+ ?assertEqual(#{?MODULE => Expected}, hprof:get_trace_map()),
+ %% verify tracing flags
+ verify_trace([{?MODULE, F, A} || {F, A} <- Expected], [{?MODULE, pattern_fun, 2}]),
+ %% trace the entire lists module, and then exclude pattern_fun/1,2,3 and seq/1
+ _ = hprof:set_pattern(?MODULE, '_', '_'),
+ 3 = hprof:clear_pattern(?MODULE, pattern_fun, '_'),
+ 1 = hprof:clear_pattern(?MODULE, seq, 1),
+ Cleared = [{pattern_fun, 1}, {pattern_fun, 2}, {pattern_fun, 3}, {seq, 1}],
+ Traced = ?MODULE:module_info(functions) -- Cleared,
+ verify_trace([{?MODULE, F, A} || {F, A} <- Traced], [{?MODULE, F, A} || {F, A} <- Cleared]),
+ %% clear all, which clears lists too
+ _ = hprof:clear_pattern('_', '_', '_'),
+ verify_trace([], [{?MODULE, F, A} || {F, A} <- Traced ++ Cleared]),
+ ?assertEqual(#{}, hprof:get_trace_map()),
+ hprof:stop().
+
+verify_trace(On, Off) ->
+ [?assertEqual({call_memory, []}, erlang:trace_info(MFA, call_memory)) || MFA <- On],
+ [?assertEqual({call_memory, false}, erlang:trace_info(MFA, call_memory)) || MFA <- Off].
+
+pattern_fun(Any) -> Any.
+pattern_fun(Any, Any2) -> {Any, Any2}.
+pattern_fun(Any, Any2, Any3) -> {Any, Any2, Any3}.
+
+processes() ->
+ [{doc, "Tests that process management for enabling/disabling traces works"}].
+
+processes(Config) when is_list(Config) ->
+ Pid = spawn_link(fun spawn_loop/0),
+ Pid2 = spawn_link(fun spawn_loop/0),
+ register(?FUNCTION_NAME, Pid2),
+ %% test a mix of pids/registered processes/single PID calls
+ ?assertEqual(2, hprof:enable_trace([Pid, Pid2])),
+ ?assertEqual(0, hprof:disable_trace('$sure_not_exist')),
+ ?assertEqual({1, ['$sure_not_exist']}, hprof:enable_trace([Pid, '$sure_not_exist'])),
+ ok = gen:stop(Pid),
+ ok = gen:stop(Pid2).
+
+server() ->
+ [{doc, "Tests for gen_server-based API"}].
+
+server(Config) when is_list(Config) ->
+ %% start an extra pg scope - we'll trace it during profiling
+ {ok, Scope} = pg:start_link(?FUNCTION_NAME),
+ %% simulate existing process
+ Pid = spawn_link(fun spawn_loop/0),
+ %% start the profiler
+ {ok, Srv} = hprof:start_link(),
+ %% test ad-hoc profile clash
+ ?assertException(error, {already_started, Srv}, hprof:profile(fun spawn_loop/0)),
+ %% test live trace
+ 1 = hprof:set_pattern(?MODULE, dispatch, '_'),
+ _ = hprof:set_pattern(pg, '_', '_'),
+ %% watch for pg traces and for our process
+ 2 = hprof:enable_trace([Pid, ?FUNCTION_NAME]),
+ %% run the traced operation
+ _ = gen_server:call(Pid, {apply, pg, join, [?FUNCTION_NAME, group, Pid]}),
+ %% collect profile (can save it to a file for later analysis)
+ FirstProfile = hprof:collect(),
+ %% must not be empty, and must contain 3-words dispatch from this module,
+ %% and at least something from pg in two processes
+ ?assertNotEqual([], FirstProfile),
+ ?assertEqual({?MODULE, dispatch, 2, [{Pid, 1, 3}]}, lists:keyfind(?MODULE, 1, FirstProfile)),
+ ?assertMatch({pg, handle_call, 3, [{Scope, _, _}]}, lists:keyfind(handle_call, 2, FirstProfile)),
+ ?assertMatch({pg, join, 3, [{Pid, _, _}]}, lists:keyfind(join, 2, FirstProfile)),
+ %% pause tracing
+ ok = hprof:pause(),
+ %% ensure paused by running more code but keeping the trace
+ %% ensure collection still returns the previous result
+ _ = gen_server:call(Pid, {apply, pg, join, [?FUNCTION_NAME, group, Pid]}),
+ ?assertEqual(FirstProfile, hprof:collect()),
+ %% continue, ensure new results are collected
+ ok = hprof:continue(),
+ _ = gen_server:call(Pid, {apply, pg, leave, [?FUNCTION_NAME, group, [Pid, Pid]]}),
+ ?assertNotEqual(FirstProfile, hprof:collect()),
+ %% restart all counters from zero and ensure that we again collect the original data
+ ok = hprof:restart(),
+ _ = gen_server:call(Pid, {apply, pg, join, [?FUNCTION_NAME, group, Pid]}),
+ ?assertEqual(FirstProfile, hprof:collect()),
+
+ %% test ad-hoc profiling can be done while running server-aided
+ %% for that, profiler should have very specific pattern
+ {_, AdHoc} = hprof:profile(lists, seq, [1, 32], #{registered => false, pattern => {lists, '_', '_'},
+ report => return}),
+ %% check totals: must be 64 words allocated by a single lists:seq_loop
+ ?assertMatch({64, [{lists, _, _, 64, _, _}]}, hprof:inspect(AdHoc, total, words)),
+ %% verify that server-aided version still works
+ ?assertEqual(FirstProfile, hprof:collect()),
+ ok = hprof:stop(),
+ ok = gen_server:stop(Scope),
+ ok = gen:stop(Pid).
+
+%% nano-gen-server purely for tracing
+spawn_loop() ->
+ receive
+ {'$gen_call', From, Call} ->
+ dispatch(Call, From),
+ spawn_loop();
+ {system, From, {terminate,normal}} ->
+ gen:reply(From, ok)
+ end.
+
+dispatch({spawn_link, Fun}, From) ->
+ gen:reply(From, erlang:spawn_link(Fun));
+dispatch({apply, M, F, A}, From) ->
+ gen:reply(From, erlang:apply(M, F, A)).
+
+hierarchy() ->
+ [{doc, "Tests tracing for process hierarchy"}].
+
+hierarchy(Config) when is_list(Config) ->
+ {ok, _Srv} = hprof:start_link(),
+ Traced = hprof:enable_trace({all_children, kernel_sup}),
+ ?assert(Traced > 5),
+ ?assert(hprof:set_pattern(code_server, '_', '_') > 5),
+ _ = code:get_path(), %% makes a call to code_server
+ %% disabling all processes tracing should return more than "children of"
+ ?assert(hprof:disable_trace(processes) > Traced),
+ Profile = hprof:collect(),
+ hprof:stop(),
+ ?assertNotEqual(false, lists:keyfind(handle_call, 2, Profile)).
+
+code_reload() ->
+ [{doc, "Tests that collection does not fail for a hot-code-reloaded module"}].
+
+code_reload(Config) when is_list(Config) ->
+ Sample = hprof:profile(fun () -> code:load_file(?MODULE) end, #{report => return}),
+ %% don't care about actual returned values, but do care that profile/2 does not crash
+ ?assertNotEqual([], Sample).
--
2.35.3